Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add slug input #596

Merged
merged 4 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions formwork/fields/slug.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

use Formwork\App;
use Formwork\Fields\Exceptions\ValidationException;
use Formwork\Fields\Field;

use Formwork\Utils\Constraint;

return function (App $app) {
return [
'validate' => function (Field $field, $value): string {
if (Constraint::isEmpty($value)) {
return '';
}

if (!is_string($value) && !is_numeric($value)) {
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
}

if ($field->has('min') && strlen((string) $value) < $field->get('min')) {
throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('min')));
}

if ($field->has('max') && strlen((string) $value) > $field->get('max')) {
throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('max')));
}

if ($field->has('pattern') && !Constraint::matchesRegex((string) $value, $field->get('pattern'))) {
throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not match the required pattern', $field->name(), $field->value()));
}

if (!$field->hasUniqueValue()) {
throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be unique', $field->name(), $field->value()));
}

return (string) $value;
},

'source' => function (Field $field): ?Field {
if (($source = $field->get('source')) === null) {
return null;
}
return $field->parent()?->get($source);
},

'autoUpdate' => function (Field $field): bool {
return $field->is('autoUpdate', true);
},

'hasUniqueValue' => function (Field $field): bool {
$root = $field->get('root');

if ($root === null) {
return true;
}

$parentField = $field->parent()?->get($root);

if ($parentField === null || $parentField->type() !== 'page') {
throw new ValidationException(sprintf('Invalid parent reference for field "%s" of type "%s"', $field->name(), $field->type()));
}

$children = $parentField->return()->children();

foreach ($children as $child) {
if ($child->slug() === $field->value()) {
return false;
}
}

return true;
},
];
};
10 changes: 9 additions & 1 deletion formwork/src/Fields/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ public function isHidden(): bool
return $this->is('visible', false);
}

/**
* Return whether the field is readonly
*/
public function isReadonly(): bool
{
return $this->is('readonly');
}

/**
* Validate field value
*/
Expand Down Expand Up @@ -239,7 +247,7 @@ public function isValidated(): bool
*/
public function is(string $key, bool $default = false): bool
{
return $this->baseGet($key, $default) === true;
return $this->get($key, $default) === true;
}

public function get(string $key, mixed $default = null): mixed
Expand Down
25 changes: 22 additions & 3 deletions formwork/src/Pages/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class Page extends Model implements Stringable
/**
* Ignored field names on frontmatter generation
*/
protected const IGNORED_FIELD_NAMES = ['content', 'template', 'parent'];
protected const IGNORED_FIELD_NAMES = ['content', 'slug', 'template', 'parent'];

/**
* Ignored field types on frontmatter generation
Expand Down Expand Up @@ -185,7 +185,7 @@ public function __construct(array $data = [])
];
}

$this->fields->setValues([...$this->data, 'parent' => $this->parent()?->route(), 'template' => $this->template]);
$this->fields->setValues([...$this->data, 'slug' => $this->slug, 'parent' => $this->parent()?->route(), 'template' => $this->template]);

$this->loaded = true;
}
Expand Down Expand Up @@ -460,6 +460,9 @@ public function setSlug(string $slug): void
if (!$this->validateSlug($slug)) {
throw new InvalidArgumentException('Invalid page slug');
}
if ($slug === $this->slug) {
return;
}
if ($this->isIndexPage() || $this->isErrorPage()) {
throw new UnexpectedValueException('Cannot change slug of index or error pages');
}
Expand Down Expand Up @@ -600,6 +603,22 @@ public function isDeletable(): bool
return !($this->hasChildren() || $this->isIndexPage() || $this->isErrorPage());
}

/**
* Return whether the slug is editable
*/
public function isSlugEditable(): bool
{
return !$this->isIndexPage() && !$this->isErrorPage();
}

/**
* Return whether the slug is readonly
*/
public function isSlugReadonly(): bool
{
return !$this->isSlugEditable();
}

/**
* Return whether the page has loaded
*/
Expand Down Expand Up @@ -683,7 +702,7 @@ public function save(?string $language = null): void
$defaults = $this->defaults();

$fieldCollection = $this->fields
->setValues([...$this->data, 'parent' => $this->parent()->route(), 'template' => $this->template])
->setValues([...$this->data, 'slug' => $this->slug, 'parent' => $this->parent()->route(), 'template' => $this->template])
->validate();

