mirror of
https://code.castopod.org/adaures/castopod
synced 2025-05-24 02:52:01 +00:00
feat(plugins): add group field type + multiple option to render field arrays
- update docs - render hint and helper options for all fields - replace option's hint with description
This commit is contained in:
parent
f50098ec89
commit
11ccd0ebe7
@ -23,6 +23,7 @@ import "./modules/video-clip-previewer";
|
|||||||
import VideoClipBuilder from "./modules/VideoClipBuilder";
|
import VideoClipBuilder from "./modules/VideoClipBuilder";
|
||||||
import "./modules/xml-editor";
|
import "./modules/xml-editor";
|
||||||
import "@patternfly/elements/pf-tabs/pf-tabs.js";
|
import "@patternfly/elements/pf-tabs/pf-tabs.js";
|
||||||
|
import FieldArray from "./modules/FieldArray";
|
||||||
|
|
||||||
Dropdown();
|
Dropdown();
|
||||||
Tooltip();
|
Tooltip();
|
||||||
@ -39,3 +40,4 @@ PublishMessageWarning();
|
|||||||
HotKeys();
|
HotKeys();
|
||||||
ValidateFileSize();
|
ValidateFileSize();
|
||||||
VideoClipBuilder();
|
VideoClipBuilder();
|
||||||
|
FieldArray();
|
||||||
|
159
app/Resources/js/modules/FieldArray.ts
Normal file
159
app/Resources/js/modules/FieldArray.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
|
const FieldArray = (): void => {
|
||||||
|
const fieldArrays: NodeListOf<HTMLElement> =
|
||||||
|
document.querySelectorAll("[data-field-array]");
|
||||||
|
|
||||||
|
for (let i = 0; i < fieldArrays.length; i++) {
|
||||||
|
const fieldArray = fieldArrays[i];
|
||||||
|
const fieldArrayContainer = fieldArray.querySelector(
|
||||||
|
"[data-field-array-container]"
|
||||||
|
);
|
||||||
|
const items: NodeListOf<HTMLElement> = fieldArray.querySelectorAll(
|
||||||
|
"[data-field-array-item]"
|
||||||
|
);
|
||||||
|
const addButton = fieldArray.querySelector(
|
||||||
|
"button[data-field-array-add]"
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
|
const deleteButtons: NodeListOf<HTMLButtonElement> =
|
||||||
|
fieldArray.querySelectorAll("[data-field-array-delete]");
|
||||||
|
|
||||||
|
deleteButtons.forEach((deleteBtn) => {
|
||||||
|
deleteBtn.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
deleteBtn.blur();
|
||||||
|
fieldArrayContainer
|
||||||
|
?.querySelector(
|
||||||
|
`[data-field-array-item="${deleteBtn.dataset.fieldArrayDelete}"]`
|
||||||
|
)
|
||||||
|
?.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// create base element to clone
|
||||||
|
const baseItem = items[0].cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
|
const elements: NodeListOf<HTMLFormElement> = baseItem.querySelectorAll(
|
||||||
|
"input, select, textarea"
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fieldArrayContainer && addButton) {
|
||||||
|
addButton.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const newItem = baseItem.cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
|
const deleteBtn: HTMLButtonElement | null = newItem.querySelector(
|
||||||
|
"button[data-field-array-delete]"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener("click", () => {
|
||||||
|
deleteBtn.blur();
|
||||||
|
newItem.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldArrayContainer.appendChild(newItem);
|
||||||
|
newItem.scrollIntoView({
|
||||||
|
behavior: "auto",
|
||||||
|
block: "center",
|
||||||
|
inline: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
// reload tooltip module for showing remove button label
|
||||||
|
Tooltip();
|
||||||
|
|
||||||
|
// focus to first form element if mouse click
|
||||||
|
if (event.screenX !== 0 && event.screenY !== 0) {
|
||||||
|
const elements: NodeListOf<HTMLFormElement> =
|
||||||
|
newItem.querySelectorAll("input, select, textarea");
|
||||||
|
|
||||||
|
if (elements.length > 0) {
|
||||||
|
elements[0].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateIndexes = () => {
|
||||||
|
// get last child item to set item count
|
||||||
|
const items: NodeListOf<HTMLElement> =
|
||||||
|
fieldArrayContainer.querySelectorAll("[data-field-array-item]");
|
||||||
|
|
||||||
|
let itemIndex = 0;
|
||||||
|
items.forEach((item) => {
|
||||||
|
const itemNumber: HTMLElement | null = item.querySelector(
|
||||||
|
"[data-field-array-number]"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (itemNumber) {
|
||||||
|
itemNumber.innerHTML = "#";
|
||||||
|
const indexNum = itemIndex + 1;
|
||||||
|
if (item.dataset.fieldArrayItem !== itemIndex.toString()) {
|
||||||
|
item.classList.add("motion-safe:animate-single-pulse");
|
||||||
|
setTimeout(() => {
|
||||||
|
item.classList.remove("motion-safe:animate-single-pulse");
|
||||||
|
itemNumber.innerHTML = indexNum.toString();
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
itemNumber.innerHTML = indexNum.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.dataset.fieldArrayItem = itemIndex.toString();
|
||||||
|
const deleteBtn = item.querySelector(
|
||||||
|
"button[data-field-array-delete]"
|
||||||
|
) as HTMLButtonElement | null;
|
||||||
|
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.dataset.fieldArrayDelete = itemIndex.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemElements: NodeListOf<HTMLFormElement> =
|
||||||
|
item.querySelectorAll("input, select, textarea");
|
||||||
|
|
||||||
|
itemElements.forEach((element) => {
|
||||||
|
const label: HTMLLabelElement | null = item.querySelector(
|
||||||
|
`label[for="${element.id}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementID = element.name.replace(
|
||||||
|
/(.*\[)\d+?(\].*)/g,
|
||||||
|
`$1${itemIndex}$2`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
label.htmlFor = elementID;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.id = elementID;
|
||||||
|
element.name = elementID;
|
||||||
|
});
|
||||||
|
|
||||||
|
itemIndex++;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// add mutation observer to run index updates when field array
|
||||||
|
// items are added or removed
|
||||||
|
const callback = function (mutationList: MutationRecord[]) {
|
||||||
|
for (const mutation of mutationList) {
|
||||||
|
if (mutation.type === "childList") {
|
||||||
|
updateIndexes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new MutationObserver(callback);
|
||||||
|
|
||||||
|
observer.observe(fieldArrayContainer, { childList: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FieldArray;
|
@ -1,3 +1,13 @@
|
|||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-helper {
|
||||||
|
@apply text-skin-muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.post-content {
|
.post-content {
|
||||||
& a {
|
& a {
|
||||||
@ -78,4 +88,13 @@
|
|||||||
#facc15 20px
|
#facc15 20px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.divide-fieldset-y > :not([hidden], legend) ~ :not([hidden], legend) {
|
||||||
|
@apply pt-4;
|
||||||
|
|
||||||
|
--tw-divide-y-reverse: 0;
|
||||||
|
|
||||||
|
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||||
|
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,33 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
.form-radio-btn {
|
.form-radio-btn {
|
||||||
@apply absolute mt-3 ml-3 border-contrast border-3 text-accent-base;
|
@apply absolute right-4 top-4 border-contrast border-3 text-accent-base;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@apply ring-accent;
|
@apply ring-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
@apply ring-2 ring-contrast;
|
|
||||||
|
|
||||||
& + label {
|
& + label {
|
||||||
@apply text-accent-contrast bg-accent-base;
|
@apply text-accent-hover bg-base border-accent-base shadow-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + label .form-radio-btn-description {
|
||||||
|
@apply text-accent-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& + label {
|
& + label {
|
||||||
@apply inline-flex items-center py-2 pl-8 pr-2 text-sm font-semibold rounded-lg cursor-pointer border-contrast bg-elevated border-3;
|
@apply h-full w-full inline-flex flex-col items-start py-3 px-4 text-sm font-bold rounded-lg cursor-pointer border-contrast bg-elevated border-3 transition-all;
|
||||||
|
|
||||||
color: hsl(var(--color-text-muted));
|
box-shadow: 2px 2px 0 hsl(var(--color-border-contrast));
|
||||||
|
}
|
||||||
|
|
||||||
|
& + label span {
|
||||||
|
@apply pr-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + label .form-radio-btn-description {
|
||||||
|
@apply font-normal text-xs text-skin-muted text-balance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ class Checkbox extends FormComponent
|
|||||||
|
|
||||||
protected string $hint = '';
|
protected string $hint = '';
|
||||||
|
|
||||||
|
protected string $helper = '';
|
||||||
|
|
||||||
protected bool $isChecked = false;
|
protected bool $isChecked = false;
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
@ -37,10 +39,26 @@ class Checkbox extends FormComponent
|
|||||||
'slot' => $this->hint,
|
'slot' => $this->hint,
|
||||||
]))->render();
|
]))->render();
|
||||||
|
|
||||||
$this->mergeClass('inline-flex items-center');
|
$this->mergeClass('inline-flex items-start gap-x-2');
|
||||||
|
|
||||||
|
$helperText = '';
|
||||||
|
if ($this->helper !== '') {
|
||||||
|
$helperId = $this->name . 'Help';
|
||||||
|
$helperText = (new Helper([
|
||||||
|
'id' => $helperId,
|
||||||
|
'slot' => $this->helper,
|
||||||
|
'class' => '-mt-1',
|
||||||
|
]))->render();
|
||||||
|
$this->attributes['aria-describedby'] = $helperId;
|
||||||
|
}
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<label {$this->getStringifiedAttributes()}>{$checkboxInput}<span class="ml-2">{$this->slot}{$hint}</span></label>
|
<label {$this->getStringifiedAttributes()}>{$checkboxInput}
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>{$this->slot}{$hint}</span>
|
||||||
|
{$helperText}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ class Helper extends Component
|
|||||||
#[Override]
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$this->mergeClass('text-skin-muted');
|
$this->mergeClass('form-helper');
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<small {$this->getStringifiedAttributes()}>{$this->slot}</small>
|
<small {$this->getStringifiedAttributes()}>{$this->slot}</small>
|
||||||
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Views\Components\Forms;
|
namespace App\Views\Components\Forms;
|
||||||
|
|
||||||
use App\Views\Components\Hint;
|
|
||||||
use Override;
|
use Override;
|
||||||
|
|
||||||
class RadioButton extends FormComponent
|
class RadioButton extends FormComponent
|
||||||
@ -17,7 +16,7 @@ class RadioButton extends FormComponent
|
|||||||
|
|
||||||
protected bool $isSelected = false;
|
protected bool $isSelected = false;
|
||||||
|
|
||||||
protected string $hint = '';
|
protected string $description = '';
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
@ -32,21 +31,30 @@ class RadioButton extends FormComponent
|
|||||||
$data['required'] = 'required';
|
$data['required'] = 'required';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->mergeClass('relative w-full');
|
||||||
|
|
||||||
|
$descriptionText = '';
|
||||||
|
if ($this->description !== '') {
|
||||||
|
$describerId = $this->name . 'Help';
|
||||||
|
$descriptionText = <<<HTML
|
||||||
|
<span id="{$describerId}" class="form-radio-btn-description">{$this->description}</span>
|
||||||
|
HTML;
|
||||||
|
$data['aria-describedby'] = $describerId;
|
||||||
|
}
|
||||||
|
|
||||||
$radioInput = form_radio(
|
$radioInput = form_radio(
|
||||||
$data,
|
$data,
|
||||||
$this->value,
|
$this->value,
|
||||||
old($this->name) ? old($this->name) === $this->value : $this->isSelected,
|
old($this->name) ? old($this->name) === $this->value : $this->isSelected,
|
||||||
);
|
);
|
||||||
|
|
||||||
$hint = $this->hint === '' ? '' : (new Hint([
|
|
||||||
'class' => 'ml-1 text-base',
|
|
||||||
'slot' => $this->hint,
|
|
||||||
]))->render();
|
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<div {$this->getStringifiedAttributes()}">
|
<div {$this->getStringifiedAttributes()}">
|
||||||
{$radioInput}
|
{$radioInput}
|
||||||
<label for="{$this->value}">{$this->slot}{$hint}</label>
|
<label for="{$this->value}">
|
||||||
|
<span>{$this->slot}</span>
|
||||||
|
{$descriptionText}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,10 @@ class RadioGroup extends FormComponent
|
|||||||
*/
|
*/
|
||||||
protected array $options = [];
|
protected array $options = [];
|
||||||
|
|
||||||
protected string $helper = '';
|
|
||||||
|
|
||||||
protected string $hint = '';
|
protected string $hint = '';
|
||||||
|
|
||||||
|
protected string $helper = '';
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
@ -37,7 +37,7 @@ class RadioGroup extends FormComponent
|
|||||||
'value' => $option['value'],
|
'value' => $option['value'],
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'slot' => $option['label'],
|
'slot' => $option['label'],
|
||||||
'hint' => $option['hint'] ?? '',
|
'description' => $option['description'] ?? '',
|
||||||
'isSelected' => var_export($this->value === null ? ($option['value'] === $this->options[array_key_first($this->options)]['value']) : ($this->value === $option['value']), true),
|
'isSelected' => var_export($this->value === null ? ($option['value'] === $this->options[array_key_first($this->options)]['value']) : ($this->value === $option['value']), true),
|
||||||
'isRequired' => var_export($this->isRequired, true),
|
'isRequired' => var_export($this->isRequired, true),
|
||||||
]))->render();
|
]))->render();
|
||||||
@ -62,7 +62,7 @@ class RadioGroup extends FormComponent
|
|||||||
<fieldset {$this->getStringifiedAttributes()}>
|
<fieldset {$this->getStringifiedAttributes()}>
|
||||||
<legend class="-mb-1 text-sm font-semibold">{$this->label}{$hint}</legend>
|
<legend class="-mb-1 text-sm font-semibold">{$this->label}{$hint}</legend>
|
||||||
{$helperText}
|
{$helperText}
|
||||||
<div class="flex gap-1 mt-1">{$options}</div>
|
<div class="grid grid-cols-radioGroup gap-2 mt-1">{$options}</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ class Select extends FormComponent
|
|||||||
$options = '';
|
$options = '';
|
||||||
$selected = $this->value ?? $this->defaultValue;
|
$selected = $this->value ?? $this->defaultValue;
|
||||||
foreach ($this->options as $option) {
|
foreach ($this->options as $option) {
|
||||||
$options .= '<option ' . (array_key_exists('hint', $option) ? 'data-label-description="' . $option['hint'] . '" ' : '') . 'value="' . $option['value'] . '"' . ($option['value'] === $selected ? ' selected' : '') . '>' . $option['label'] . '</option>';
|
$options .= '<option ' . (array_key_exists('description', $option) ? 'data-label-description="' . $option['description'] . '" ' : '') . 'value="' . $option['value'] . '"' . ($option['value'] === $selected ? ' selected' : '') . '>' . $option['label'] . '</option>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->attributes['name'] = $this->name;
|
$this->attributes['name'] = $this->name;
|
||||||
|
@ -45,7 +45,7 @@ class SelectMulti extends FormComponent
|
|||||||
$options = '';
|
$options = '';
|
||||||
$selected = $this->value ?? $this->defaultValue;
|
$selected = $this->value ?? $this->defaultValue;
|
||||||
foreach ($this->options as $option) {
|
foreach ($this->options as $option) {
|
||||||
$options .= '<option ' . (array_key_exists('hint', $option) ? 'data-label-description="' . $option['hint'] . '" ' : '') . 'value="' . $option['value'] . '"' . (in_array($option['value'], $selected, true) ? ' selected' : '') . '>' . $option['label'] . '</option>';
|
$options .= '<option ' . (array_key_exists('description', $option) ? 'data-label-description="' . $option['description'] . '" ' : '') . 'value="' . $option['value'] . '"' . (in_array($option['value'], $selected, true) ? ' selected' : '') . '>' . $option['label'] . '</option>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->attributes['name'] = $this->name . '[]';
|
$this->attributes['name'] = $this->name . '[]';
|
||||||
|
@ -17,12 +17,14 @@ class Toggler extends FormComponent
|
|||||||
|
|
||||||
protected string $hint = '';
|
protected string $hint = '';
|
||||||
|
|
||||||
|
protected string $helper = '';
|
||||||
|
|
||||||
protected bool $isChecked = false;
|
protected bool $isChecked = false;
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$this->mergeClass('relative justify-between inline-flex items-center gap-x-2');
|
$this->mergeClass('relative justify-between inline-flex items-start gap-x-2');
|
||||||
|
|
||||||
$checkbox = form_checkbox(
|
$checkbox = form_checkbox(
|
||||||
[
|
[
|
||||||
@ -39,9 +41,23 @@ class Toggler extends FormComponent
|
|||||||
'slot' => $this->hint,
|
'slot' => $this->hint,
|
||||||
]))->render();
|
]))->render();
|
||||||
|
|
||||||
|
$helperText = '';
|
||||||
|
if ($this->helper !== '') {
|
||||||
|
$helperId = $this->name . 'Help';
|
||||||
|
$helperText = (new Helper([
|
||||||
|
'id' => $helperId,
|
||||||
|
'slot' => $this->helper,
|
||||||
|
'class' => '-mt-1',
|
||||||
|
]))->render();
|
||||||
|
$this->attributes['aria-describedby'] = $helperId;
|
||||||
|
}
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<label {$this->getStringifiedAttributes()}>
|
<label {$this->getStringifiedAttributes()}>
|
||||||
|
<div class="flex flex-col">
|
||||||
<span>{$this->slot}{$hint}</span>
|
<span>{$this->slot}{$hint}</span>
|
||||||
|
{$helperText}
|
||||||
|
</div>
|
||||||
{$checkbox}
|
{$checkbox}
|
||||||
<span class="form-switch-slider"></span>
|
<span class="form-switch-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?= helper(['components', 'svg']) ?>
|
<?= helper(['components', 'svg']) ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="<?= service('request')
|
||||||
|
->getLocale() ?>">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?= helper(['components', 'svg']) ?>
|
<?= helper(['components', 'svg']) ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="<?= service('request')
|
||||||
|
->getLocale() ?>">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
@ -102,13 +102,15 @@ each property being a field key and the value being a `Field` object.
|
|||||||
A field is a form element:
|
A field is a form element:
|
||||||
|
|
||||||
| Property | Type | Note |
|
| Property | Type | Note |
|
||||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
|
||||||
| `type` | `checkbox` \| `datetime` \| `email` \| `markdown` \| `number` \| `radio-group` \| `select-multiple` \| `select` \| `text` \| `textarea` \| `toggler` \| `url` | Default is `text` |
|
| `type` | `checkbox` \| `datetime` \| `email` \| `group` \| `markdown` \| `number` \| `radio-group` \| `select-multiple` \| `select` \| `text` \| `textarea` \| `toggler` \| `url` | Default is `text` |
|
||||||
| `label` (required) | `string` | Can be translated (see i18n) |
|
| `label` (required) | `string` | Can be translated (see i18n) |
|
||||||
| `hint` | `string` | Can be translated (see i18n) |
|
| `hint` | `string` | Can be translated (see i18n) |
|
||||||
| `helper` | `string` | Can be translated (see i18n) |
|
| `helper` | `string` | Can be translated (see i18n) |
|
||||||
| `optional` | `boolean` | Default is `false` |
|
| `optional` | `boolean` | Default is `false` |
|
||||||
| `options` | `Options` | Required for `radio-group`, `select-multiple`, and `select` types. |
|
| `options` | `Options` | Required for `radio-group`, `select-multiple`, and `select` types. |
|
||||||
|
| `multiple` | `boolean` | Default is `false` |
|
||||||
|
| `fields` | `Array<string, Field>` | Required for `group` type |
|
||||||
|
|
||||||
#### Options object
|
#### Options object
|
||||||
|
|
||||||
@ -119,7 +121,7 @@ The `Options` object properties are option keys and the value is an `Option`.
|
|||||||
| Property | Type | Note |
|
| Property | Type | Note |
|
||||||
| ------------------ | -------- | ---------------------------- |
|
| ------------------ | -------- | ---------------------------- |
|
||||||
| `label` (required) | `string` | Can be translated (see i18n) |
|
| `label` (required) | `string` | Can be translated (see i18n) |
|
||||||
| `hint` | `string` | Can be translated (see i18n) |
|
| `description` | `string` | Can be translated (see i18n) |
|
||||||
|
|
||||||
### files
|
### files
|
||||||
|
|
||||||
|
@ -38,6 +38,10 @@ return [
|
|||||||
'noChoicesText' => 'No choices to choose from',
|
'noChoicesText' => 'No choices to choose from',
|
||||||
'maxItemText' => 'Cannot add more items',
|
'maxItemText' => 'Cannot add more items',
|
||||||
],
|
],
|
||||||
|
'fieldArray' => [
|
||||||
|
'add' => 'Add',
|
||||||
|
'remove' => 'Remove',
|
||||||
|
],
|
||||||
'upload_file' => 'Upload a file',
|
'upload_file' => 'Upload a file',
|
||||||
'remote_url' => 'Remote URL',
|
'remote_url' => 'Remote URL',
|
||||||
'save' => 'Save',
|
'save' => 'Save',
|
||||||
|
@ -109,11 +109,11 @@ return [
|
|||||||
'type' => [
|
'type' => [
|
||||||
'label' => 'Type',
|
'label' => 'Type',
|
||||||
'full' => 'Full',
|
'full' => 'Full',
|
||||||
'full_hint' => 'Complete content (the episode)',
|
'full_description' => 'Complete content (the episode)',
|
||||||
'trailer' => 'Trailer',
|
'trailer' => 'Trailer',
|
||||||
'trailer_hint' => 'Short, promotional piece of content that represents a preview of the current show',
|
'trailer_description' => 'Short, promotional piece of content that represents a preview of the current show',
|
||||||
'bonus' => 'Bonus',
|
'bonus' => 'Bonus',
|
||||||
'bonus_hint' => 'Extra content for the show (for example, behind the scenes info or interviews with the cast) or cross-promotional content for another show',
|
'bonus_description' => 'Extra content for the show (for example, behind the scenes info or interviews with the cast) or cross-promotional content for another show',
|
||||||
],
|
],
|
||||||
'premium_title' => 'Premium',
|
'premium_title' => 'Premium',
|
||||||
'premium' => 'Episode must be accessible to premium subscribers only',
|
'premium' => 'Episode must be accessible to premium subscribers only',
|
||||||
|
@ -72,19 +72,19 @@ return [
|
|||||||
'type' => [
|
'type' => [
|
||||||
'label' => 'Type',
|
'label' => 'Type',
|
||||||
'episodic' => 'Episodic',
|
'episodic' => 'Episodic',
|
||||||
'episodic_hint' => 'If episodes are intended to be consumed without any specific order. Newest episodes will be presented first.',
|
'episodic_description' => 'If episodes are intended to be consumed without any specific order. Newest episodes will be presented first.',
|
||||||
'serial' => 'Serial',
|
'serial' => 'Serial',
|
||||||
'serial_hint' => 'If episodes are intended to be consumed in sequential order. Episodes will be presented in numeric order.',
|
'serial_description' => 'If episodes are intended to be consumed in sequential order. Episodes will be presented in numeric order.',
|
||||||
],
|
],
|
||||||
'medium' => [
|
'medium' => [
|
||||||
'label' => 'Medium',
|
'label' => 'Medium',
|
||||||
'hint' => 'Medium as represented by podcast:medium tag in RSS. Changing this may change how players present your feed.',
|
'hint' => 'Medium as represented by podcast:medium tag in RSS. Changing this may change how players present your feed.',
|
||||||
'podcast' => 'Podcast',
|
'podcast' => 'Podcast',
|
||||||
'podcast_hint' => 'Describes a feed for a podcast show.',
|
'podcast_description' => 'Describes a feed for a podcast show.',
|
||||||
'music' => 'Music',
|
'music' => 'Music',
|
||||||
'music_hint' => 'A feed of music organized into an "album" with each item a song within the album.',
|
'music_description' => 'A feed of music organized into an "album" with each item a song within the album.',
|
||||||
'audiobook' => 'Audiobook',
|
'audiobook' => 'Audiobook',
|
||||||
'audiobook_hint' => 'Specific types of audio with one item per feed, or where items represent chapters within the book.',
|
'audiobook_description' => 'Specific types of audio with one item per feed, or where items represent chapters within the book.',
|
||||||
],
|
],
|
||||||
'description' => 'Description',
|
'description' => 'Description',
|
||||||
'classification_section_title' => 'Classification',
|
'classification_section_title' => 'Classification',
|
||||||
|
@ -13,37 +13,41 @@ use CodeIgniter\HTTP\RedirectResponse;
|
|||||||
use CodeIgniter\HTTP\URI;
|
use CodeIgniter\HTTP\URI;
|
||||||
use CodeIgniter\I18n\Time;
|
use CodeIgniter\I18n\Time;
|
||||||
use Modules\Admin\Controllers\BaseController;
|
use Modules\Admin\Controllers\BaseController;
|
||||||
|
use Modules\Plugins\Core\BasePlugin;
|
||||||
use Modules\Plugins\Core\Markdown;
|
use Modules\Plugins\Core\Markdown;
|
||||||
use Modules\Plugins\Core\Plugins;
|
use Modules\Plugins\Core\Plugins;
|
||||||
|
use Modules\Plugins\Manifest\Field;
|
||||||
|
|
||||||
class PluginController extends BaseController
|
class PluginController extends BaseController
|
||||||
{
|
{
|
||||||
|
protected Plugins $plugins;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->plugins = service('plugins');
|
||||||
|
}
|
||||||
|
|
||||||
public function installed(): string
|
public function installed(): string
|
||||||
{
|
{
|
||||||
/** @var Plugins $plugins */
|
|
||||||
$plugins = service('plugins');
|
|
||||||
|
|
||||||
$pager = service('pager');
|
$pager = service('pager');
|
||||||
|
|
||||||
$page = (int) ($this->request->getGet('page') ?? 1);
|
$page = (int) ($this->request->getGet('page') ?? 1);
|
||||||
$perPage = 10;
|
$perPage = 10;
|
||||||
$total = $plugins->getInstalledCount();
|
$total = $this->plugins->getInstalledCount();
|
||||||
|
|
||||||
$pager_links = $pager->makeLinks($page, $perPage, $total);
|
$pager_links = $pager->makeLinks($page, $perPage, $total);
|
||||||
|
|
||||||
return view('plugins/installed', [
|
return view('plugins/installed', [
|
||||||
'total' => $total,
|
'total' => $total,
|
||||||
'plugins' => $plugins->getPlugins($page, $perPage),
|
'plugins' => $this->plugins->getPlugins($page, $perPage),
|
||||||
'pager_links' => $pager_links,
|
'pager_links' => $pager_links,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function vendor(string $vendor): string
|
public function vendor(string $vendor): string
|
||||||
{
|
{
|
||||||
/** @var Plugins $plugins */
|
|
||||||
$plugins = service('plugins');
|
|
||||||
|
|
||||||
$vendorPlugins = $plugins->getVendorPlugins($vendor);
|
$vendorPlugins = $this->plugins->getVendorPlugins($vendor);
|
||||||
replace_breadcrumb_params([
|
replace_breadcrumb_params([
|
||||||
$vendor => $vendor,
|
$vendor => $vendor,
|
||||||
]);
|
]);
|
||||||
@ -56,12 +60,10 @@ class PluginController extends BaseController
|
|||||||
|
|
||||||
public function view(string $vendor, string $package): string
|
public function view(string $vendor, string $package): string
|
||||||
{
|
{
|
||||||
/** @var Plugins $plugins */
|
|
||||||
$plugins = service('plugins');
|
|
||||||
|
|
||||||
$plugin = $plugins->getPlugin($vendor, $package);
|
$plugin = $this->plugins->getPlugin($vendor, $package);
|
||||||
|
|
||||||
if ($plugin === null) {
|
if (! $plugin instanceof BasePlugin) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,12 +82,10 @@ class PluginController extends BaseController
|
|||||||
string $podcastId = null,
|
string $podcastId = null,
|
||||||
string $episodeId = null
|
string $episodeId = null
|
||||||
): string {
|
): string {
|
||||||
/** @var Plugins $plugins */
|
|
||||||
$plugins = service('plugins');
|
|
||||||
|
|
||||||
$plugin = $plugins->getPlugin($vendor, $package);
|
$plugin = $this->plugins->getPlugin($vendor, $package);
|
||||||
|
|
||||||
if ($plugin === null) {
|
if (! $plugin instanceof BasePlugin) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,12 +146,10 @@ class PluginController extends BaseController
|
|||||||
string $podcastId = null,
|
string $podcastId = null,
|
||||||
string $episodeId = null
|
string $episodeId = null
|
||||||
): RedirectResponse {
|
): RedirectResponse {
|
||||||
/** @var Plugins $plugins */
|
|
||||||
$plugins = service('plugins');
|
|
||||||
|
|
||||||
$plugin = $plugins->getPlugin($vendor, $package);
|
$plugin = $this->plugins->getPlugin($vendor, $package);
|
||||||
|
|
||||||
if ($plugin === null) {
|
if (! $plugin instanceof BasePlugin) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,13 +168,37 @@ class PluginController extends BaseController
|
|||||||
// construct validation rules first
|
// construct validation rules first
|
||||||
$rules = [];
|
$rules = [];
|
||||||
foreach ($plugin->getSettingsFields($type) as $field) {
|
foreach ($plugin->getSettingsFields($type) as $field) {
|
||||||
$typeRules = $plugins::FIELDS_VALIDATIONS[$field->type];
|
$typeRules = $this->plugins::FIELDS_VALIDATIONS[$field->type];
|
||||||
if (! in_array('permit_empty', $typeRules, true)) {
|
if (! in_array('permit_empty', $typeRules, true)) {
|
||||||
$typeRules[] = $field->optional ? 'permit_empty' : 'required';
|
$typeRules[] = $field->optional ? 'permit_empty' : 'required';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($field->multiple) {
|
||||||
|
if ($field->type === 'group') {
|
||||||
|
foreach ($field->fields as $subField) {
|
||||||
|
$typeRules = $this->plugins::FIELDS_VALIDATIONS[$subField->type];
|
||||||
|
if (! in_array('permit_empty', $typeRules, true)) {
|
||||||
|
$typeRules[] = $subField->optional ? 'permit_empty' : 'required';
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules[sprintf('%s.*.%s', $field->key, $subField->key)] = $typeRules;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$rules[$field->key . '.*'] = $typeRules;
|
||||||
|
}
|
||||||
|
} elseif ($field->type === 'group') {
|
||||||
|
foreach ($field->fields as $subField) {
|
||||||
|
$typeRules = $this->plugins::FIELDS_VALIDATIONS[$subField->type];
|
||||||
|
if (! in_array('permit_empty', $typeRules, true)) {
|
||||||
|
$typeRules[] = $subField->optional ? 'permit_empty' : 'required';
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules[sprintf('%s.%s', $field->key, $subField->key)] = $typeRules;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
$rules[$field->key] = $typeRules;
|
$rules[$field->key] = $typeRules;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (! $this->validate($rules)) {
|
if (! $this->validate($rules)) {
|
||||||
return redirect()
|
return redirect()
|
||||||
@ -188,8 +210,117 @@ class PluginController extends BaseController
|
|||||||
$validatedData = $this->validator->getValidated();
|
$validatedData = $this->validator->getValidated();
|
||||||
|
|
||||||
foreach ($plugin->getSettingsFields($type) as $field) {
|
foreach ($plugin->getSettingsFields($type) as $field) {
|
||||||
$value = $validatedData[$field->key] ?? null;
|
$fieldValue = $validatedData[$field->key] ?? null;
|
||||||
$fieldValue = $value === '' ? null : match ($plugins::FIELDS_CASTS[$field->type] ?? 'text') {
|
|
||||||
|
$this->plugins->setOption($plugin, $field->key, $this->castFieldValue($field, $fieldValue), $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->with('message', lang('Plugins.messages.saveSettingsSuccess', [
|
||||||
|
'pluginTitle' => $plugin->getTitle(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activate(string $vendor, string $package): RedirectResponse
|
||||||
|
{
|
||||||
|
|
||||||
|
$plugin = $this->plugins->getPlugin($vendor, $package);
|
||||||
|
|
||||||
|
if (! $plugin instanceof BasePlugin) {
|
||||||
|
throw PageNotFoundException::forPageNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->plugins->activate($plugin);
|
||||||
|
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deactivate(string $vendor, string $package): RedirectResponse
|
||||||
|
{
|
||||||
|
|
||||||
|
$plugin = $this->plugins->getPlugin($vendor, $package);
|
||||||
|
|
||||||
|
if (! $plugin instanceof BasePlugin) {
|
||||||
|
throw PageNotFoundException::forPageNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->plugins->deactivate($plugin);
|
||||||
|
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uninstall(string $vendor, string $package): RedirectResponse
|
||||||
|
{
|
||||||
|
|
||||||
|
$plugin = $this->plugins->getPlugin($vendor, $package);
|
||||||
|
|
||||||
|
if (! $plugin instanceof BasePlugin) {
|
||||||
|
throw PageNotFoundException::forPageNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->plugins->uninstall($plugin);
|
||||||
|
|
||||||
|
return redirect()->back();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function castFieldValue(Field $field, mixed $fieldValue): mixed
|
||||||
|
{
|
||||||
|
if ($fieldValue === '' || $fieldValue === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = null;
|
||||||
|
if ($field->multiple) {
|
||||||
|
$value = [];
|
||||||
|
foreach ($fieldValue as $key => $val) {
|
||||||
|
if ($val === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->type === 'group') {
|
||||||
|
foreach ($val as $subKey => $subVal) {
|
||||||
|
/** @var Field|false $subField */
|
||||||
|
$subField = array_column($field->fields, null, 'key')[$subKey] ?? false;
|
||||||
|
if (! $subField) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = $this->castValue($subVal, $subField->type);
|
||||||
|
if ($v) {
|
||||||
|
$value[$key][$subKey] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$value[$key] = $this->castValue($val, $field->type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($field->type === 'group') {
|
||||||
|
foreach ($fieldValue as $subKey => $subVal) {
|
||||||
|
/** @var Field|false $subField */
|
||||||
|
$subField = array_column($field->fields, null, 'key')[$subKey] ?? false;
|
||||||
|
if (! $subField) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = $this->castValue($subVal, $subField->type);
|
||||||
|
if ($v) {
|
||||||
|
$value[$subKey] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$value = $this->castValue($fieldValue, $field->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value === [] ? null : $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function castValue(mixed $value, string $type): mixed
|
||||||
|
{
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($this->plugins::FIELDS_CASTS[$type] ?? 'text') {
|
||||||
'bool' => $value === 'yes',
|
'bool' => $value === 'yes',
|
||||||
'int' => (int) $value,
|
'int' => (int) $value,
|
||||||
'uri' => new URI($value),
|
'uri' => new URI($value),
|
||||||
@ -201,60 +332,5 @@ class PluginController extends BaseController
|
|||||||
'markdown' => new Markdown($value),
|
'markdown' => new Markdown($value),
|
||||||
default => $value,
|
default => $value,
|
||||||
};
|
};
|
||||||
$plugins->setOption($plugin, $field->key, $fieldValue, $context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->back()
|
|
||||||
->with('message', lang('Plugins.messages.saveSettingsSuccess', [
|
|
||||||
'pluginTitle' => $plugin->getTitle(),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function activate(string $vendor, string $package): RedirectResponse
|
|
||||||
{
|
|
||||||
/** @var Plugins $plugins */
|
|
||||||
$plugins = service('plugins');
|
|
||||||
|
|
||||||
$plugin = $plugins->getPlugin($vendor, $package);
|
|
||||||
|
|
||||||
if ($plugin === null) {
|
|
||||||
throw PageNotFoundException::forPageNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
$plugins->activate($plugin);
|
|
||||||
|
|
||||||
return redirect()->back();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deactivate(string $vendor, string $package): RedirectResponse
|
|
||||||
{
|
|
||||||
/** @var Plugins $plugins */
|
|
||||||
$plugins = service('plugins');
|
|
||||||
|
|
||||||
$plugin = $plugins->getPlugin($vendor, $package);
|
|
||||||
|
|
||||||
if ($plugin === null) {
|
|
||||||
throw PageNotFoundException::forPageNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
$plugins->deactivate($plugin);
|
|
||||||
|
|
||||||
return redirect()->back();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uninstall(string $vendor, string $package): RedirectResponse
|
|
||||||
{
|
|
||||||
/** @var Plugins $plugins */
|
|
||||||
$plugins = service('plugins');
|
|
||||||
|
|
||||||
$plugin = $plugins->getPlugin($vendor, $package);
|
|
||||||
|
|
||||||
if ($plugin === null) {
|
|
||||||
throw PageNotFoundException::forPageNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
$plugins->uninstall($plugin);
|
|
||||||
|
|
||||||
return redirect()->back();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ class Plugins
|
|||||||
'textarea' => ['string'],
|
'textarea' => ['string'],
|
||||||
'toggler' => ['permit_empty'],
|
'toggler' => ['permit_empty'],
|
||||||
'url' => ['valid_url_strict'],
|
'url' => ['valid_url_strict'],
|
||||||
|
'group' => ['permit_empty', 'is_list'],
|
||||||
];
|
];
|
||||||
|
|
||||||
public const FIELDS_CASTS = [
|
public const FIELDS_CASTS = [
|
||||||
|
@ -7,27 +7,33 @@ namespace Modules\Plugins\Manifest;
|
|||||||
use Override;
|
use Override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @property 'checkbox'|'datetime'|'email'|'markdown'|'number'|'radio-group'|'select-multiple'|'select'|'text'|'textarea'|'toggler'|'url'|'group' $type
|
||||||
* @property string $key
|
* @property string $key
|
||||||
* @property 'text'|'email'|'url'|'markdown'|'number'|'switch' $type
|
|
||||||
* @property string $label
|
* @property string $label
|
||||||
* @property string $hint
|
* @property string $hint
|
||||||
* @property string $helper
|
* @property string $helper
|
||||||
* @property bool $optional
|
* @property bool $optional
|
||||||
|
* @property Option[] $options
|
||||||
|
* @property bool $multiple
|
||||||
|
* @property Field[] $fields
|
||||||
*/
|
*/
|
||||||
class Field extends ManifestObject
|
class Field extends ManifestObject
|
||||||
{
|
{
|
||||||
protected const VALIDATION_RULES = [
|
protected const VALIDATION_RULES = [
|
||||||
'type' => 'permit_empty|in_list[checkbox,datetime,email,markdown,number,radio-group,select-multiple,select,text,textarea,toggler,url]',
|
'type' => 'permit_empty|in_list[checkbox,datetime,email,markdown,number,radio-group,select-multiple,select,text,textarea,toggler,url,group]',
|
||||||
'key' => 'required|alpha_dash',
|
'key' => 'required|alpha_dash',
|
||||||
'label' => 'required|string',
|
'label' => 'required|string',
|
||||||
'hint' => 'permit_empty|string',
|
'hint' => 'permit_empty|string',
|
||||||
'helper' => 'permit_empty|string',
|
'helper' => 'permit_empty|string',
|
||||||
'optional' => 'permit_empty|is_boolean',
|
'optional' => 'permit_empty|is_boolean',
|
||||||
'options' => 'permit_empty|is_list',
|
'options' => 'permit_empty|is_list',
|
||||||
|
'multiple' => 'permit_empty|is_boolean',
|
||||||
|
'fields' => 'permit_empty|is_list',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected const CASTS = [
|
protected const CASTS = [
|
||||||
'options' => [Option::class],
|
'options' => [Option::class],
|
||||||
|
'fields' => [self::class],
|
||||||
];
|
];
|
||||||
|
|
||||||
protected string $type = 'text';
|
protected string $type = 'text';
|
||||||
@ -42,6 +48,8 @@ class Field extends ManifestObject
|
|||||||
|
|
||||||
protected bool $optional = false;
|
protected bool $optional = false;
|
||||||
|
|
||||||
|
protected bool $multiple = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Option[]
|
* @var Option[]
|
||||||
*/
|
*/
|
||||||
@ -60,37 +68,49 @@ class Field extends ManifestObject
|
|||||||
$data['options'] = $newOptions;
|
$data['options'] = $newOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('fields', $data)) {
|
||||||
|
$newFields = [];
|
||||||
|
foreach ($data['fields'] as $key => $field) {
|
||||||
|
$field['key'] = $key;
|
||||||
|
$newFields[] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['fields'] = $newFields;
|
||||||
|
}
|
||||||
|
|
||||||
parent::loadData($data);
|
parent::loadData($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{label:string,value:string,hint:string}[]
|
* @return array{label:string,value:string,description:string}[]
|
||||||
*/
|
*/
|
||||||
public function getOptionsArray(string $i18nKey): array
|
public function getOptionsArray(string $pluginKey): array
|
||||||
{
|
{
|
||||||
|
$i18nKey = sprintf('%s.settings.%s.%s.options', $pluginKey, $this->type, $this->key);
|
||||||
|
|
||||||
$optionsArray = [];
|
$optionsArray = [];
|
||||||
foreach ($this->options as $option) {
|
foreach ($this->options as $option) {
|
||||||
$optionsArray[] = [
|
$optionsArray[] = [
|
||||||
'value' => $option->value,
|
'value' => $option->value,
|
||||||
'label' => esc($this->getTranslated($i18nKey . '.' . $option->value . '.label', $option->label)),
|
'label' => $option->getTranslated($i18nKey, 'label'),
|
||||||
'hint' => esc($this->getTranslated($i18nKey . '.' . $option->value . '.hint', (string) $option->hint)),
|
'description' => $option->getTranslated($i18nKey, 'description'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $optionsArray;
|
return $optionsArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTranslated(string $i18nKey, string $default): string
|
public function getTranslated(string $pluginKey, string $property): string
|
||||||
{
|
{
|
||||||
$key = 'Plugin.' . $i18nKey;
|
$key = sprintf('Plugin.%s.settings.%s.%s.%s', $pluginKey, $this->type, $this->key, $property);
|
||||||
|
|
||||||
/** @var string $i18nField */
|
/** @var string $i18nField */
|
||||||
$i18nField = lang($key);
|
$i18nField = lang($key);
|
||||||
|
|
||||||
if ($default === '' || $i18nField === $key) {
|
if ($this->{$property} === '' || $i18nField === $key) {
|
||||||
return $default;
|
return esc($this->{$property});
|
||||||
}
|
}
|
||||||
|
|
||||||
return $i18nField;
|
return esc($i18nField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,19 +7,33 @@ namespace Modules\Plugins\Manifest;
|
|||||||
/**
|
/**
|
||||||
* @property string $label
|
* @property string $label
|
||||||
* @property string $value
|
* @property string $value
|
||||||
* @property ?string $hint
|
* @property string $hint
|
||||||
*/
|
*/
|
||||||
class Option extends ManifestObject
|
class Option extends ManifestObject
|
||||||
{
|
{
|
||||||
protected const VALIDATION_RULES = [
|
protected const VALIDATION_RULES = [
|
||||||
'label' => 'required|string',
|
'label' => 'required|string',
|
||||||
'value' => 'required|alpha_numeric_punct',
|
'value' => 'required|alpha_numeric_punct',
|
||||||
'hint' => 'permit_empty|string',
|
'description' => 'permit_empty|string',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected string $label;
|
protected string $label;
|
||||||
|
|
||||||
protected string $value;
|
protected string $value;
|
||||||
|
|
||||||
protected ?string $hint = null;
|
protected string $description = '';
|
||||||
|
|
||||||
|
public function getTranslated(string $i18nKey, string $property): string
|
||||||
|
{
|
||||||
|
$key = sprintf('%s.%s.%s', $i18nKey, $this->value, $property);
|
||||||
|
|
||||||
|
/** @var string $i18nField */
|
||||||
|
$i18nField = lang($key);
|
||||||
|
|
||||||
|
if ($this->{$property} === '' || $i18nField === $key) {
|
||||||
|
return esc($this->{$property});
|
||||||
|
}
|
||||||
|
|
||||||
|
return esc($i18nField);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,6 +173,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"enum": [
|
"enum": [
|
||||||
|
"group",
|
||||||
"checkbox",
|
"checkbox",
|
||||||
"datetime",
|
"datetime",
|
||||||
"email",
|
"email",
|
||||||
@ -206,12 +207,23 @@
|
|||||||
"^[A-Za-z0-9]+[\\w\\-\\:\\.]*$": { "$ref": "#/$defs/option" }
|
"^[A-Za-z0-9]+[\\w\\-\\:\\.]*$": { "$ref": "#/$defs/option" }
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"multiple": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[A-Za-z]+[\\w\\-\\:\\.]*$": { "$ref": "#/$defs/field" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["label"],
|
"required": ["label"],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{ "$ref": "#/$defs/field-multiple-implies-options-is-required" }
|
{ "$ref": "#/$defs/field-multiple-implies-options-is-required" },
|
||||||
|
{ "$ref": "#/$defs/field-group-type-implies-fields-is-required" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"option": {
|
"option": {
|
||||||
@ -220,7 +232,7 @@
|
|||||||
"label": {
|
"label": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"hint": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -245,6 +257,21 @@
|
|||||||
},
|
},
|
||||||
{ "required": ["options"] }
|
{ "required": ["options"] }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"field-group-type-implies-fields-is-required": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"not": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"anyOf": [{ "const": "group" }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "required": ["fields"] }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
const defaultTheme = require("tailwindcss/defaultTheme");
|
const defaultTheme = require("tailwindcss/defaultTheme");
|
||||||
|
const { transform } = require("typescript");
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -122,6 +123,7 @@ module.exports = {
|
|||||||
colorButtons: "repeat(auto-fill, minmax(4rem, 1fr))",
|
colorButtons: "repeat(auto-fill, minmax(4rem, 1fr))",
|
||||||
platforms: "repeat(auto-fill, minmax(18rem, 1fr))",
|
platforms: "repeat(auto-fill, minmax(18rem, 1fr))",
|
||||||
plugins: "repeat(auto-fill, minmax(20rem, 1fr))",
|
plugins: "repeat(auto-fill, minmax(20rem, 1fr))",
|
||||||
|
radioGroup: "repeat(auto-fit, minmax(14rem, 1fr))",
|
||||||
},
|
},
|
||||||
gridTemplateRows: {
|
gridTemplateRows: {
|
||||||
admin: "40px 1fr",
|
admin: "40px 1fr",
|
||||||
@ -162,6 +164,18 @@ module.exports = {
|
|||||||
zIndex: {
|
zIndex: {
|
||||||
60: 60,
|
60: 60,
|
||||||
},
|
},
|
||||||
|
keyframes: {
|
||||||
|
"slight-pulse": {
|
||||||
|
"0%": { transform: "scale(1)" },
|
||||||
|
"60%": { transform: "scale(0.96)" },
|
||||||
|
"75%": { transform: "scale(1.05)" },
|
||||||
|
"95%": { transform: "scale(0.98)" },
|
||||||
|
"100%": { transform: "scale(1)" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"single-pulse": "slight-pulse 300ms linear 1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variants: {},
|
variants: {},
|
||||||
|
@ -76,17 +76,17 @@
|
|||||||
[
|
[
|
||||||
'label' => lang('Episode.form.type.full'),
|
'label' => lang('Episode.form.type.full'),
|
||||||
'value' => 'full',
|
'value' => 'full',
|
||||||
'hint' => lang('Episode.form.type.full_hint'),
|
'description' => lang('Episode.form.type.full_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Episode.form.type.trailer'),
|
'label' => lang('Episode.form.type.trailer'),
|
||||||
'value' => 'trailer',
|
'value' => 'trailer',
|
||||||
'hint' => lang('Episode.form.type.trailer_hint'),
|
'description' => lang('Episode.form.type.trailer_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Episode.form.type.bonus'),
|
'label' => lang('Episode.form.type.bonus'),
|
||||||
'value' => 'bonus',
|
'value' => 'bonus',
|
||||||
'hint' => lang('Episode.form.type.bonus_hint'),
|
'description' => lang('Episode.form.type.bonus_description'),
|
||||||
],
|
],
|
||||||
])) ?>"
|
])) ?>"
|
||||||
isRequired="true"
|
isRequired="true"
|
||||||
|
@ -80,17 +80,17 @@
|
|||||||
[
|
[
|
||||||
'label' => lang('Episode.form.type.full'),
|
'label' => lang('Episode.form.type.full'),
|
||||||
'value' => 'full',
|
'value' => 'full',
|
||||||
'hint' => lang('Episode.form.type.full_hint'),
|
'description' => lang('Episode.form.type.full_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Episode.form.type.trailer'),
|
'label' => lang('Episode.form.type.trailer'),
|
||||||
'value' => 'trailer',
|
'value' => 'trailer',
|
||||||
'hint' => lang('Episode.form.type.trailer_hint'),
|
'description' => lang('Episode.form.type.trailer_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Episode.form.type.bonus'),
|
'label' => lang('Episode.form.type.bonus'),
|
||||||
'value' => 'bonus',
|
'value' => 'bonus',
|
||||||
'hint' => lang('Episode.form.type.bonus_hint'),
|
'description' => lang('Episode.form.type.bonus_description'),
|
||||||
],
|
],
|
||||||
])) ?>"
|
])) ?>"
|
||||||
isRequired="true"
|
isRequired="true"
|
||||||
|
144
themes/cp_admin/plugins/_field.php
Normal file
144
themes/cp_admin/plugins/_field.php
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?php switch ($type): case 'checkbox': ?>
|
||||||
|
<x-Forms.Checkbox
|
||||||
|
class="<?= $class ?>"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
isChecked="<?= $value ? 'true' : 'false' ?>"
|
||||||
|
><?= $label ?></x-Forms.Checkbox>
|
||||||
|
<?php break;
|
||||||
|
case 'toggler': ?>
|
||||||
|
<x-Forms.Toggler
|
||||||
|
class="<?= $class ?>"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
isChecked="<?= $value ? 'true' : 'false' ?>"
|
||||||
|
><?= $label ?></x-Forms.Toggler>
|
||||||
|
<?php break;
|
||||||
|
case 'radio-group': ?>
|
||||||
|
<x-Forms.RadioGroup
|
||||||
|
class="<?= $class ?>"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
options="<?= $options ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= $value ?>"
|
||||||
|
/>
|
||||||
|
<?php break;
|
||||||
|
case 'select': ?>
|
||||||
|
<x-Forms.Field
|
||||||
|
as="Select"
|
||||||
|
class="<?= $class ?>"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
options="<?= $options ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= $value ?>"
|
||||||
|
/>
|
||||||
|
<?php break;
|
||||||
|
case 'select-multiple': ?>
|
||||||
|
<x-Forms.Field
|
||||||
|
as="SelectMulti"
|
||||||
|
class="<?= $class ?>"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
options="<?= $options ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= esc(json_encode($value)) ?>"
|
||||||
|
/>
|
||||||
|
<?php break;
|
||||||
|
case 'email': ?>
|
||||||
|
<x-Forms.Field
|
||||||
|
as="Input"
|
||||||
|
class="<?= $class ?>"
|
||||||
|
type="email"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= $value ?>"
|
||||||
|
/>
|
||||||
|
<?php break;
|
||||||
|
case 'url': ?>
|
||||||
|
<x-Forms.Field
|
||||||
|
as="Input"
|
||||||
|
class="<?= $class ?>"
|
||||||
|
type="url"
|
||||||
|
placeholder="https://…"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= $value ?>"
|
||||||
|
/>
|
||||||
|
<?php break;
|
||||||
|
case 'number': ?>
|
||||||
|
<x-Forms.Field
|
||||||
|
as="Input"
|
||||||
|
class="<?= $class ?>"
|
||||||
|
type="number"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= $value ?>"
|
||||||
|
/>
|
||||||
|
<?php break;
|
||||||
|
case 'textarea': ?>
|
||||||
|
<x-Forms.Field
|
||||||
|
as="Textarea"
|
||||||
|
class="<?= $class ?>"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= $value ?>"
|
||||||
|
/>
|
||||||
|
<?php break;
|
||||||
|
case 'markdown': ?>
|
||||||
|
<x-Forms.Field
|
||||||
|
as="MarkdownEditor"
|
||||||
|
class="<?= $class ?>"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= $value ?>"
|
||||||
|
/>
|
||||||
|
<?php break;
|
||||||
|
case 'datetime': ?>
|
||||||
|
<x-Forms.Field
|
||||||
|
as="DatetimePicker"
|
||||||
|
class="<?= $class ?>"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= $value ?>"
|
||||||
|
/>
|
||||||
|
<?php break;
|
||||||
|
default: ?>
|
||||||
|
<x-Forms.Field
|
||||||
|
as="Input"
|
||||||
|
class="<?= $class ?>"
|
||||||
|
name="<?= $name ?>"
|
||||||
|
label="<?= $label ?>"
|
||||||
|
hint="<?= $hint ?>"
|
||||||
|
helper="<?= $helper ?>"
|
||||||
|
isRequired="<?= $optional ? 'false' : 'true' ?>"
|
||||||
|
value="<?= $value ?>"
|
||||||
|
/>
|
||||||
|
<?php endswitch; ?>
|
@ -2,138 +2,94 @@
|
|||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<?php $hasDatetime = false; ?>
|
<?php $hasDatetime = false; ?>
|
||||||
<?php foreach ($fields as $field): ?>
|
<?php foreach ($fields as $field): ?>
|
||||||
<?php switch ($field->type): case 'checkbox': ?>
|
<?php if ($field->type === 'datetime') {
|
||||||
<x-Forms.Checkbox
|
$hasDatetime = true;
|
||||||
name="<?= $field->key ?>"
|
} ?>
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
<?php if ($field->multiple):
|
||||||
isChecked="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
|
if ($field->type === 'group'): ?>
|
||||||
><?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?></x-Forms.Checkbox>
|
<div class="flex flex-col gap-4" data-field-array="<?= $field->key ?>">
|
||||||
<?php break;
|
<fieldset class="flex flex-col gap-6 rounded" data-field-array-container="<?= $field->key ?>">
|
||||||
case 'toggler': ?>
|
<legend class="relative z-10 mb-4 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] tracking-wide text-base"><?= $field->getTranslated($plugin->getKey(), 'label') ?></legend>
|
||||||
<x-Forms.Toggler
|
<?php
|
||||||
name="<?= $field->key ?>"
|
$fieldArrayValues = get_plugin_setting($plugin->getKey(), $field->key, $context) ?? [''];
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
foreach ($fieldArrayValues as $index => $value): ?>
|
||||||
isChecked="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
|
<fieldset class="relative flex flex-col border border-subtle p-4 rounded-tl-none rounded-md gap-2 bg-base" data-field-array-item="<?= $index ?>">
|
||||||
><?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?></x-Forms.Toggler>
|
<legend class="absolute font-mono left-0 -top-px -ml-6 rounded-l-full rounded-r-none w-6 text-xs h-6 inline-flex items-center justify-center font-semibold border border-subtle bg-base"><span class="sr-only"><?= $field->getTranslated($plugin->getKey(), 'label') ?></span> <span data-field-array-number><?= $index + 1 ?></span></legend>
|
||||||
<?php break;
|
<?php foreach ($field->fields as $subfield): ?>
|
||||||
case 'radio-group': ?>
|
<?= view('plugins/_field', [
|
||||||
<x-Forms.RadioGroup
|
'class' => 'flex-1',
|
||||||
name="<?= $field->key ?>"
|
'type' => $subfield->type,
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
'name' => sprintf('%s[%s][%s]', $field->key, $index, $subfield->key),
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
'label' => $subfield->getTranslated($plugin->getKey(), 'label'),
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
'hint' => $subfield->getTranslated($plugin->getKey(), 'hint'),
|
||||||
options="<?= esc(json_encode($field->getOptionsArray(sprintf('%s.settings.%s.%s.options', $plugin->getKey(), $type, $field->key)))) ?>"
|
'value' => $value[$subfield->key] ?? '',
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
'helper' => $subfield->getTranslated($plugin->getKey(), 'helper'),
|
||||||
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
|
'options' => esc(json_encode($subfield->getOptionsArray($plugin->getKey()))),
|
||||||
/>
|
'optional' => $subfield->optional,
|
||||||
<?php break;
|
]) ?>
|
||||||
case 'select': ?>
|
<?php endforeach; ?>
|
||||||
<x-Forms.Field
|
<x-IconButton variant="danger" glyph="delete-bin-fill" data-field-array-delete="<?= $index ?>" class="absolute right-0 top-0 -mt-4 -mr-4"><?= lang('Common.forms.fieldArray.remove') ?></x-IconButton>
|
||||||
as="Select"
|
</fieldset>
|
||||||
name="<?= $field->key ?>"
|
<?php endforeach; ?>
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
</fieldset>
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
<x-Button iconLeft="add-fill" data-field-array-add="<?= $field->key ?>" variant="secondary" type="button" class="mt-2"><?= lang('Common.forms.fieldArray.add') ?></x-Button>
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
</div>
|
||||||
options="<?= esc(json_encode($field->getOptionsArray(sprintf('%s.settings.%s.%s.options', $plugin->getKey(), $type, $field->key)))) ?>"
|
<?php else: ?>
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
<div class="flex flex-col gap-4" data-field-array="<?= $field->key ?>">
|
||||||
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
|
<fieldset class="flex flex-col gap-2" data-field-array-container="<?= $field->key ?>">
|
||||||
/>
|
<?php $fieldArrayValue = get_plugin_setting($plugin->getKey(), $field->key, $context) ?? [''];
|
||||||
<?php break;
|
foreach ($fieldArrayValue as $index => $value): ?>
|
||||||
case 'select-multiple': ?>
|
<div class="relative flex items-end" data-field-array-item="<?= $index ?>">
|
||||||
<x-Forms.Field
|
<span class="self-start mr-1 -ml-5 w-4 rtl text-sm before:content-['.']" data-field-array-number style="direction:rtl"><?= $index + 1 ?></span>
|
||||||
as="SelectMulti"
|
<?= view('plugins/_field', [
|
||||||
name="<?= $field->key ?>"
|
'class' => 'flex-1',
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
'type' => $field->type,
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
'name' => sprintf('%s[%s]', $field->key, $index),
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
'label' => $field->getTranslated($plugin->getKey(), 'label'),
|
||||||
options="<?= esc(json_encode($field->getOptionsArray(sprintf('%s.settings.%s.%s.options', $plugin->getKey(), $type, $field->key)))) ?>"
|
'hint' => $field->getTranslated($plugin->getKey(), 'hint'),
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
'value' => $value,
|
||||||
value="<?= esc(json_encode(get_plugin_setting($plugin->getKey(), $field->key, $context))) ?>"
|
'helper' => $field->getTranslated($plugin->getKey(), 'helper'),
|
||||||
/>
|
'options' => esc(json_encode($field->getOptionsArray($plugin->getKey()))),
|
||||||
<?php break;
|
'optional' => $field->optional,
|
||||||
case 'email': ?>
|
]) ?>
|
||||||
<x-Forms.Field
|
<x-IconButton variant="danger" glyph="delete-bin-fill" data-field-array-delete="<?= $index ?>" type="button" class="mb-2 ml-2"><?= lang('Common.forms.fieldArray.remove') ?></x-IconButton>
|
||||||
as="Input"
|
</div>
|
||||||
type="email"
|
<?php endforeach; ?>
|
||||||
name="<?= $field->key ?>"
|
</fieldset>
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
<x-Button iconLeft="add-fill" data-field-array-add="<?= $field->key ?>" variant="secondary" type="button" class="mt-2"><?= lang('Common.forms.fieldArray.add') ?></x-Button>
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
</div>
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
<?php endif; ?>
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
<?php elseif ($field->type === 'group'):
|
||||||
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
|
$value = get_plugin_setting($plugin->getKey(), $field->key, $context); ?>
|
||||||
/>
|
<fieldset class="flex flex-col border border-subtle p-4 rounded-tl-none rounded-md gap-2 bg-base">
|
||||||
<?php break;
|
<legend class="relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] tracking-wide text-base"><?= $field->getTranslated($plugin->getKey(), 'label') ?></legend>
|
||||||
case 'url': ?>
|
<?php foreach ($field->fields as $subfield): ?>
|
||||||
<x-Forms.Field
|
<?= view('plugins/_field', [
|
||||||
as="Input"
|
'class' => 'flex-1',
|
||||||
type="url"
|
'type' => $subfield->type,
|
||||||
placeholder="https://…"
|
'name' => sprintf('%s[%s]', $field->key, $subfield->key),
|
||||||
name="<?= $field->key ?>"
|
'label' => $subfield->getTranslated($plugin->getKey(), 'label'),
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
'hint' => $subfield->getTranslated($plugin->getKey(), 'hint'),
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
'value' => $value[$subfield->key] ?? '',
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
'helper' => $subfield->getTranslated($plugin->getKey(), 'helper'),
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
'options' => esc(json_encode($subfield->getOptionsArray($plugin->getKey()))),
|
||||||
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
|
'optional' => $subfield->optional,
|
||||||
/>
|
]) ?>
|
||||||
<?php break;
|
<?php endforeach; ?>
|
||||||
case 'number': ?>
|
</fieldset>
|
||||||
<x-Forms.Field
|
<?php else: ?>
|
||||||
as="Input"
|
<?= view('plugins/_field', [
|
||||||
type="number"
|
'class' => '',
|
||||||
name="<?= $field->key ?>"
|
'type' => $field->type,
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
'name' => $field->key,
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
'label' => $field->getTranslated($plugin->getKey(), 'label'),
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
'hint' => $field->getTranslated($plugin->getKey(), 'hint'),
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
'value' => get_plugin_setting($plugin->getKey(), $field->key, $context),
|
||||||
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
|
'helper' => $field->getTranslated($plugin->getKey(), 'helper'),
|
||||||
/>
|
'options' => esc(json_encode($field->getOptionsArray($plugin->getKey()))),
|
||||||
<?php break;
|
'optional' => $field->optional,
|
||||||
case 'textarea': ?>
|
]) ?>
|
||||||
<x-Forms.Field
|
<?php endif; ?>
|
||||||
as="Textarea"
|
|
||||||
name="<?= $field->key ?>"
|
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
|
||||||
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
|
|
||||||
/>
|
|
||||||
<?php break;
|
|
||||||
case 'markdown': ?>
|
|
||||||
<x-Forms.Field
|
|
||||||
as="MarkdownEditor"
|
|
||||||
name="<?= $field->key ?>"
|
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
|
||||||
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
|
|
||||||
/>
|
|
||||||
<?php break;
|
|
||||||
case 'datetime':
|
|
||||||
$hasDatetime = true ?>
|
|
||||||
<x-Forms.Field
|
|
||||||
as="DatetimePicker"
|
|
||||||
name="<?= $field->key ?>"
|
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
|
||||||
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
|
|
||||||
/>
|
|
||||||
<?php break;
|
|
||||||
default: ?>
|
|
||||||
<x-Forms.Field
|
|
||||||
as="Input"
|
|
||||||
name="<?= $field->key ?>"
|
|
||||||
label="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.label', $plugin->getKey(), $type, $field->key), $field->label)) ?>"
|
|
||||||
hint="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.hint', $plugin->getKey(), $type, $field->key), $field->hint)) ?>"
|
|
||||||
helper="<?= esc($field->getTranslated(sprintf('%s.settings.%s.%s.helper', $plugin->getKey(), $type, $field->key), $field->helper)) ?>"
|
|
||||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
|
||||||
value="<?= get_plugin_setting($plugin->getKey(), $field->key, $context) ?>"
|
|
||||||
/>
|
|
||||||
<?php endswitch; ?>
|
|
||||||
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<?php if ($hasDatetime): ?>
|
<?php if ($hasDatetime): ?>
|
||||||
|
@ -48,12 +48,12 @@
|
|||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.type.episodic'),
|
'label' => lang('Podcast.form.type.episodic'),
|
||||||
'value' => 'episodic',
|
'value' => 'episodic',
|
||||||
'hint' => lang('Podcast.form.type.episodic_hint'),
|
'description' => lang('Podcast.form.type.episodic_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.type.serial'),
|
'label' => lang('Podcast.form.type.serial'),
|
||||||
'value' => 'serial',
|
'value' => 'serial',
|
||||||
'hint' => lang('Podcast.form.type.serial_hint'),
|
'description' => lang('Podcast.form.type.serial_description'),
|
||||||
],
|
],
|
||||||
])) ?>"
|
])) ?>"
|
||||||
isRequired="true"
|
isRequired="true"
|
||||||
@ -66,17 +66,17 @@
|
|||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.medium.podcast'),
|
'label' => lang('Podcast.form.medium.podcast'),
|
||||||
'value' => 'podcast',
|
'value' => 'podcast',
|
||||||
'hint' => lang('Podcast.form.medium.podcast_hint'),
|
'description' => lang('Podcast.form.medium.podcast_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.medium.music'),
|
'label' => lang('Podcast.form.medium.music'),
|
||||||
'value' => 'music',
|
'value' => 'music',
|
||||||
'hint' => lang('Podcast.form.medium.music_hint'),
|
'description' => lang('Podcast.form.medium.music_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.medium.audiobook'),
|
'label' => lang('Podcast.form.medium.audiobook'),
|
||||||
'value' => 'audiobook',
|
'value' => 'audiobook',
|
||||||
'hint' => lang('Podcast.form.medium.audiobook_hint'),
|
'description' => lang('Podcast.form.medium.audiobook_description'),
|
||||||
],
|
],
|
||||||
])) ?>"
|
])) ?>"
|
||||||
isRequired="true"
|
isRequired="true"
|
||||||
|
@ -72,12 +72,12 @@
|
|||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.type.episodic'),
|
'label' => lang('Podcast.form.type.episodic'),
|
||||||
'value' => 'episodic',
|
'value' => 'episodic',
|
||||||
'hint' => lang('Podcast.form.type.episodic_hint'),
|
'description' => lang('Podcast.form.type.episodic_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.type.serial'),
|
'label' => lang('Podcast.form.type.serial'),
|
||||||
'value' => 'serial',
|
'value' => 'serial',
|
||||||
'hint' => lang('Podcast.form.type.serial_hint'),
|
'description' => lang('Podcast.form.type.serial_description'),
|
||||||
],
|
],
|
||||||
])) ?>"
|
])) ?>"
|
||||||
isRequired="true"
|
isRequired="true"
|
||||||
@ -91,17 +91,17 @@
|
|||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.medium.podcast'),
|
'label' => lang('Podcast.form.medium.podcast'),
|
||||||
'value' => 'podcast',
|
'value' => 'podcast',
|
||||||
'hint' => lang('Podcast.form.medium.podcast_hint'),
|
'description' => lang('Podcast.form.medium.podcast_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.medium.music'),
|
'label' => lang('Podcast.form.medium.music'),
|
||||||
'value' => 'music',
|
'value' => 'music',
|
||||||
'hint' => lang('Podcast.form.medium.music_hint'),
|
'description' => lang('Podcast.form.medium.music_description'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => lang('Podcast.form.medium.audiobook'),
|
'label' => lang('Podcast.form.medium.audiobook'),
|
||||||
'value' => 'audiobook',
|
'value' => 'audiobook',
|
||||||
'hint' => lang('Podcast.form.medium.audiobook_hint'),
|
'description' => lang('Podcast.form.medium.audiobook_description'),
|
||||||
],
|
],
|
||||||
])) ?>"
|
])) ?>"
|
||||||
isRequired="true"
|
isRequired="true"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user