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') ?>
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(), +
\ 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') ?> +
+ icon($field->get('icon', 'file')) ?> + +
\ 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') ?> +
+ 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') ?> +
+ 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 @@
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 @@
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 @@
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