foreach ($fieldCollection as $field) {
Expand Down
2 changes: 0 additions & 2 deletions formwork/src/Panel/Controllers/PagesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,6 @@ public function edit(RouteParams $routeParams): Response

$this->modal('changes');

$this->modal('slug');

$this->modal('deletePage');

$this->modal('deleteFile');
Expand Down
4 changes: 3 additions & 1 deletion panel/modals/newPage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ fields:
required: true

slug:
type: text
type: slug
label: '{{panel.pages.newPage.slug}}'
suggestion: '{{panel.pages.newPage.slugSuggestion}}'
required: true
pattern: '^[a-z0-9\-]+$'
source: title
root: parent

parent:
type: page
Expand Down
32 changes: 0 additions & 32 deletions panel/modals/slug.yaml

This file was deleted.

39 changes: 0 additions & 39 deletions panel/src/scss/components/_pages.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@
font-size: $font-size-sm;
}

.page-route-changeable {
padding: $focusring-width;
margin: -($focusring-width);
}

.button .page-language {
font-size: $font-size-xs;
}
Expand Down Expand Up @@ -100,40 +95,6 @@
white-space: nowrap;
}

.page-slug-change {
padding: 0;
border-color: transparent;
margin: 0;
background-color: transparent;
box-shadow: none;
color: var(--color-base-300);
cursor: pointer;

&:hover,
&:focus {
border-color: transparent;
background-color: transparent;
color: var(--color-base-300);
}

&:focus {
@include focusring;
}

& .icon {
display: inline-block;
margin-right: 0;
color: var(--color-base-100);
opacity: 0;
transition: opacity $transition-time-sm;
}

&:hover .icon,
&:focus .icon {
opacity: 1;
}
}

.is-dragging .page-title {
pointer-events: none;
}
Expand Down
9 changes: 2 additions & 7 deletions panel/src/scss/components/forms/_forms-base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@
background-color: var(--color-base-700);
color: var(--color-base-300);
}

&[readonly] {
// Safari Mobile bug
@include user-select-none;
}
}

.form-input[type="checkbox"],
Expand Down Expand Up @@ -129,7 +124,7 @@
margin-bottom: 0;
}

.form-input-reset {
.form-input-action {
position: absolute;
top: 50%;
right: 0.5rem;
Expand Down Expand Up @@ -159,7 +154,7 @@
padding-left: 1.75rem;
}

.form-input-wrap .form-input:has(+ .form-input-reset) {
.form-input-wrap .form-input:has(+ .form-input-action) {
padding-right: 1.625rem;
}

Expand Down
5 changes: 4 additions & 1 deletion panel/src/ts/components/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ImageInput } from "./inputs/image-input";
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";

export class Inputs {
Expand All @@ -35,7 +36,9 @@ export class Inputs {

$$("select:not([hidden])", parent).forEach((element: HTMLSelectElement) => (this[element.name] = new SelectInput(element, app.config.SelectInput)));

$$(".form-input-reset", parent).forEach((element) => {
$$(".form-input-slug", parent).forEach((element: HTMLInputElement) => (this[element.name] = new SlugInput(element)));

$$(".form-input-action[data-reset]", parent).forEach((element) => {
const targetId = element.dataset.reset;
if (targetId) {
element.addEventListener("click", () => {
Expand Down
28 changes: 28 additions & 0 deletions panel/src/ts/components/inputs/slug-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { makeSlug, validateSlug } from "../../utils/validation";
import { $ } from "../../utils/selectors";

export class SlugInput {
constructor(element: HTMLInputElement) {
const source = $(`[id="${element.dataset.source}"]`) as HTMLInputElement | null;
const autoUpdate = "autoUpdate" in element.dataset && element.dataset.autoUpdate === "true";

if (source) {
if (autoUpdate) {
source.addEventListener("input", () => (element.value = makeSlug(source.value)));
} else {
const generateButton = $(`[data-generate-slug="${element.id}"]`) as HTMLButtonElement | null;
if (generateButton) {
generateButton.addEventListener("click", () => (element.value = makeSlug(source.value)));
}
}
}

const handleSlugChange = (event: Event) => {
const target = event.target as HTMLInputElement;
target.value = validateSlug(target.value);
};

element.addEventListener("keyup", handleSlugChange);
element.addEventListener("blur", handleSlugChange);
}
}
Loading