mirror of
https://github.com/getformwork/formwork.git
synced 2025-01-17 13:38:22 +01:00
Merge pull request #596 from getformwork/feature/slug-input
Add slug input
This commit is contained in:
commit
cd8bbc284a
74
formwork/fields/slug.php
Normal file
74
formwork/fields/slug.php
Normal file
@ -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;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
@ -173,6 +173,14 @@ class Field implements Arrayable, Stringable
|
|||||||
return $this->is('visible', false);
|
return $this->is('visible', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the field is readonly
|
||||||
|
*/
|
||||||
|
public function isReadonly(): bool
|
||||||
|
{
|
||||||
|
return $this->is('readonly');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate field value
|
* Validate field value
|
||||||
*/
|
*/
|
||||||
@ -239,7 +247,7 @@ class Field implements Arrayable, Stringable
|
|||||||
*/
|
*/
|
||||||
public function is(string $key, bool $default = false): 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
|
public function get(string $key, mixed $default = null): mixed
|
||||||
|
@ -58,7 +58,7 @@ class Page extends Model implements Stringable
|
|||||||
/**
|
/**
|
||||||
* Ignored field names on frontmatter generation
|
* 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
|
* Ignored field types on frontmatter generation
|
||||||
@ -185,7 +185,7 @@ class Page extends Model implements Stringable
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$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;
|
$this->loaded = true;
|
||||||
}
|
}
|
||||||
@ -460,6 +460,9 @@ class Page extends Model implements Stringable
|
|||||||
if (!$this->validateSlug($slug)) {
|
if (!$this->validateSlug($slug)) {
|
||||||
throw new InvalidArgumentException('Invalid page slug');
|
throw new InvalidArgumentException('Invalid page slug');
|
||||||
}
|
}
|
||||||
|
if ($slug === $this->slug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if ($this->isIndexPage() || $this->isErrorPage()) {
|
if ($this->isIndexPage() || $this->isErrorPage()) {
|
||||||
throw new UnexpectedValueException('Cannot change slug of index or error pages');
|
throw new UnexpectedValueException('Cannot change slug of index or error pages');
|
||||||
}
|
}
|
||||||
@ -600,6 +603,22 @@ class Page extends Model implements Stringable
|
|||||||
return !($this->hasChildren() || $this->isIndexPage() || $this->isErrorPage());
|
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
|
* Return whether the page has loaded
|
||||||
*/
|
*/
|
||||||
@ -683,7 +702,7 @@ class Page extends Model implements Stringable
|
|||||||
$defaults = $this->defaults();
|
$defaults = $this->defaults();
|
||||||
|
|
||||||
$fieldCollection = $this->fields
|
$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();
|
->validate();
|
||||||
|
|
||||||
foreach ($fieldCollection as $field) {
|
foreach ($fieldCollection as $field) {
|
||||||
|
@ -197,8 +197,6 @@ class PagesController extends AbstractController
|
|||||||
|
|
||||||
$this->modal('changes');
|
$this->modal('changes');
|
||||||
|
|
||||||
$this->modal('slug');
|
|
||||||
|
|
||||||
$this->modal('deletePage');
|
$this->modal('deletePage');
|
||||||
|
|
||||||
$this->modal('deleteFile');
|
$this->modal('deleteFile');
|
||||||
|
@ -9,11 +9,13 @@ fields:
|
|||||||
required: true
|
required: true
|
||||||
|
|
||||||
slug:
|
slug:
|
||||||
type: text
|
type: slug
|
||||||
label: '{{panel.pages.newPage.slug}}'
|
label: '{{panel.pages.newPage.slug}}'
|
||||||
suggestion: '{{panel.pages.newPage.slugSuggestion}}'
|
suggestion: '{{panel.pages.newPage.slugSuggestion}}'
|
||||||
required: true
|
required: true
|
||||||
pattern: '^[a-z0-9\-]+$'
|
pattern: '^[a-z0-9\-]+$'
|
||||||
|
source: title
|
||||||
|
root: parent
|
||||||
|
|
||||||
parent:
|
parent:
|
||||||
type: page
|
type: page
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
title: '{{panel.pages.changeSlug}}'
|
|
||||||
|
|
||||||
form: false
|
|
||||||
|
|
||||||
fields:
|
|
||||||
newSlug:
|
|
||||||
type: text
|
|
||||||
label: '{{panel.pages.newPage.slug}}'
|
|
||||||
suggestion: '{{panel.pages.newPage.slugSuggestion}}'
|
|
||||||
required: true
|
|
||||||
|
|
||||||
buttons:
|
|
||||||
dismiss:
|
|
||||||
action: dismiss
|
|
||||||
icon: times-circle
|
|
||||||
label: '{{panel.modal.action.cancel}}'
|
|
||||||
variant: secondary
|
|
||||||
|
|
||||||
continue:
|
|
||||||
action: command
|
|
||||||
icon: check-circle
|
|
||||||
label: '{{panel.modal.action.continue}}'
|
|
||||||
align: right
|
|
||||||
command: continue
|
|
||||||
|
|
||||||
generate:
|
|
||||||
action: command
|
|
||||||
icon: sparks
|
|
||||||
label: '{{panel.pages.changeSlug.generate}}'
|
|
||||||
variant: link
|
|
||||||
align: right
|
|
||||||
command: generate-slug
|
|
@ -16,11 +16,6 @@
|
|||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-route-changeable {
|
|
||||||
padding: $focusring-width;
|
|
||||||
margin: -($focusring-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button .page-language {
|
.button .page-language {
|
||||||
font-size: $font-size-xs;
|
font-size: $font-size-xs;
|
||||||
}
|
}
|
||||||
@ -100,40 +95,6 @@
|
|||||||
white-space: nowrap;
|
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 {
|
.is-dragging .page-title {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -26,11 +26,6 @@
|
|||||||
background-color: var(--color-base-700);
|
background-color: var(--color-base-700);
|
||||||
color: var(--color-base-300);
|
color: var(--color-base-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[readonly] {
|
|
||||||
// Safari Mobile bug
|
|
||||||
@include user-select-none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input[type="checkbox"],
|
.form-input[type="checkbox"],
|
||||||
@ -129,7 +124,7 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input-reset {
|
.form-input-action {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
@ -159,7 +154,7 @@
|
|||||||
padding-left: 1.75rem;
|
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;
|
padding-right: 1.625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import { ImageInput } from "./inputs/image-input";
|
|||||||
import { ImagePicker } from "./inputs/image-picker";
|
import { ImagePicker } from "./inputs/image-picker";
|
||||||
import { RangeInput } from "./inputs/range-input";
|
import { RangeInput } from "./inputs/range-input";
|
||||||
import { SelectInput } from "./inputs/select-input";
|
import { SelectInput } from "./inputs/select-input";
|
||||||
|
import { SlugInput } from "./inputs/slug-input";
|
||||||
import { TagInput } from "./inputs/tag-input";
|
import { TagInput } from "./inputs/tag-input";
|
||||||
|
|
||||||
export class Inputs {
|
export class Inputs {
|
||||||
@ -35,7 +36,9 @@ export class Inputs {
|
|||||||
|
|
||||||
$$("select:not([hidden])", parent).forEach((element: HTMLSelectElement) => (this[element.name] = new SelectInput(element, app.config.SelectInput)));
|
$$("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;
|
const targetId = element.dataset.reset;
|
||||||
if (targetId) {
|
if (targetId) {
|
||||||
element.addEventListener("click", () => {
|
element.addEventListener("click", () => {
|
||||||
|
28
panel/src/ts/components/inputs/slug-input.ts
Normal file
28
panel/src/ts/components/inputs/slug-input.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { $, $$ } from "../../utils/selectors";
|
import { $, $$ } from "../../utils/selectors";
|
||||||
import { escapeRegExp, makeDiacriticsRegExp, makeSlug, validateSlug } from "../../utils/validation";
|
import { escapeRegExp, makeDiacriticsRegExp } from "../../utils/validation";
|
||||||
import { app } from "../../app";
|
import { app } from "../../app";
|
||||||
import { debounce } from "../../utils/events";
|
import { debounce } from "../../utils/events";
|
||||||
import { Notification } from "../notification";
|
import { Notification } from "../notification";
|
||||||
@ -12,12 +12,10 @@ export class Pages {
|
|||||||
const commandCollapseAllPages = $("[data-command=collapse-all-pages]") as HTMLButtonElement;
|
const commandCollapseAllPages = $("[data-command=collapse-all-pages]") as HTMLButtonElement;
|
||||||
const commandReorderPages = $("[data-command=reorder-pages]") as HTMLButtonElement;
|
const commandReorderPages = $("[data-command=reorder-pages]") as HTMLButtonElement;
|
||||||
const commandPreview = $("[data-command=preview]") as HTMLButtonElement;
|
const commandPreview = $("[data-command=preview]") as HTMLButtonElement;
|
||||||
const commandChangeSlug = $("[data-command=change-slug]") as HTMLButtonElement;
|
|
||||||
|
|
||||||
const searchInput = $(".page-search");
|
const searchInput = $(".page-search");
|
||||||
|
|
||||||
const newPageModal = app.modals["newPageModal"];
|
const newPageModal = app.modals["newPageModal"];
|
||||||
const slugModal = app.modals["slugModal"];
|
|
||||||
|
|
||||||
$$(".pages-tree").forEach((element) => {
|
$$(".pages-tree").forEach((element) => {
|
||||||
if (element.dataset.orderableChildren === "true") {
|
if (element.dataset.orderableChildren === "true") {
|
||||||
@ -122,22 +120,8 @@ export class Pages {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newPageModal) {
|
if (newPageModal) {
|
||||||
const titleInput = $('[id="newPageModal.title"]') as HTMLInputElement;
|
|
||||||
const slugInput = $('[id="newPageModal.slug"]') as HTMLInputElement;
|
|
||||||
const parentSelect = $('[id="newPageModal.parent"]') as HTMLInputElement;
|
const parentSelect = $('[id="newPageModal.parent"]') as HTMLInputElement;
|
||||||
|
|
||||||
titleInput.addEventListener("keyup", (event) => {
|
|
||||||
($('[id="newPageModal.slug"]') as HTMLInputElement).value = makeSlug((event.target as HTMLInputElement).value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSlugChange = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
target.value = validateSlug(target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
slugInput.addEventListener("keyup", handleSlugChange);
|
|
||||||
slugInput.addEventListener("blur", handleSlugChange);
|
|
||||||
|
|
||||||
parentSelect.addEventListener("change", () => {
|
parentSelect.addEventListener("change", () => {
|
||||||
const option = $('.dropdown-list[data-for="newPageModal.parent"] .selected');
|
const option = $('.dropdown-list[data-for="newPageModal.parent"] .selected');
|
||||||
|
|
||||||
@ -196,56 +180,6 @@ export class Pages {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slugModal && commandChangeSlug) {
|
|
||||||
const newSlugInput = $('[id="slugModal.newSlug"]') as HTMLInputElement;
|
|
||||||
const commandGenerateSlug = $("[data-command=generate-slug]", slugModal.element) as HTMLElement;
|
|
||||||
const commandContinue = $("[data-command=continue]", slugModal.element) as HTMLElement;
|
|
||||||
|
|
||||||
commandChangeSlug.addEventListener("click", () => {
|
|
||||||
app.modals["slugModal"].show(undefined, (modal) => {
|
|
||||||
const slug = (document.getElementById("slug") as HTMLInputElement).value;
|
|
||||||
const slugInput = $('[id="slugModal.newSlug"]', modal.element) as HTMLInputElement;
|
|
||||||
slugInput.value = slug;
|
|
||||||
slugInput.placeholder = slug;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSlugChange = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
target.value = validateSlug(target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
newSlugInput.addEventListener("keyup", handleSlugChange);
|
|
||||||
newSlugInput.addEventListener("blur", handleSlugChange);
|
|
||||||
|
|
||||||
commandGenerateSlug.addEventListener("click", () => {
|
|
||||||
const slug = makeSlug((document.getElementById("title") as HTMLInputElement).value);
|
|
||||||
newSlugInput.value = slug;
|
|
||||||
newSlugInput.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeSlug = () => {
|
|
||||||
const slug = newSlugInput.value.replace(/^-+|-+$/, "");
|
|
||||||
|
|
||||||
if (slug.length > 0) {
|
|
||||||
const route = ($(".page-route-inner") as HTMLElement).innerHTML;
|
|
||||||
newSlugInput.value = slug;
|
|
||||||
($("#slug") as HTMLInputElement).value = slug;
|
|
||||||
($(".page-route-inner") as HTMLElement).innerHTML = route.replace(/\/[a-z0-9-]+\/$/, `/${slug}/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.modals["slugModal"].hide();
|
|
||||||
};
|
|
||||||
|
|
||||||
commandContinue.addEventListener("click", changeSlug);
|
|
||||||
|
|
||||||
newSlugInput.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
changeSlug();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$$("[data-modal=renameFileModal]").forEach((element) => {
|
$$("[data-modal=renameFileModal]").forEach((element) => {
|
||||||
element.addEventListener("click", () => {
|
element.addEventListener("click", () => {
|
||||||
const input = $('[id="renameFileModal.filename"]') as HTMLInputElement;
|
const input = $('[id="renameFileModal.filename"]') as HTMLInputElement;
|
||||||
|
@ -12,5 +12,5 @@
|
|||||||
'disabled' => $field->isDisabled(),
|
'disabled' => $field->isDisabled(),
|
||||||
'hidden' => $field->isHidden(),
|
'hidden' => $field->isHidden(),
|
||||||
]) ?>>
|
]) ?>>
|
||||||
<span class="form-input-reset" data-reset="<?= $field->name() ?>"><?= $this->icon('times-circle') ?></span>
|
<span class="form-input-action" data-reset="<?= $field->name() ?>"><?= $this->icon('times-circle') ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,5 +13,5 @@
|
|||||||
'disabled' => $field->isDisabled(),
|
'disabled' => $field->isDisabled(),
|
||||||
'hidden' => $field->isHidden(),
|
'hidden' => $field->isHidden(),
|
||||||
]) ?>>
|
]) ?>>
|
||||||
<span class="form-input-reset" data-reset="<?= $field->name() ?>"><?= $this->icon('times-circle') ?></span>
|
<span class="form-input-action" data-reset="<?= $field->name() ?>"><?= $this->icon('times-circle') ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
23
panel/views/fields/slug.php
Normal file
23
panel/views/fields/slug.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php $this->layout('fields.field') ?>
|
||||||
|
<div class="form-input-wrap">
|
||||||
|
<input <?= $this->attr([
|
||||||
|
'class' => ['form-input', 'form-input-slug', $field->get('class')],
|
||||||
|
'type' => 'text',
|
||||||
|
'id' => $field->name(),
|
||||||
|
'name' => $field->formName(),
|
||||||
|
'value' => $field->value(),
|
||||||
|
'placeholder' => $field->placeholder(),
|
||||||
|
'minlength' => $field->get('min'),
|
||||||
|
'maxlength' => $field->get('max'),
|
||||||
|
'pattern' => $field->get('pattern'),
|
||||||
|
'required' => $field->isRequired(),
|
||||||
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden(),
|
||||||
|
'readonly' => $field->isReadonly(),
|
||||||
|
'data-source' => $field->source()?->name(),
|
||||||
|
'data-auto-update' => $field->autoUpdate() ? 'true' : 'false',
|
||||||
|
]) ?>>
|
||||||
|
<?php if (!$field->autoUpdate() && !$field->isReadonly()): ?>
|
||||||
|
<span class="form-input-action" data-generate-slug="<?= $field->name() ?>" title="<?= $this->translate('panel.pages.changeSlug.generate') ?>"><?= $this->icon('sparks') ?></span>
|
||||||
|
<?php endif ?>
|
||||||
|
</div>
|
@ -13,16 +13,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div><?= $this->insert('_pages/status', ['page' => $page]) ?></div>
|
<div><?= $this->insert('_pages/status', ['page' => $page]) ?></div>
|
||||||
<?php if (!$page->isIndexPage() && !$page->isErrorPage()) : ?>
|
<div class="page-route"><?= $page->route() ?></div>
|
||||||
<div class="page-route page-route-changeable min-w-0">
|
|
||||||
<button type="button" class="button page-slug-change truncate max-w-100" data-command="change-slug" title="<?= $this->translate('panel.pages.changeSlug') ?>"><span class="page-route-inner"><?= $page->route() ?></span> <?= $this->icon('pencil') ?></button>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<div class="page-route"><?= $page->route() ?></div>
|
|
||||||
<?php endif ?>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="slug" name="slug" value="<?= $page->slug() ?>">
|
|
||||||
<?php if ($currentLanguage) : ?>
|
<?php if ($currentLanguage) : ?>
|
||||||
<input type="hidden" id="language" name="language" value="<?= $currentLanguage ?>">
|
<input type="hidden" id="language" name="language" value="<?= $currentLanguage ?>">
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
@ -18,6 +18,15 @@ fields:
|
|||||||
label: '{{page.status.published}}'
|
label: '{{page.status.published}}'
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
|
slug:
|
||||||
|
type: slug
|
||||||
|
label: '{{page.slug}}'
|
||||||
|
suggestion: '{{panel.pages.newPage.slugSuggestion}}'
|
||||||
|
required: true
|
||||||
|
readonly@: page.isSlugReadonly
|
||||||
|
source: title
|
||||||
|
autoUpdate: false
|
||||||
|
|
||||||
parent:
|
parent:
|
||||||
type: page
|
type: page
|
||||||
label: '{{page.parent}}'
|
label: '{{page.parent}}'
|
||||||
|
@ -22,7 +22,7 @@ layout:
|
|||||||
collapsible: true
|
collapsible: true
|
||||||
collapsed: true
|
collapsed: true
|
||||||
label: '{{page.attributes}}'
|
label: '{{page.attributes}}'
|
||||||
fields: [parent, template]
|
fields: [slug, parent, template]
|
||||||
|
|
||||||
files:
|
files:
|
||||||
collapsible: true
|
collapsible: true
|
||||||
@ -76,6 +76,15 @@ fields:
|
|||||||
label: '{{page.cacheable}}'
|
label: '{{page.cacheable}}'
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
|
slug:
|
||||||
|
type: slug
|
||||||
|
label: '{{page.slug}}'
|
||||||
|
suggestion: '{{panel.pages.newPage.slugSuggestion}}'
|
||||||
|
required: true
|
||||||
|
readonly@: page.isSlugReadonly
|
||||||
|
source: title
|
||||||
|
autoUpdate: false
|
||||||
|
|
||||||
parent:
|
parent:
|
||||||
type: page
|
type: page
|
||||||
label: '{{page.parent}}'
|
label: '{{page.parent}}'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user