From 84cae5fce935224d6ee826a193623a3def0a52e2 Mon Sep 17 00:00:00 2001
From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com>
Date: Sat, 22 Feb 2025 12:11:33 +0100
Subject: [PATCH 1/4] Rewrite image input
---
formwork/fields/image.php | 35 +++++++++++++++++--
formwork/translations/en.yaml | 1 +
panel/src/scss/components/_dropdowns.scss | 16 +++++++++
.../scss/components/forms/_forms-image.scss | 4 ---
panel/src/ts/components/inputs.ts | 3 --
panel/src/ts/components/inputs/image-input.ts | 24 -------------
.../src/ts/components/inputs/select-input.ts | 17 +++++++--
panel/views/fields/image.php | 26 +++++++-------
site/schemes/pages/post.yaml | 6 ++--
site/templates/partials/cover-image.php | 4 +--
10 files changed, 83 insertions(+), 53 deletions(-)
delete mode 100644 panel/src/ts/components/inputs/image-input.ts
diff --git a/formwork/fields/image.php b/formwork/fields/image.php
index 440a31299..40a340159 100644
--- a/formwork/fields/image.php
+++ b/formwork/fields/image.php
@@ -1,14 +1,36 @@
function (Field $field): Field {
- return $field;
+ 'return' => function (Field $field): ?Image {
+ return $field->value() !== null
+ ? $field->getImages()->get($field->value())
+ : null;
+ },
+
+ 'getImages' => function (Field $field): FileCollection {
+ if (!$field->has('options')) {
+ $model = $field->parent()?->model();
+
+ if ($model === null || !method_exists($model, 'files')) {
+ throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type()));
+ }
+
+ $files = $model->files();
+ } else {
+ $files = $field->get('options');
+ }
+
+ return $files->filter(static fn(File $file) => $file instanceof Image);
},
'validate' => function (Field $field, $value): ?string {
@@ -22,5 +44,14 @@
return $value;
},
+
+ 'options' => function (Field $field): array {
+ return $field->getImages()
+ ->map(static fn(Image $image) => [
+ 'value' => $image->name(),
+ 'icon' => 'image',
+ 'thumb' => $image->square(300, 'contain')->uri(),
+ ])->toArray();
+ },
];
};
diff --git a/formwork/translations/en.yaml b/formwork/translations/en.yaml
index cc5823f01..e0702e73e 100644
--- a/formwork/translations/en.yaml
+++ b/formwork/translations/en.yaml
@@ -18,6 +18,7 @@ date.weekdays.short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
fields.array.add: Add
fields.array.remove: Remove
fields.file.uploadLabel: Click to choose a file to upload or drag it here
+fields.image.none: (None)
fields.select.empty: No matching options
file.metadata: Metadata
file.metadata.alternativeText: Alternative text
diff --git a/panel/src/scss/components/_dropdowns.scss b/panel/src/scss/components/_dropdowns.scss
index f5ed026f2..5ae86f478 100644
--- a/panel/src/scss/components/_dropdowns.scss
+++ b/panel/src/scss/components/_dropdowns.scss
@@ -95,3 +95,19 @@
border-top: 1px solid var(--color-base-500);
margin: 0.25rem 0;
}
+
+.dropdown-list .dropdown-item > .dropdown-thumb {
+ display: inline-block;
+ width: 1.5rem;
+ height: 1.5rem;
+ border-radius: $border-radius;
+ margin: -0.75rem 0.5rem -0.5rem 0;
+ background-color: var(--color-base-700);
+ object-fit: contain;
+ vertical-align: middle;
+}
+
+.dropdown-list .dropdown-item > .icon {
+ margin-right: 0.5rem;
+ margin-left: 0.25rem;
+}
diff --git a/panel/src/scss/components/forms/_forms-image.scss b/panel/src/scss/components/forms/_forms-image.scss
index 0c0a17631..2f552f2b6 100644
--- a/panel/src/scss/components/forms/_forms-image.scss
+++ b/panel/src/scss/components/forms/_forms-image.scss
@@ -1,10 +1,6 @@
@use "../mixins" as *;
@use "../variables" as *;
-.form-input-image {
- cursor: default;
-}
-
.image-picker-thumbnails {
display: flex;
overflow: auto;
diff --git a/panel/src/ts/components/inputs.ts b/panel/src/ts/components/inputs.ts
index 87bbd00eb..519f5d935 100644
--- a/panel/src/ts/components/inputs.ts
+++ b/panel/src/ts/components/inputs.ts
@@ -5,7 +5,6 @@ import { DateInput } from "./inputs/date-input";
import { DurationInput } from "./inputs/duration-input";
import { EditorInput } from "./inputs/editor-input";
import { FileInput } from "./inputs/file-input";
-import { ImageInput } from "./inputs/image-input";
import { ImagePicker } from "./inputs/image-picker";
import { RangeInput } from "./inputs/range-input";
import { SelectInput } from "./inputs/select-input";
@@ -18,8 +17,6 @@ export class Inputs {
constructor(parent: HTMLElement) {
$$(".form-input-date", parent).forEach((element: HTMLInputElement) => (this[element.name] = new DateInput(element, app.config.DateInput)));
- $$(".form-input-image", parent).forEach((element: HTMLInputElement) => (this[element.name] = new ImageInput(element)));
-
$$(".image-picker", parent).forEach((element: HTMLSelectElement) => (this[element.name] = new ImagePicker(element)));
$$(".editor-textarea", parent).forEach((element: HTMLTextAreaElement) => (this[element.name] = new EditorInput(element)));
diff --git a/panel/src/ts/components/inputs/image-input.ts b/panel/src/ts/components/inputs/image-input.ts
deleted file mode 100644
index cd2f1ec8f..000000000
--- a/panel/src/ts/components/inputs/image-input.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { $ } from "../../utils/selectors";
-import { app } from "../../app";
-
-export class ImageInput {
- constructor(element: HTMLInputElement) {
- element.addEventListener("click", () => {
- app.modals["imagesModal"].show(undefined, (modal) => {
- const selected = $(".image-picker-thumbnail.selected", modal.element);
- if (selected) {
- selected.classList.remove("selected");
- }
- if (element.value) {
- const thumbnail = $(`.image-picker-thumbnail[data-filename="${element.value}"]`, modal.element);
- if (thumbnail) {
- thumbnail.classList.add("selected");
- }
- }
- const confirm = $("[data-command=pick-image]", modal.element) as HTMLElement;
- confirm.dataset.target = element.id;
- confirm.addEventListener("click", () => modal.hide());
- });
- });
- }
-}
diff --git a/panel/src/ts/components/inputs/select-input.ts b/panel/src/ts/components/inputs/select-input.ts
index df85710aa..a247bc34e 100644
--- a/panel/src/ts/components/inputs/select-input.ts
+++ b/panel/src/ts/components/inputs/select-input.ts
@@ -1,5 +1,6 @@
import { $, $$ } from "../../utils/selectors";
import { escapeRegExp, makeDiacriticsRegExp } from "../../utils/validation";
+import { insertIcon } from "../icons";
type SelectInputListItem = {
label: string;
@@ -120,7 +121,19 @@ export class SelectInput {
item.classList.add("disabled");
}
+ if (option.dataset.thumb) {
+ const img = document.createElement("img");
+ img.src = option.dataset.thumb;
+ img.className = "dropdown-thumb";
+ item.insertAdjacentElement("afterbegin", img);
+ } else if (option.dataset.icon) {
+ insertIcon(option.dataset.icon, item);
+ }
+
for (const key in option.dataset) {
+ if (["icon", "thumb"].includes(key)) {
+ continue;
+ }
item.dataset[key] = option.dataset[key];
}
@@ -358,7 +371,7 @@ export class SelectInput {
function setCurrent(item: HTMLElement) {
select.value = item.dataset.value as string;
- labelInput.value = item.innerText;
+ labelInput.value = item.innerText.trim();
select.dispatchEvent(new Event("input", { bubbles: true }));
select.dispatchEvent(new Event("change", { bubbles: true }));
}
@@ -368,7 +381,7 @@ export class SelectInput {
}
function getCurrentLabel() {
- return getCurrent().innerText;
+ return getCurrent().innerText.trim();
}
function selectCurrent() {
diff --git a/panel/views/fields/image.php b/panel/views/fields/image.php
index 20953c0f4..4087ade4c 100644
--- a/panel/views/fields/image.php
+++ b/panel/views/fields/image.php
@@ -1,17 +1,19 @@
layout('fields.field') ?>
= $this->icon($field->get('icon', 'image')) ?>
- attr([
- 'type' => 'text',
- 'class' => ['form-input', 'form-input-image'],
- 'id' => $field->name(),
- 'name' => $field->formName(),
- 'value' => basename($field->value() ?? ''),
- 'placeholder' => $field->placeholder(),
- 'readonly' => true,
- 'required' => $field->isRequired(),
- 'disabled' => $field->isDisabled(),
- 'hidden' => $field->isHidden(),
+ attr([
+ 'class' => 'form-select',
+ 'id' => $field->name(),
+ 'name' => $field->formName(),
+ 'required' => $field->isRequired(),
+ 'disabled' => $field->isDisabled(),
+ 'hidden' => $field->isHidden(),
]) ?>>
- = $this->icon('times-circle') ?>
+ isRequired()): ?>
+ = $this->translate('fields.image.none') ?>
+
+ options() as $value => $label) : ?>
+ attr(['value' => $value, 'selected' => $value == $field->value(), 'data-icon' => $label['icon'], 'data-thumb' => $label['thumb']]) ?>>= $this->escape($label['value']) ?>
+
+
\ No newline at end of file
diff --git a/site/schemes/pages/post.yaml b/site/schemes/pages/post.yaml
index df3063faf..c2ead81cb 100644
--- a/site/schemes/pages/post.yaml
+++ b/site/schemes/pages/post.yaml
@@ -10,7 +10,7 @@ options:
layout:
sections:
content:
- fields: [title, image, tags, summary, content]
+ fields: [title, coverImage, tags, summary, content]
fields:
summary:
@@ -18,11 +18,9 @@ fields:
label: '{{page.summary}}'
rows: 5
- image:
+ coverImage:
type: image
- default: null
label: '{{page.image}}'
- placeholder: '{{page.noImage}}'
tags:
type: tags
diff --git a/site/templates/partials/cover-image.php b/site/templates/partials/cover-image.php
index a04d7949c..493f2ed8c 100644
--- a/site/templates/partials/cover-image.php
+++ b/site/templates/partials/cover-image.php
@@ -1,3 +1,3 @@
-has('image') && !$page->image()->isEmpty() && $page->images()->has($page->image())) : ?>
-
+has('coverImage') && ($image = $page->coverImage())) : ?>
+
\ No newline at end of file
From 395a6bd32850f117bcb95d1e9dfec0923afb7ed6 Mon Sep 17 00:00:00 2001
From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com>
Date: Sat, 22 Feb 2025 12:14:14 +0100
Subject: [PATCH 2/4] Add file input and multiple value variants files and
images
---
formwork/fields/file.php | 61 ++++++++++++++++
formwork/fields/files.php | 79 +++++++++++++++++++++
formwork/fields/images.php | 75 +++++++++++++++++++
formwork/translations/en.yaml | 2 +
panel/src/ts/components/inputs/tag-input.ts | 26 ++++++-
panel/views/fields/file.php | 19 +++++
panel/views/fields/files.php | 20 ++++++
panel/views/fields/images.php | 20 ++++++
8 files changed, 299 insertions(+), 3 deletions(-)
create mode 100644 formwork/fields/file.php
create mode 100644 formwork/fields/files.php
create mode 100644 formwork/fields/images.php
create mode 100644 panel/views/fields/file.php
create mode 100644 panel/views/fields/files.php
create mode 100644 panel/views/fields/images.php
diff --git a/formwork/fields/file.php b/formwork/fields/file.php
new file mode 100644
index 000000000..82e070075
--- /dev/null
+++ b/formwork/fields/file.php
@@ -0,0 +1,61 @@
+ function (Field $field): ?File {
+ return $field->value() !== null
+ ? $field->getFiles()->get($field->value())
+ : null;
+ },
+
+ 'getFiles' => function (Field $field): FileCollection {
+ if (!$field->has('options')) {
+ $model = $field->parent()?->model();
+
+ if ($model === null || !method_exists($model, 'files')) {
+ throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type()));
+ }
+
+ return $model->files();
+ }
+
+ return $field->get('options');
+ },
+
+ 'validate' => function (Field $field, $value): ?string {
+ if (Constraint::isEmpty($value)) {
+ return null;
+ }
+
+ if (!is_string($value)) {
+ throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
+ }
+
+ return $value;
+ },
+
+ 'options' => function (Field $field): array {
+ $collection = $field->getFiles();
+
+ if ($field->has('fileType')) {
+ $collection = $collection->filter(static fn(File $file) => in_array($file->type(), (array) $field->get('fileType'), true));
+ }
+
+ return $collection
+ ->map(static fn(File $file) => [
+ 'value' => $file->name(),
+ 'icon' => 'file-' . $file->type(),
+ 'thumb' => $file instanceof Image ? $file->square(300, 'contain')->uri() : null,
+ ])->toArray();
+ },
+ ];
+};
diff --git a/formwork/fields/files.php b/formwork/fields/files.php
new file mode 100644
index 000000000..215f6d890
--- /dev/null
+++ b/formwork/fields/files.php
@@ -0,0 +1,79 @@
+ function (Field $field): FileCollection {
+ if (!$field->has('options')) {
+ $model = $field->parent()?->model();
+
+ if ($model === null || !method_exists($model, 'files')) {
+ throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type()));
+ }
+
+ return $model->files();
+ }
+
+ return $field->get('options');
+ },
+
+ 'toString' => function ($field) {
+ return implode(', ', $field->value() ?? []);
+ },
+
+ 'return' => function (Field $field): FileCollection {
+ return $field->getFiles()->filter(static fn(File $file) => in_array($file->name(), $field->value(), true));
+ },
+
+ 'validate' => function (Field $field, $value): array {
+ if (Constraint::isEmpty($value)) {
+ return [];
+ }
+
+ if (is_string($value)) {
+ $value = array_map(trim(...), explode(',', $value));
+ }
+
+ if (!is_array($value)) {
+ throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
+ }
+
+ if ($field->has('pattern')) {
+ $value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern')));
+ }
+
+ if ($field->has('limit') && count($value) > $field->get('limit')) {
+ throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit')));
+ }
+
+ return array_values(array_filter($value));
+ },
+
+ 'options' => function (Field $field): array {
+ $collection = $field->getFiles();
+
+ if ($field->has('fileType')) {
+ $collection = $collection->filter(static fn(File $file) => in_array($file->type(), (array) $field->get('fileType'), true));
+ }
+
+ return $collection
+ ->map(static fn(File $file) => [
+ 'value' => $file->name(),
+ 'icon' => 'file-' . $file->type(),
+ 'thumb' => $file instanceof Image ? $file->square(300, 'contain')->uri() : null,
+ ])->toArray();
+ },
+
+ 'limit' => function (Field $field): ?int {
+ return $field->get('limit', null);
+ },
+ ];
+};
diff --git a/formwork/fields/images.php b/formwork/fields/images.php
new file mode 100644
index 000000000..e8c27aa56
--- /dev/null
+++ b/formwork/fields/images.php
@@ -0,0 +1,75 @@
+ function (Field $field): FileCollection {
+ if (!$field->has('options')) {
+ $model = $field->parent()?->model();
+
+ if ($model === null || !method_exists($model, 'files')) {
+ throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type()));
+ }
+
+ $files = $model->files();
+ } else {
+ $files = $field->get('options');
+ }
+
+ return $files->filter(static fn(File $file) => $file instanceof Image);
+ },
+
+ 'toString' => function ($field) {
+ return implode(', ', $field->value() ?? []);
+ },
+
+ 'return' => function (Field $field): FileCollection {
+ return $field->getImages()->filter(static fn(File $file) => in_array($file->name(), $field->value(), true));
+ },
+
+ 'validate' => function (Field $field, $value): array {
+ if (Constraint::isEmpty($value)) {
+ return [];
+ }
+
+ if (is_string($value)) {
+ $value = array_map(trim(...), explode(',', $value));
+ }
+
+ if (!is_array($value)) {
+ throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
+ }
+
+ if ($field->has('pattern')) {
+ $value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern')));
+ }
+
+ if ($field->has('limit') && count($value) > $field->get('limit')) {
+ throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit')));
+ }
+
+ return array_values(array_filter($value));
+ },
+
+ 'options' => function (Field $field): array {
+ return $field->getImages()
+ ->map(static fn(Image $image) => [
+ 'value' => $image->name(),
+ 'icon' => 'image',
+ 'thumb' => $image->square(300, 'contain')->uri(),
+ ])->toArray();
+ },
+
+ 'limit' => function (Field $field): ?int {
+ return $field->get('limit', null);
+ },
+ ];
+};
diff --git a/formwork/translations/en.yaml b/formwork/translations/en.yaml
index e0702e73e..bfed02359 100644
--- a/formwork/translations/en.yaml
+++ b/formwork/translations/en.yaml
@@ -17,6 +17,7 @@ date.weekdays.long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Fr
date.weekdays.short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
fields.array.add: Add
fields.array.remove: Remove
+fields.file.none: (None)
fields.file.uploadLabel: Click to choose a file to upload or drag it here
fields.image.none: (None)
fields.select.empty: No matching options
@@ -29,6 +30,7 @@ page.files: Files
page.image: Image
page.listed: Listed
page.listed.description: Listed pages are visible in the menu
+page.noFiles: No files
page.noImage: No image
page.none: (None)
page.noTags: No tags
diff --git a/panel/src/ts/components/inputs/tag-input.ts b/panel/src/ts/components/inputs/tag-input.ts
index c4afdc29f..2db60f591 100644
--- a/panel/src/ts/components/inputs/tag-input.ts
+++ b/panel/src/ts/components/inputs/tag-input.ts
@@ -1,6 +1,7 @@
import { $, $$ } from "../../utils/selectors";
import { escapeRegExp, makeDiacriticsRegExp } from "../../utils/validation";
import { debounce } from "../../utils/events";
+import { insertIcon } from "../icons";
import Sortable from "sortablejs";
interface TagInputOptions {
@@ -11,6 +12,12 @@ interface TagInputOptions {
orderable: boolean;
}
+interface TagInputDropdownItem {
+ value: string;
+ icon?: string;
+ thumb?: string;
+}
+
export class TagInput {
constructor(input: HTMLInputElement, userOptions: Partial) {
const defaults = { addKeyCodes: ["Comma"], limit: Infinity, accept: "options", orderable: true };
@@ -130,7 +137,7 @@ export class TagInput {
function createDropdown() {
if ("options" in input.dataset) {
- const list: { [key: string | number]: string } = JSON.parse(input.dataset.options ?? "{}");
+ const list: { [key: string | number]: string | TagInputDropdownItem } = JSON.parse(input.dataset.options ?? "{}");
const isAssociative = !Array.isArray(list);
if ("accept" in input.dataset) {
@@ -143,9 +150,22 @@ export class TagInput {
for (const key in list) {
const item = document.createElement("div");
+
+ const { value, icon, thumb } = typeof list[key] === "object" ? list[key] : { value: list[key], icon: undefined, thumb: undefined };
+
item.className = "dropdown-item";
- item.innerHTML = list[key];
- item.dataset.value = isAssociative ? key : list[key];
+ item.innerHTML = value;
+ item.dataset.value = isAssociative ? key : value;
+
+ if (thumb) {
+ const img = document.createElement("img");
+ img.src = thumb;
+ img.className = "dropdown-thumb";
+ item.insertAdjacentElement("afterbegin", img);
+ } else if (icon) {
+ insertIcon(icon, item);
+ }
+
item.addEventListener("click", function () {
if (this.dataset.value) {
addTag(this.dataset.value);
diff --git a/panel/views/fields/file.php b/panel/views/fields/file.php
new file mode 100644
index 000000000..8a9b57cb5
--- /dev/null
+++ b/panel/views/fields/file.php
@@ -0,0 +1,19 @@
+layout('fields.field') ?>
+
+ = $this->icon($field->get('icon', 'file')) ?>
+ attr([
+ 'class' => 'form-select',
+ 'id' => $field->name(),
+ 'name' => $field->formName(),
+ 'required' => $field->isRequired(),
+ 'disabled' => $field->isDisabled(),
+ 'hidden' => $field->isHidden(),
+ ]) ?>>
+ isRequired()): ?>
+ = $this->translate('fields.file.none') ?>
+
+ options() as $value => $label) : ?>
+ attr(['value' => $value, 'selected' => $value == $field->value(), 'data-icon' => $label['icon'], 'data-thumb' => $label['thumb']]) ?>>= $this->escape($label['value']) ?>
+
+
+
\ No newline at end of file
diff --git a/panel/views/fields/files.php b/panel/views/fields/files.php
new file mode 100644
index 000000000..e88da1b47
--- /dev/null
+++ b/panel/views/fields/files.php
@@ -0,0 +1,20 @@
+layout('fields.field') ?>
+
+ = $this->icon($field->get('icon', 'file')) ?>
+ attr([
+ 'class' => 'form-input',
+ 'type' => 'text',
+ 'id' => $field->name(),
+ 'name' => $field->formName(),
+ 'value' => implode(', ', (array) $field->value()),
+ 'placeholder' => $field->placeholder(),
+ 'required' => $field->isRequired(),
+ 'disabled' => $field->isDisabled(),
+ 'hidden' => $field->isHidden(),
+ 'data-field' => 'tags',
+ 'data-limit' => $field->limit(),
+ 'data-options' => Formwork\Parsers\Json::encode($field->options()),
+ 'data-accept' => 'options',
+ 'data-orderable' => $field->is('orderable', true),
+ ]) ?>>
+
\ No newline at end of file
diff --git a/panel/views/fields/images.php b/panel/views/fields/images.php
new file mode 100644
index 000000000..16a236d83
--- /dev/null
+++ b/panel/views/fields/images.php
@@ -0,0 +1,20 @@
+layout('fields.field') ?>
+
+ = $this->icon($field->get('icon', 'image')) ?>
+ attr([
+ 'class' => 'form-input',
+ 'type' => 'text',
+ 'id' => $field->name(),
+ 'name' => $field->formName(),
+ 'value' => implode(', ', (array) $field->value()),
+ 'placeholder' => $field->placeholder(),
+ 'required' => $field->isRequired(),
+ 'disabled' => $field->isDisabled(),
+ 'hidden' => $field->isHidden(),
+ 'data-field' => 'tags',
+ 'data-limit' => $field->limit(),
+ 'data-options' => Formwork\Parsers\Json::encode($field->options()),
+ 'data-accept' => 'options',
+ 'data-orderable' => $field->is('orderable', true),
+ ]) ?>>
+
\ No newline at end of file
From e5f96a02a4d3dd4dcef4b3b19310b18bf6def711 Mon Sep 17 00:00:00 2001
From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com>
Date: Sat, 22 Feb 2025 12:49:13 +0100
Subject: [PATCH 3/4] Remove tag input naming inconsistencies
---
panel/src/scss/components/_forms.scss | 2 +-
.../{_forms-tag.scss => _forms-tags.scss} | 32 +++++++------------
panel/src/ts/components/inputs.ts | 4 +--
.../inputs/{tag-input.ts => tags-input.ts} | 18 +++++------
panel/views/fields/files.php | 3 +-
panel/views/fields/images.php | 3 +-
panel/views/fields/tags.php | 3 +-
7 files changed, 27 insertions(+), 38 deletions(-)
rename panel/src/scss/components/forms/{_forms-tag.scss => _forms-tags.scss} (77%)
rename panel/src/ts/components/inputs/{tag-input.ts => tags-input.ts} (98%)
diff --git a/panel/src/scss/components/_forms.scss b/panel/src/scss/components/_forms.scss
index de17eddcc..90bdecd88 100644
--- a/panel/src/scss/components/_forms.scss
+++ b/panel/src/scss/components/_forms.scss
@@ -8,5 +8,5 @@
@use "forms/forms-file";
@use "forms/forms-image";
@use "forms/forms-range";
-@use "forms/forms-tag";
+@use "forms/forms-tags";
@use "forms/forms-togglegroup";
diff --git a/panel/src/scss/components/forms/_forms-tag.scss b/panel/src/scss/components/forms/_forms-tags.scss
similarity index 77%
rename from panel/src/scss/components/forms/_forms-tag.scss
rename to panel/src/scss/components/forms/_forms-tags.scss
index fbe9f6312..04632bed1 100644
--- a/panel/src/scss/components/forms/_forms-tag.scss
+++ b/panel/src/scss/components/forms/_forms-tags.scss
@@ -1,7 +1,7 @@
@use "../mixins" as *;
@use "../variables" as *;
-.form-input-tag {
+.form-input-tags-wrap {
position: relative;
display: block;
box-sizing: border-box;
@@ -16,19 +16,19 @@
@include user-select-none;
}
-.form-input-wrap > .form-input-tag {
+.form-input-wrap > .form-input-tags-wrap {
margin-bottom: 0;
}
-.form-input-wrap .form-input-icon + .form-input-tag {
- padding-left: 1.5rem;
+.form-input-wrap .form-input-icon + .form-input-tags-wrap {
+ padding-left: 1.75rem;
}
-.form-input-tag.focused {
+.form-input-tags-wrap.focused {
border-color: var(--color-accent-500);
}
-.tag-inner-input {
+.form-input-tags-wrap .form-input {
display: inline-block;
width: auto;
max-width: 100%;
@@ -46,11 +46,7 @@
}
}
-.form-input-tag-hidden {
- display: none;
-}
-
-.form-input-tag .tag {
+.form-input-tags-wrap .tag {
display: inline-block;
box-sizing: border-box;
padding: 0 0.375rem;
@@ -65,16 +61,12 @@
}
}
-.form-input-tag .tag:first-child {
- margin-left: 0.25rem;
-}
-
-.form-input-tag.disabled,
-.form-input-tag.disabled .tag-inner-input {
+.form-input-tags-wrap.disabled,
+.form-input-tags-wrap.disabled .form-input {
background-color: var(--color-base-800);
}
-.form-input-tag.disabled .tag {
+.form-input-tags-wrap.disabled .tag {
background-color: var(--color-base-600);
}
@@ -96,7 +88,7 @@
font-weight: 600;
}
-.form-input-tag.is-dragging,
-.form-input-tag.is-dragging * {
+.form-input-tags-wrap.is-dragging,
+.form-input-tags-wrap.is-dragging * {
cursor: grabbing !important;
}
diff --git a/panel/src/ts/components/inputs.ts b/panel/src/ts/components/inputs.ts
index 519f5d935..131a5f1f4 100644
--- a/panel/src/ts/components/inputs.ts
+++ b/panel/src/ts/components/inputs.ts
@@ -9,7 +9,7 @@ import { ImagePicker } from "./inputs/image-picker";
import { RangeInput } from "./inputs/range-input";
import { SelectInput } from "./inputs/select-input";
import { SlugInput } from "./inputs/slug-input";
-import { TagInput } from "./inputs/tag-input";
+import { TagsInput } from "./inputs/tags-input";
export class Inputs {
[name: string]: object;
@@ -23,7 +23,7 @@ export class Inputs {
$$("input[type=file]", parent).forEach((element: HTMLInputElement) => (this[element.name] = new FileInput(element)));
- $$("input[data-field=tags]", parent).forEach((element: HTMLInputElement) => (this[element.name] = new TagInput(element, app.config.TagInput)));
+ $$(".form-input-tags", parent).forEach((element: HTMLInputElement) => (this[element.name] = new TagsInput(element, app.config.TagInput)));
$$("input[data-field=duration]", parent).forEach((element: HTMLInputElement) => (this[element.name] = new DurationInput(element, app.config.DurationInput)));
diff --git a/panel/src/ts/components/inputs/tag-input.ts b/panel/src/ts/components/inputs/tags-input.ts
similarity index 98%
rename from panel/src/ts/components/inputs/tag-input.ts
rename to panel/src/ts/components/inputs/tags-input.ts
index 2db60f591..ee31fecd6 100644
--- a/panel/src/ts/components/inputs/tag-input.ts
+++ b/panel/src/ts/components/inputs/tags-input.ts
@@ -4,7 +4,7 @@ import { debounce } from "../../utils/events";
import { insertIcon } from "../icons";
import Sortable from "sortablejs";
-interface TagInputOptions {
+interface TagsInputOptions {
labels: { [key: string]: string };
addKeyCodes: string[];
limit: number;
@@ -12,14 +12,14 @@ interface TagInputOptions {
orderable: boolean;
}
-interface TagInputDropdownItem {
+interface TagsInputDropdownItem {
value: string;
icon?: string;
thumb?: string;
}
-export class TagInput {
- constructor(input: HTMLInputElement, userOptions: Partial) {
+export class TagsInput {
+ constructor(input: HTMLInputElement, userOptions: Partial) {
const defaults = { addKeyCodes: ["Comma"], limit: Infinity, accept: "options", orderable: true };
const options = Object.assign({}, defaults, userOptions);
@@ -49,15 +49,15 @@ export class TagInput {
options.orderable = false;
}
- field.className = "form-input-tag";
+ field.className = "form-input-tags-wrap";
- innerInput.className = "form-input tag-inner-input";
+ innerInput.className = "form-input";
innerInput.type = "text";
+ innerInput.id = input.id;
innerInput.placeholder = input.placeholder;
- hiddenInput.className = "form-input-tag-hidden";
+ hiddenInput.className = "form-input-hidden";
hiddenInput.name = input.name;
- hiddenInput.id = input.id;
hiddenInput.type = "text";
hiddenInput.value = input.value;
hiddenInput.readOnly = true;
@@ -137,7 +137,7 @@ export class TagInput {
function createDropdown() {
if ("options" in input.dataset) {
- const list: { [key: string | number]: string | TagInputDropdownItem } = JSON.parse(input.dataset.options ?? "{}");
+ const list: { [key: string | number]: string | TagsInputDropdownItem } = JSON.parse(input.dataset.options ?? "{}");
const isAssociative = !Array.isArray(list);
if ("accept" in input.dataset) {
diff --git a/panel/views/fields/files.php b/panel/views/fields/files.php
index e88da1b47..1a9650841 100644
--- a/panel/views/fields/files.php
+++ b/panel/views/fields/files.php
@@ -2,7 +2,7 @@
= $this->icon($field->get('icon', 'file')) ?>
attr([
- 'class' => 'form-input',
+ 'class' => ['form-input', 'form-input-tags'],
'type' => 'text',
'id' => $field->name(),
'name' => $field->formName(),
@@ -11,7 +11,6 @@
'required' => $field->isRequired(),
'disabled' => $field->isDisabled(),
'hidden' => $field->isHidden(),
- 'data-field' => 'tags',
'data-limit' => $field->limit(),
'data-options' => Formwork\Parsers\Json::encode($field->options()),
'data-accept' => 'options',
diff --git a/panel/views/fields/images.php b/panel/views/fields/images.php
index 16a236d83..453221dff 100644
--- a/panel/views/fields/images.php
+++ b/panel/views/fields/images.php
@@ -2,7 +2,7 @@
= $this->icon($field->get('icon', 'image')) ?>
attr([
- 'class' => 'form-input',
+ 'class' => ['form-input', 'form-input-tags'],
'type' => 'text',
'id' => $field->name(),
'name' => $field->formName(),
@@ -11,7 +11,6 @@
'required' => $field->isRequired(),
'disabled' => $field->isDisabled(),
'hidden' => $field->isHidden(),
- 'data-field' => 'tags',
'data-limit' => $field->limit(),
'data-options' => Formwork\Parsers\Json::encode($field->options()),
'data-accept' => 'options',
diff --git a/panel/views/fields/tags.php b/panel/views/fields/tags.php
index f1e92d935..f0c6e601a 100644
--- a/panel/views/fields/tags.php
+++ b/panel/views/fields/tags.php
@@ -2,7 +2,7 @@
= $this->icon($field->get('icon', 'tag')) ?>
attr([
- 'class' => 'form-input',
+ 'class' => ['form-input', 'form-input-tags'],
'type' => 'text',
'id' => $field->name(),
'name' => $field->formName(),
@@ -11,7 +11,6 @@
'required' => $field->isRequired(),
'disabled' => $field->isDisabled(),
'hidden' => $field->isHidden(),
- 'data-field' => 'tags',
'data-limit' => $field->get('limit'),
'data-options' => $field->has('options') ? Formwork\Parsers\Json::encode($field->options()) : null,
'data-accept' => $field->get('accept', 'options'),
From fb154eb4cec46f375e890dd50a15d75722fcf8e6 Mon Sep 17 00:00:00 2001
From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com>
Date: Sat, 22 Feb 2025 13:01:10 +0100
Subject: [PATCH 4/4] Remove tags, images, and files methods inconsistencies
---
formwork/fields/files.php | 6 +++++-
formwork/fields/images.php | 6 +++++-
formwork/fields/tags.php | 10 +++++++++-
panel/views/fields/files.php | 2 +-
panel/views/fields/images.php | 2 +-
panel/views/fields/tags.php | 8 ++++----
6 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/formwork/fields/files.php b/formwork/fields/files.php
index 215f6d890..4be82e2cf 100644
--- a/formwork/fields/files.php
+++ b/formwork/fields/files.php
@@ -50,7 +50,7 @@
$value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern')));
}
- if ($field->has('limit') && count($value) > $field->get('limit')) {
+ if ($field->limit() !== null && count($value) > $field->limit()) {
throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit')));
}
@@ -75,5 +75,9 @@
'limit' => function (Field $field): ?int {
return $field->get('limit', null);
},
+
+ 'isOrderable' => function ($field): bool {
+ return $field->is('orderable', true);
+ },
];
};
diff --git a/formwork/fields/images.php b/formwork/fields/images.php
index e8c27aa56..a15179527 100644
--- a/formwork/fields/images.php
+++ b/formwork/fields/images.php
@@ -52,7 +52,7 @@
$value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern')));
}
- if ($field->has('limit') && count($value) > $field->get('limit')) {
+ if ($field->limit() !== null && count($value) > $field->limit()) {
throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit')));
}
@@ -71,5 +71,9 @@
'limit' => function (Field $field): ?int {
return $field->get('limit', null);
},
+
+ 'isOrderable' => function ($field): bool {
+ return $field->is('orderable', true);
+ },
];
};
diff --git a/formwork/fields/tags.php b/formwork/fields/tags.php
index 7cc21c316..9322fe29e 100644
--- a/formwork/fields/tags.php
+++ b/formwork/fields/tags.php
@@ -34,7 +34,7 @@
$value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern')));
}
- if ($field->has('limit') && count($value) > $field->get('limit')) {
+ if ($field->limit() !== null && count($value) > $field->limit()) {
throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit')));
}
@@ -47,8 +47,16 @@
return $options !== null ? Arr::from($options) : null;
},
+ 'accept' => function ($field): string {
+ return $field->get('accept', 'options');
+ },
+
'limit' => function ($field): ?int {
return $field->get('limit', null);
},
+
+ 'isOrderable' => function ($field): bool {
+ return $field->is('orderable', true);
+ },
];
};
diff --git a/panel/views/fields/files.php b/panel/views/fields/files.php
index 1a9650841..f82743953 100644
--- a/panel/views/fields/files.php
+++ b/panel/views/fields/files.php
@@ -14,6 +14,6 @@
'data-limit' => $field->limit(),
'data-options' => Formwork\Parsers\Json::encode($field->options()),
'data-accept' => 'options',
- 'data-orderable' => $field->is('orderable', true),
+ 'data-orderable' => $field->isOrderable(),
]) ?>>
\ No newline at end of file
diff --git a/panel/views/fields/images.php b/panel/views/fields/images.php
index 453221dff..0b15ee773 100644
--- a/panel/views/fields/images.php
+++ b/panel/views/fields/images.php
@@ -14,6 +14,6 @@
'data-limit' => $field->limit(),
'data-options' => Formwork\Parsers\Json::encode($field->options()),
'data-accept' => 'options',
- 'data-orderable' => $field->is('orderable', true),
+ 'data-orderable' => $field->isOrderable(),
]) ?>>
\ No newline at end of file
diff --git a/panel/views/fields/tags.php b/panel/views/fields/tags.php
index f0c6e601a..0e66a33f6 100644
--- a/panel/views/fields/tags.php
+++ b/panel/views/fields/tags.php
@@ -11,9 +11,9 @@
'required' => $field->isRequired(),
'disabled' => $field->isDisabled(),
'hidden' => $field->isHidden(),
- 'data-limit' => $field->get('limit'),
- 'data-options' => $field->has('options') ? Formwork\Parsers\Json::encode($field->options()) : null,
- 'data-accept' => $field->get('accept', 'options'),
- 'data-orderable' => $field->is('orderable', true),
+ 'data-limit' => $field->limit(),
+ 'data-options' => $field->options() ? Formwork\Parsers\Json::encode($field->options()) : null,
+ 'data-accept' => $field->accept(),
+ 'data-orderable' => $field->isOrderable(),
]) ?>>
\ No newline at end of file