mirror of
https://github.com/getformwork/formwork.git
synced 2025-01-17 21:49:04 +01:00
Rewrite pages and fields
This commit is contained in:
parent
b61bdfeb6d
commit
23e8d19d74
@ -1,120 +1,57 @@
|
|||||||
title: User
|
title: User
|
||||||
|
|
||||||
|
layout:
|
||||||
|
type: sections
|
||||||
|
sections:
|
||||||
|
user:
|
||||||
|
label: '{{admin.users.user}}'
|
||||||
|
fields: [fullname, email, password, language, role, color-scheme, avatar]
|
||||||
|
|
||||||
fields:
|
fields:
|
||||||
rows1:
|
fullname:
|
||||||
type: rows
|
type: text
|
||||||
fields:
|
label: '{{admin.user.fullname}}'
|
||||||
row1:
|
required: true
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.user.fullname}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
fullname:
|
|
||||||
type: text
|
|
||||||
required: true
|
|
||||||
|
|
||||||
row2:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.user.email}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
email:
|
|
||||||
type: email
|
|
||||||
required: true
|
|
||||||
|
|
||||||
row3:
|
email:
|
||||||
type: row
|
type: email
|
||||||
fields:
|
label: '{{admin.user.email}}'
|
||||||
column1:
|
required: true
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.user.password}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
password:
|
|
||||||
type: password
|
|
||||||
placeholder: '{{admin.user.password.type-new-password}}'
|
|
||||||
disabled: true
|
|
||||||
pattern: '^.{8,}$'
|
|
||||||
autocomplete: new-password
|
|
||||||
|
|
||||||
row4:
|
password:
|
||||||
type: row
|
type: password
|
||||||
fields:
|
label: '{{admin.user.password}}'
|
||||||
column1:
|
placeholder: '{{admin.user.password.type-new-password}}'
|
||||||
type: column
|
disabled: true
|
||||||
width: 1-3
|
pattern: '^.{8,}$'
|
||||||
label: '{{admin.user.language}}'
|
autocomplete: new-password
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
language:
|
|
||||||
type: select
|
|
||||||
required: true
|
|
||||||
translate: false
|
|
||||||
import:
|
|
||||||
options: 'Formwork\Admin\Admin::availableTranslations'
|
|
||||||
|
|
||||||
row5:
|
language:
|
||||||
type: row
|
type: select
|
||||||
fields:
|
label: '{{admin.user.language}}'
|
||||||
column1:
|
required: true
|
||||||
type: column
|
translate: [label]
|
||||||
width: 1-3
|
import:
|
||||||
label: '{{admin.user.role}}'
|
options: 'Formwork\Admin\Admin::availableTranslations'
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
role:
|
|
||||||
type: select
|
|
||||||
disabled: true
|
|
||||||
import:
|
|
||||||
options: 'Formwork\Admin\Users\Users::availableRoles'
|
|
||||||
|
|
||||||
row6:
|
role:
|
||||||
type: row
|
type: select
|
||||||
fields:
|
label: '{{admin.user.role}}'
|
||||||
column1:
|
disabled: true
|
||||||
type: column
|
import:
|
||||||
width: 1-3
|
options: 'Formwork\Admin\Users\Users::availableRoles'
|
||||||
label: '{{admin.user.color-scheme}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
color-scheme:
|
|
||||||
type: togglegroup
|
|
||||||
options:
|
|
||||||
light: '{{admin.user.color-scheme.light}}'
|
|
||||||
dark: '{{admin.user.color-scheme.dark}}'
|
|
||||||
auto: '{{admin.user.color-scheme.auto}}'
|
|
||||||
|
|
||||||
row7:
|
|
||||||
type: row
|
color-scheme:
|
||||||
fields:
|
type: togglegroup
|
||||||
column1:
|
label: '{{admin.user.color-scheme}}'
|
||||||
type: column
|
options:
|
||||||
width: 1-3
|
light: '{{admin.user.color-scheme.light}}'
|
||||||
label: '{{admin.user.avatar}}'
|
dark: '{{admin.user.color-scheme.dark}}'
|
||||||
column2:
|
auto: '{{admin.user.color-scheme.auto}}'
|
||||||
type: column
|
|
||||||
width: 2-3
|
avatar:
|
||||||
fields:
|
type: file
|
||||||
avatar:
|
label: '{{admin.user.avatar}}'
|
||||||
type: file
|
accept: .jpg, .jpeg, .png, .gif
|
||||||
accept: .jpg, .jpeg, .png, .gif
|
|
||||||
|
@ -176,7 +176,7 @@ admin.pages.page.status: Status
|
|||||||
admin.pages.page.tags: Tags
|
admin.pages.page.tags: Tags
|
||||||
admin.pages.page.title: Title
|
admin.pages.page.title: Title
|
||||||
admin.pages.page.unpublish-date: Unpublish Date
|
admin.pages.page.unpublish-date: Unpublish Date
|
||||||
admin.pages.page.visible: Visible in the menu
|
admin.pages.page.listed: Visible in the menu
|
||||||
admin.pages.pages: Pages
|
admin.pages.pages: Pages
|
||||||
admin.pages.pages.collapse-all: Collapse All
|
admin.pages.pages.collapse-all: Collapse All
|
||||||
admin.pages.pages.expand-all: Expand All
|
admin.pages.pages.expand-all: Expand All
|
||||||
@ -190,7 +190,9 @@ admin.pages.status.not-published: Not Published
|
|||||||
admin.pages.status.not-routable: Not Routable
|
admin.pages.status.not-routable: Not Routable
|
||||||
admin.pages.status.published: Published
|
admin.pages.status.published: Published
|
||||||
admin.pages.status.routable: Routable
|
admin.pages.status.routable: Routable
|
||||||
|
admin.pages.summary: Summary
|
||||||
admin.pages.template: Template
|
admin.pages.template: Template
|
||||||
|
admin.pages.text: Text
|
||||||
admin.pages.toggle-children: Toggle Children Pages
|
admin.pages.toggle-children: Toggle Children Pages
|
||||||
admin.panel: Administration Panel
|
admin.panel: Administration Panel
|
||||||
admin.register.create-user: Formwork Admin is installed but no users were found. Please register a user now.
|
admin.register.create-user: Formwork Admin is installed but no users were found. Please register a user now.
|
||||||
|
@ -1,5 +1 @@
|
|||||||
<?php foreach ($fields as $field): ?>
|
<?php $this->insert('fields.layout.' . $fields->layout()->type(), ['sections' => $fields->layout()->sections()]) ?>
|
||||||
<?php if ($field->isVisible()): ?>
|
|
||||||
<?php $this->insert('fields.' . $field->type(), ['field' => $field]) ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<div <?= $this->attr([
|
<div <?= $this->attr([
|
||||||
'class' => ['input-array', $field->get('associative') ? 'input-array-associative' : ''],
|
'class' => ['input-array', $field->get('associative') ? 'input-array-associative' : ''],
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
|
'hidden' => $field->isHidden(),
|
||||||
'data-name' => $field->formName()
|
'data-name' => $field->formName()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
<?php foreach ($field->value() ?: ['' => ''] as $key => $value): ?>
|
<?php foreach ($field->value() ?: ['' => ''] as $key => $value): ?>
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="input-checkbox-label">
|
<label class="input-checkbox-label">
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'class' => 'input-checkbox',
|
'class' => 'input-checkbox',
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
'name' => $field->formName(),
|
'name' => $field->formName(),
|
||||||
'checked' => $field->value() == true,
|
'checked' => $field->value() == true,
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled()
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
<span class="input-checkbox-text"><?= $field->label() ?></span>
|
<span class="input-checkbox-text"><?= $field->label() ?></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<?= $this->insert('fields.partials.description') ?>
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
<div class="col-m-<?= $field->get('width') ?>">
|
|
||||||
<?php if ($field->has('label')): ?>
|
|
||||||
<?= $field->label() ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php $this->insert('fields', ['fields' => $field->get('fields')]) ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<div class="input-wrap">
|
<div class="input-wrap">
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
@ -8,7 +8,8 @@
|
|||||||
'value' => $field->value(),
|
'value' => $field->value(),
|
||||||
'placeholder' => $field->placeholder(),
|
'placeholder' => $field->placeholder(),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled()
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
<span class="input-reset" data-reset="<?= $field->name() ?>"><?= $this->icon('times-circle') ?></span>
|
<span class="input-reset" data-reset="<?= $field->name() ?>"><?= $this->icon('times-circle') ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
@ -9,6 +9,7 @@
|
|||||||
'value' => $field->value(),
|
'value' => $field->value(),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled(),
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden(),
|
||||||
'data-field' => 'duration',
|
'data-field' => 'duration',
|
||||||
'data-unit' => $field->get('unit', 'seconds'),
|
'data-unit' => $field->get('unit', 'seconds'),
|
||||||
'data-intervals' => $field->has('intervals') ? implode(', ', $field->get('intervals')) : null
|
'data-intervals' => $field->has('intervals') ? implode(', ', $field->get('intervals')) : null
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'type' => 'email',
|
'type' => 'email',
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
@ -9,5 +9,7 @@
|
|||||||
'maxlength' => $field->get('max'),
|
'maxlength' => $field->get('max'),
|
||||||
'pattern' => $field->get('pattern'),
|
'pattern' => $field->get('pattern'),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled()
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'type' => 'file',
|
'type' => 'file',
|
||||||
'class' => 'input-file',
|
'class' => 'input-file',
|
||||||
@ -6,7 +6,10 @@
|
|||||||
'name' => $field->formName() . '[]',
|
'name' => $field->formName() . '[]',
|
||||||
'accept' => $field->get('accept', implode(', ', $formwork->config()->get('files.allowed_extensions'))),
|
'accept' => $field->get('accept', implode(', ', $formwork->config()->get('files.allowed_extensions'))),
|
||||||
'data-auto-upload' => $field->get('auto-upload') ? 'true' : 'false',
|
'data-auto-upload' => $field->get('auto-upload') ? 'true' : 'false',
|
||||||
'multiple' => $field->get('multiple') ? true : false
|
'multiple' => $field->get('multiple'),
|
||||||
|
'required' => $field->isRequired(),
|
||||||
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
<label for="<?= $field->name() ?>" class="input-file-label">
|
<label for="<?= $field->name() ?>" class="input-file-label">
|
||||||
<span><?= $this->translate('fields.file.upload-label') ?></span>
|
<span><?= $this->translate('fields.file.upload-label') ?></span>
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<div class="section-header"><?= $field->label() ?></div>
|
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<div class="input-wrap">
|
<div class="input-wrap">
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
@ -7,7 +7,10 @@
|
|||||||
'name' => $field->formName(),
|
'name' => $field->formName(),
|
||||||
'value' => basename($field->value() ?? ''),
|
'value' => basename($field->value() ?? ''),
|
||||||
'placeholder' => $field->placeholder(),
|
'placeholder' => $field->placeholder(),
|
||||||
'readonly' => true
|
'readonly' => true,
|
||||||
|
'required' => $field->isRequired(),
|
||||||
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
<span class="input-reset" data-reset="<?= $field->name() ?>"><?= $this->icon('times-circle') ?></span>
|
<span class="input-reset" data-reset="<?= $field->name() ?>"><?= $this->icon('times-circle') ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
5
admin/views/fields/layout/default.php
Normal file
5
admin/views/fields/layout/default.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php foreach ($fields as $field): ?>
|
||||||
|
<?php if ($field->isVisible()): ?>
|
||||||
|
<?php $this->insert('fields.' . $field->type(), ['field' => $field]) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
@ -1,23 +1,32 @@
|
|||||||
<div class="sections">
|
<div class="sections">
|
||||||
<?php
|
<?php
|
||||||
foreach ($field->get('fields') as $section):
|
foreach ($sections as $section):
|
||||||
?>
|
?>
|
||||||
<div <?= $this->attr(['class' => ['section', $section->is('collapsible') ? 'collapsible' : '', $section->is('collapsed') ? 'collapsed' : '']]) ?>>
|
<div <?= $this->attr(['class' => ['section', $section->is('collapsible') ? 'collapsible' : '', $section->is('collapsed') ? 'collapsed' : '']]) ?>>
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<?php
|
<?php
|
||||||
if ($section->is('collapsible')):
|
if ($section->is('collapsible')):
|
||||||
?>
|
?>
|
||||||
<span class="section-toggle"><?= $this->icon('chevron-up') ?></span>
|
<span class="section-toggle"><?= $this->icon('chevron-up') ?></span>
|
||||||
<?php
|
<?php
|
||||||
endif;
|
endif;
|
||||||
?>
|
?>
|
||||||
<?= $section->label() ?>
|
<?= $section->label() ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content" style="padding: 0 .5rem;">
|
<div class="section-content" style="padding: 0 .5rem;">
|
||||||
<?php $this->insert('fields', ['fields' => $section->get('fields')]) ?>
|
<?php
|
||||||
|
foreach ($fields->getMultiple($section->get('fields', [])) as $field):
|
||||||
|
?>
|
||||||
|
<?php if ($field->isVisible()): ?>
|
||||||
|
<?php $this->insert('fields.' . $field->type(), ['field' => $field]) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
endforeach;
|
endforeach;
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<div class="editor-wrap">
|
<div class="editor-wrap">
|
||||||
<div class="editor-toolbar" data-for="<?= $field->name() ?>">
|
<div class="editor-toolbar" data-for="<?= $field->name() ?>">
|
||||||
<button type="button" class="toolbar-button" data-command="bold" title="<?= $this->translate('admin.pages.editor.bold') ?>"><?= $this->icon('bold') ?></button>
|
<button type="button" class="toolbar-button" data-command="bold" title="<?= $this->translate('admin.pages.editor.bold') ?>"><?= $this->icon('bold') ?></button>
|
||||||
@ -18,9 +18,15 @@
|
|||||||
'class' => 'editor-textarea',
|
'class' => 'editor-textarea',
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
'name' => $field->formName(),
|
'name' => $field->formName(),
|
||||||
|
'value' => $field->value(),
|
||||||
'placeholder' => $field->placeholder(),
|
'placeholder' => $field->placeholder(),
|
||||||
|
'minlength' => $field->get('min'),
|
||||||
|
'maxlength' => $field->get('max'),
|
||||||
|
'autocomplete' => $field->get('autocomplete', 'off'),
|
||||||
|
'rows' => $field->get('rows'),
|
||||||
|
'cols' => $field->get('cols'),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled(),
|
'disabled' => $field->isDisabled(),
|
||||||
'autocomplete' => 'off'
|
'hidden' => $field->isHidden()
|
||||||
]) ?>><?= $this->escape($field->value() ?? '') ?></textarea>
|
]) ?>><?= $this->escape($field->value() ?? '') ?></textarea>
|
||||||
</div>
|
</div>
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
@ -9,5 +9,6 @@
|
|||||||
'value' => $field->value(),
|
'value' => $field->value(),
|
||||||
'placeholder' => $field->placeholder(),
|
'placeholder' => $field->placeholder(),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled()
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
<ul class="files-list">
|
<ul class="files-list">
|
||||||
<?php
|
<?php
|
||||||
foreach ($page->files() as $file):
|
foreach ($page->files() as $file):
|
||||||
?>
|
?>
|
||||||
<li>
|
<li>
|
||||||
<div class="files-item">
|
<div class="files-item">
|
||||||
<?= $this->icon(is_null($file->type()) ? 'file' : 'file-' . $file->type()) ?> <div class="files-item-cell file-name" data-overflow-tooltip="true"><?= $file->name() ?> <span class="file-size">(<?= $file->size() ?>)</span></div>
|
<?= $this->icon(is_null($file->type()) ? 'file' : 'file-' . $file->type()) ?> <div class="files-item-cell file-name" data-overflow-tooltip="true"><?= $file->name() ?> <span class="file-size">(<?= $file->size() ?>)</span></div>
|
||||||
<div class="files-item-cell file-actions">
|
<div class="files-item-cell file-actions">
|
||||||
<a class="button button-link" role="button" href="<?= $admin->pageUri($page) . $file->name() ?>" target="formwork-preview-file-<?= $file->hash() ?>" title="<?= $this->translate('admin.pages.preview-file') ?>" aria-label="title="<?= $this->translate('admin.pages.preview-file') ?>""><?= $this->icon('eye') ?></a>
|
<a class="button button-link" role="button" href="<?= $admin->pageUri($page) . $file->name() ?>" target="formwork-preview-file-<?= $file->hash() ?>" title="<?= $this->translate('admin.pages.preview-file') ?>" aria-label="title="<?= $this->translate('admin.pages.preview-file') ?>""><?= $this->icon('eye') ?></a>
|
||||||
<?php
|
<?php
|
||||||
if ($admin->user()->permissions()->has('pages.delete_files')):
|
if ($admin->user()->permissions()->has('pages.delete_files')):
|
||||||
?>
|
?>
|
||||||
<button type="button" class="button-link" data-modal="deleteFileModal" data-modal-action="<?= $admin->uri('/pages/' . trim($page->route(), '/') . '/file/' . $file->name() . '/delete/') ?>" title="<?= $this->translate('admin.pages.delete-file') ?>" aria-label="<?= $this->translate('admin.pages.delete-file') ?>">
|
<button type="button" class="button-link" data-modal="deleteFileModal" data-modal-action="<?= $admin->uri('/pages/' . trim($page->route(), '/') . '/file/' . $file->name() . '/delete/') ?>" title="<?= $this->translate('admin.pages.delete-file') ?>" aria-label="<?= $this->translate('admin.pages.delete-file') ?>">
|
||||||
<?= $this->icon('trash') ?>
|
<?= $this->icon('trash') ?>
|
||||||
</button>
|
</button>
|
||||||
<?php
|
<?php
|
||||||
endif;
|
endif;
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
@ -1,12 +1,16 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<select id="page-parent" name="parent">
|
<select id="page-parent" name="parent">
|
||||||
<option value="." <?php if ($page->parent()->isSite()): ?> selected<?php endif; ?>><?= $this->translate('admin.pages.new-page.site') ?> (/)</option>
|
<option value="." <?php if ($page->parent()->isSite()): ?> selected<?php endif; ?>><?= $this->translate('admin.pages.new-page.site') ?> (/)</option>
|
||||||
<?php
|
<?php
|
||||||
foreach ($parents as $parent):
|
foreach ($parents as $parent):
|
||||||
$scheme = $formwork->schemes()->get('pages', $parent->template()->name());
|
$scheme = $formwork->schemes()->get('pages', $parent->template()->name());
|
||||||
if (!$scheme->get('pages', true)) continue;
|
if (!$scheme->get('pages', true)) {
|
||||||
if ($parent === $page) continue;
|
continue;
|
||||||
?>
|
}
|
||||||
|
if ($parent === $page) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
?>
|
||||||
<option value="<?= $parent->route() ?>"<?php if ($page->parent() === $parent): ?> selected<?php endif; ?>><?= str_repeat('— ', $parent->level() - 1) . $parent->title() ?></option>
|
<option value="<?= $parent->route() ?>"<?php if ($page->parent() === $parent): ?> selected<?php endif; ?>><?= str_repeat('— ', $parent->level() - 1) . $parent->title() ?></option>
|
||||||
<?php
|
<?php
|
||||||
endforeach;
|
endforeach;
|
@ -1,9 +1,9 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<select id="page-template" name="template">
|
<select id="page-template" name="template">
|
||||||
<?php
|
<?php
|
||||||
foreach ($templates as $template):
|
foreach ($templates as $template):
|
||||||
$scheme = $formwork->schemes()->get('pages', $template);
|
$scheme = $formwork->schemes()->get('pages', $template);
|
||||||
?>
|
?>
|
||||||
<option value="<?= $template ?>"<?php if ($page->template()->name() === $template): ?> selected<?php endif; ?>><?= $scheme->title() ?></option>
|
<option value="<?= $template ?>"<?php if ($page->template()->name() === $template): ?> selected<?php endif; ?>><?= $scheme->title() ?></option>
|
||||||
<?php
|
<?php
|
||||||
endforeach;
|
endforeach;
|
||||||
|
3
admin/views/fields/partials/description.php
Normal file
3
admin/views/fields/partials/description.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php if ($field->has('description')): ?>
|
||||||
|
<div style="font-size: 0.875rem; color: #7d7d7d; margin-bottom: 0.75rem;"><?= $this->markdown($field->get('description')); ?></div>
|
||||||
|
<?php endif; ?>
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'type' => 'password',
|
'type' => 'password',
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
@ -10,5 +10,6 @@
|
|||||||
'pattern' => $field->get('pattern'),
|
'pattern' => $field->get('pattern'),
|
||||||
'autocomplete' => $field->get('autocomplete'),
|
'autocomplete' => $field->get('autocomplete'),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled()
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<input <?= $this->attr([
|
<div>
|
||||||
'type' => 'range',
|
<input <?= $this->attr([
|
||||||
'id' => $field->name(),
|
'type' => 'range',
|
||||||
'name' => $field->formName(),
|
'id' => $field->name(),
|
||||||
'min' => $field->get('min'),
|
'name' => $field->formName(),
|
||||||
'max' => $field->get('max'),
|
'min' => $field->get('min'),
|
||||||
'step' => $field->get('step'),
|
'max' => $field->get('max'),
|
||||||
'value' => $field->value(),
|
'step' => $field->get('step'),
|
||||||
'required' => $field->isRequired(),
|
'value' => $field->value(),
|
||||||
'disabled' => $field->isDisabled()
|
'required' => $field->isRequired(),
|
||||||
]) ?>>
|
'disabled' => $field->isDisabled(),
|
||||||
<output class="input-range-value" for="<?= $field->name() ?>"><?= $field->value() ?></output>
|
'hidden' => $field->isHidden()
|
||||||
|
]) ?>>
|
||||||
|
<output class="input-range-value" for="<?= $field->name() ?>"><?= $field->value() ?></output>
|
||||||
|
</div>
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
<div class="row">
|
|
||||||
<?php $this->insert('fields', ['fields' => $field->get('fields')]) ?>
|
|
||||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||||||
<div class="container-full">
|
|
||||||
<?php $this->insert('fields', ['fields' => $field->get('fields')]) ?>
|
|
||||||
</div>
|
|
@ -1,9 +1,10 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<select <?= $this->attr([
|
<select <?= $this->attr([
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
'name' => $field->formName(),
|
'name' => $field->formName(),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled()
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
<?php foreach ((array) $field->get('options') as $value => $label): ?>
|
<?php foreach ((array) $field->get('options') as $value => $label): ?>
|
||||||
<option <?= $this->attr(['value' => $value, 'selected' => $value == $field->value()]) ?>><?= $label ?></option>
|
<option <?= $this->attr(['value' => $value, 'selected' => $value == $field->value()]) ?>><?= $label ?></option>
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<?php
|
<?php foreach ($field->get('fields') as $tab): ?>
|
||||||
foreach ($field->get('fields') as $tab):
|
|
||||||
?>
|
|
||||||
<a <?= $this->attr([
|
<a <?= $this->attr([
|
||||||
'class' => ['tabs-tab', $tab->get('active') ? 'active' : ''],
|
'class' => ['tabs-tab', $tab->get('active') ? 'active' : ''],
|
||||||
'data-tab' => $tab->name()
|
'data-tab' => $tab->name()
|
||||||
]) ?>><?= $tab->label() ?></a>
|
]) ?>><?= $tab->label() ?></a>
|
||||||
<?php
|
<?php endforeach; ?>
|
||||||
endforeach;
|
|
||||||
?>
|
|
||||||
</div>
|
</div>
|
||||||
<?php $this->insert('fields', ['fields' => $field->get('fields')]) ?>
|
<?php $this->insert('fields', ['fields' => $field->get('fields')]) ?>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
@ -7,6 +7,7 @@
|
|||||||
'placeholder' => $field->placeholder(),
|
'placeholder' => $field->placeholder(),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled(),
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden(),
|
||||||
'data-field' => 'tags',
|
'data-field' => 'tags',
|
||||||
'data-options' => $field->has('options') ? Formwork\Parsers\JSON::encode((array) $field->get('options')) : null
|
'data-options' => $field->has('options') ? Formwork\Parsers\JSON::encode((array) $field->get('options')) : null
|
||||||
]) ?>>
|
]) ?>>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<input <?= $this->attr([
|
<input <?= $this->attr([
|
||||||
'class' => $field->get('class'),
|
'class' => $field->get('class'),
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
@ -10,5 +10,6 @@
|
|||||||
'maxlength' => $field->get('max'),
|
'maxlength' => $field->get('max'),
|
||||||
'pattern' => $field->get('pattern'),
|
'pattern' => $field->get('pattern'),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled()
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>>
|
]) ?>>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<textarea <?= $this->attr([
|
<textarea <?= $this->attr([
|
||||||
'id' => $field->name(),
|
'id' => $field->name(),
|
||||||
'name' => $field->formName(),
|
'name' => $field->formName(),
|
||||||
'placeholder' => $field->placeholder(),
|
'placeholder' => $field->placeholder(),
|
||||||
'required' => $field->isRequired(),
|
'required' => $field->isRequired(),
|
||||||
'disabled' => $field->isDisabled()
|
'disabled' => $field->isDisabled(),
|
||||||
|
'hidden' => $field->isHidden()
|
||||||
]) ?>><?= $field->value() ?></textarea>
|
]) ?>><?= $field->value() ?></textarea>
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
<?= $this->insert('fields.label', ['field' => $field]) ?>
|
<?= $this->layout('fields.field') ?>
|
||||||
<fieldset <?= $this->attr([
|
<div>
|
||||||
'id' => $field->name(),
|
<fieldset <?= $this->attr([
|
||||||
'class' => 'input-togglegroup',
|
'id' => $field->name(),
|
||||||
'disabled' => $field->isDisabled()
|
'class' => 'input-togglegroup',
|
||||||
]) ?>>
|
'disabled' => $field->isDisabled(),
|
||||||
<?php foreach ((array) $field->get('options') as $value => $label): ?>
|
'hidden' => $field->isHidden()
|
||||||
<label>
|
]) ?>>
|
||||||
<input <?= $this->attr([
|
<?php foreach ((array) $field->get('options') as $value => $label): ?>
|
||||||
'type' => 'radio',
|
<label>
|
||||||
'name' => $field->formName(),
|
<input <?= $this->attr([
|
||||||
'value' => $value,
|
'type' => 'radio',
|
||||||
'checked' => $value == $field->value()
|
'name' => $field->formName(),
|
||||||
]) ?>>
|
'value' => $value,
|
||||||
<span><?= $label ?></span>
|
'checked' => $value == $field->value()
|
||||||
</label>
|
]) ?>>
|
||||||
<?php endforeach; ?>
|
<span><?= $label ?></span>
|
||||||
</fieldset>
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
3
admin/views/layouts/fields/field.php
Normal file
3
admin/views/layouts/fields/field.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?= $this->insert('fields.partials.label') ?>
|
||||||
|
<?= $this->content() ?>
|
||||||
|
<?= $this->insert('fields.partials.description') ?>
|
@ -5,7 +5,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<?php if (!$page->isIndexPage() && !$page->isErrorPage()): ?>
|
<?php if (!$page->isIndexPage() && !$page->isErrorPage()): ?>
|
||||||
<div class="page-route page-route-changeable">
|
<div class="page-route page-route-changeable">
|
||||||
<button type="button" class="page-slug-change" data-command="change-slug" title="<?= $this->translate('admin.pages.change-slug') ?>"><?= $page->route() ?></button><?= $this->icon('pencil') ?>
|
<button type="button" class="page-slug-change" data-command="change-slug" title="<?= $this->translate('admin.pages.change-slug') ?>"><?= $page->route() ?><?= $this->icon('pencil') ?></button>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="page-route"><span><?= $page->route() ?></span></div>
|
<div class="page-route"><span><?= $page->route() ?></span></div>
|
||||||
|
@ -10,18 +10,18 @@
|
|||||||
<?php
|
<?php
|
||||||
endif;
|
endif;
|
||||||
?>
|
?>
|
||||||
<ul class="pages-list <?= $class ?>" data-sortable-children="<?= $sortable ? 'true' : 'false' ?>"<?php if ($parent): ?> data-parent="<?= $parent ?>"<?php endif; ?>>
|
<ul class="pages-list <?= $class ?>" data-sortable-children="<?= $orderable ? 'true' : 'false' ?>"<?php if ($parent): ?> data-parent="<?= $parent ?>"<?php endif; ?>>
|
||||||
<?php
|
<?php
|
||||||
foreach ($pages as $page):
|
foreach ($pages as $page):
|
||||||
$routable = $page->published() && $page->routable();
|
$routable = $page->published() && $page->routable();
|
||||||
$date = $this->datetime($page->lastModifiedTime());
|
$date = $this->datetime($page->lastModifiedTime());
|
||||||
?>
|
?>
|
||||||
<li class="<?php if ($subpages): ?>pages-level-<?= $page->level() ?><?php endif; ?>" <?php if (!$page->sortable()): ?>data-sortable="false"<?php endif; ?>>
|
<li class="<?php if ($subpages): ?>pages-level-<?= $page->level() ?><?php endif; ?>" <?php if (!$page->orderable()): ?>data-sortable="false"<?php endif; ?>>
|
||||||
<div class="pages-item">
|
<div class="pages-item">
|
||||||
<div class="pages-item-cell page-details">
|
<div class="pages-item-cell page-details">
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
<?php
|
<?php
|
||||||
if ($sortable && $page->sortable()):
|
if ($orderable && $page->orderable()):
|
||||||
?>
|
?>
|
||||||
<span class="sort-handle" title="<?= $this->translate('admin.drag-to-reorder') ?>"><?= $this->icon('grabber') ?></span>
|
<span class="sort-handle" title="<?= $this->translate('admin.drag-to-reorder') ?>"><?= $this->icon('grabber') ?></span>
|
||||||
<?php
|
<?php
|
||||||
@ -70,14 +70,14 @@
|
|||||||
if ($subpages && $page->hasChildren()):
|
if ($subpages && $page->hasChildren()):
|
||||||
$scheme = $page->scheme();
|
$scheme = $page->scheme();
|
||||||
$reverseChildren = $scheme->get('children.reverse', false);
|
$reverseChildren = $scheme->get('children.reverse', false);
|
||||||
$sortableChildren = $scheme->get('children.sortable', true);
|
$orderableChildren = $scheme->get('children.sortable', true);
|
||||||
|
|
||||||
$this->insert('pages.list', [
|
$this->insert('pages.list', [
|
||||||
'pages' => $reverseChildren ? $page->children()->reverse() : $page->children(),
|
'pages' => $reverseChildren ? $page->children()->reverse() : $page->children(),
|
||||||
'subpages' => true,
|
'subpages' => true,
|
||||||
'class' => 'pages-children',
|
'class' => 'pages-children',
|
||||||
'parent' => $sortableChildren ? $page->route() : null,
|
'parent' => $orderableChildren ? $page->route() : null,
|
||||||
'sortable' => $sortable && $sortableChildren,
|
'sortable' => $orderable && $orderableChildren,
|
||||||
'headers' => false
|
'headers' => false
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -39,6 +39,9 @@ return [
|
|||||||
'errors' => [
|
'errors' => [
|
||||||
'set_handlers' => true
|
'set_handlers' => true
|
||||||
],
|
],
|
||||||
|
'fields' => [
|
||||||
|
'path' => FORMWORK_PATH . 'fields' . DS
|
||||||
|
],
|
||||||
'files' => [
|
'files' => [
|
||||||
'allowed_extensions' => [
|
'allowed_extensions' => [
|
||||||
'.jpg',
|
'.jpg',
|
||||||
|
32
formwork/fields/array.php
Normal file
32
formwork/fields/array.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Data\Contracts\Arrayable;
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'validate' => function (Field $field, $value) {
|
||||||
|
if (Constraint::isEmpty($value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Arrayable) {
|
||||||
|
$value = $value->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->get('associative')) {
|
||||||
|
foreach (array_keys($value) as $key) {
|
||||||
|
if (is_int($key)) {
|
||||||
|
unset($value[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter($value);
|
||||||
|
}
|
||||||
|
];
|
23
formwork/fields/checkbox.php
Normal file
23
formwork/fields/checkbox.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'validate' => function (Field $field, $value) {
|
||||||
|
if (Constraint::isTruthy($value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Constraint::isFalsy($value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
|
||||||
|
}
|
||||||
|
];
|
42
formwork/fields/date.php
Normal file
42
formwork/fields/date.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Formwork;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
use Formwork\Utils\Date;
|
||||||
|
use Formwork\Utils\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'format' => function (Field $field, string $format = null): string {
|
||||||
|
return Date::formatTimestamp($field->toTimestamp(), $format);
|
||||||
|
},
|
||||||
|
|
||||||
|
'toTimestamp' => function (Field $field): string {
|
||||||
|
return Date::toTimestamp($field->value());
|
||||||
|
},
|
||||||
|
|
||||||
|
'toDuration' => function (Field $field): string {
|
||||||
|
return Date::formatTimestampAsDistance($field->toTimestamp(), Formwork::instance()->languages()->current());
|
||||||
|
},
|
||||||
|
|
||||||
|
'toString' => function (Field $field): string {
|
||||||
|
return $field->format();
|
||||||
|
},
|
||||||
|
|
||||||
|
'return' => function (Field $field): Field {
|
||||||
|
return $field;
|
||||||
|
},
|
||||||
|
|
||||||
|
'validate' => function (Field $field, $value): ?string {
|
||||||
|
if (Constraint::isEmpty($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return date('Y-m-d H:i:s', Date::toTimestamp($value));
|
||||||
|
} catch (InvalidArgumentException $e) {
|
||||||
|
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s":%s', $field->name(), $field->type(), Str::after($e->getMessage(), ':')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
29
formwork/fields/duration.php
Normal file
29
formwork/fields/duration.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'validate' => function (Field $field, $value): int {
|
||||||
|
if (!is_numeric($value)) {
|
||||||
|
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This reliably casts numeric values to int or float
|
||||||
|
$value += 0;
|
||||||
|
|
||||||
|
if ($field->has('min') && $value < $field->get('min')) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be greater than or equal to %d', $field->name(), $field->type(), $field->get('min')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->has('max') && $value > $field->get('max')) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be less than or equal to %d', $field->name(), $field->type(), $field->get('max')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->has('step') && ($value - $field->get('min', 0)) % $field->get('step') !== 0) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not conform to the step value %d', $field->name(), $field->value(), $field->get('step')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
];
|
35
formwork/fields/email.php
Normal file
35
formwork/fields/email.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'validate' => function (Field $field, $value): string {
|
||||||
|
if (Constraint::isEmpty($value)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" is not a valid e-mail address', $field->name(), $field->value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($value)) {
|
||||||
|
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->has('min') && strlen($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($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($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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
];
|
23
formwork/fields/image.php
Normal file
23
formwork/fields/image.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'return' => function (Field $field): Field {
|
||||||
|
return $field;
|
||||||
|
},
|
||||||
|
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
];
|
44
formwork/fields/markdown.php
Normal file
44
formwork/fields/markdown.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Formwork;
|
||||||
|
use Formwork\Parsers\Markdown;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'toHTML' => function (Field $field): string {
|
||||||
|
$currentPage = Formwork::instance()->site()->currentPage();
|
||||||
|
return Markdown::parse($field->value(), ['baseRoute' => $currentPage ? $currentPage->route() : '/']);
|
||||||
|
},
|
||||||
|
|
||||||
|
'toString' => function (Field $field): string {
|
||||||
|
return $field->toHTML();
|
||||||
|
},
|
||||||
|
|
||||||
|
'return' => function (Field $field): Field {
|
||||||
|
return $field;
|
||||||
|
},
|
||||||
|
|
||||||
|
'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($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($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')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = str_replace("\r\n", "\n", $value);
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
];
|
29
formwork/fields/number.php
Normal file
29
formwork/fields/number.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'validate' => function (Field $field, $value): int | float {
|
||||||
|
if (!is_numeric($value)) {
|
||||||
|
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This reliably casts numeric values to int or float
|
||||||
|
$value += 0;
|
||||||
|
|
||||||
|
if ($field->has('min') && $value < $field->get('min')) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be greater than or equal to %d', $field->name(), $field->type(), $field->get('min')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->has('max') && $value > $field->get('max')) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be less than or equal to %d', $field->name(), $field->type(), $field->get('max')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->has('step') && ($value - $field->get('min', 0)) % $field->get('step') !== 0) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not conform to the step value %d', $field->name(), $field->value(), $field->get('step')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
];
|
31
formwork/fields/password.php
Normal file
31
formwork/fields/password.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
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($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($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($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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
];
|
29
formwork/fields/range.php
Normal file
29
formwork/fields/range.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'validate' => function (Field $field, $value): int {
|
||||||
|
if (!is_numeric($value)) {
|
||||||
|
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This reliably casts numeric values to int or float
|
||||||
|
$value += 0;
|
||||||
|
|
||||||
|
if ($field->has('min') && $value < $field->get('min')) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be greater than or equal to %d', $field->name(), $field->type(), $field->get('min')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->has('max') && $value > $field->get('max')) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be less than or equal to %d', $field->name(), $field->type(), $field->get('max')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->has('step') && ($value - $field->get('min', 0)) % $field->get('step') !== 0) {
|
||||||
|
throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not conform to the step value %d', $field->name(), $field->value(), $field->get('step')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
];
|
14
formwork/fields/select.php
Normal file
14
formwork/fields/select.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'validate' => function (Field $field, $value) {
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
// This reliably casts numeric values to int or float
|
||||||
|
return $value + 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
];
|
36
formwork/fields/tags.php
Normal file
36
formwork/fields/tags.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Data\Collection;
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'toString' => function ($field) {
|
||||||
|
return implode(', ', $field->value());
|
||||||
|
},
|
||||||
|
|
||||||
|
'return' => function (Field $field): Collection {
|
||||||
|
return Collection::from($field->value());
|
||||||
|
},
|
||||||
|
|
||||||
|
'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')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter($value));
|
||||||
|
}
|
||||||
|
];
|
31
formwork/fields/text.php
Normal file
31
formwork/fields/text.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
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($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($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($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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
];
|
29
formwork/fields/textarea.php
Normal file
29
formwork/fields/textarea.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
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($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($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')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = str_replace("\r\n", "\n", (string) $value);
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
];
|
23
formwork/fields/togglegroup.php
Normal file
23
formwork/fields/togglegroup.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Formwork\Fields\Field;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'validate' => function (Field $field, $value) {
|
||||||
|
if (Constraint::isTruthy($value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Constraint::isFalsy($value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
// This reliably casts numeric values to int or float
|
||||||
|
return $value + 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
];
|
@ -1,319 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Formwork;
|
|
||||||
|
|
||||||
use Formwork\Metadata\Metadata;
|
|
||||||
use Formwork\Utils\Arr;
|
|
||||||
use Formwork\Utils\FileSystem;
|
|
||||||
use Formwork\Utils\HTTPRequest;
|
|
||||||
use Formwork\Utils\Uri;
|
|
||||||
use BadMethodCallException;
|
|
||||||
|
|
||||||
abstract class AbstractPage
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Page path
|
|
||||||
*/
|
|
||||||
protected string $path;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page path relative to content path
|
|
||||||
*/
|
|
||||||
protected string $relativePath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page unique identifier
|
|
||||||
*/
|
|
||||||
protected string $uid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page route
|
|
||||||
*/
|
|
||||||
protected string $route;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page data
|
|
||||||
*/
|
|
||||||
protected array $data = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page uri
|
|
||||||
*/
|
|
||||||
protected string $uri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page absolute uri
|
|
||||||
*/
|
|
||||||
protected string $absoluteUri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page last modified time
|
|
||||||
*/
|
|
||||||
protected int $lastModifiedTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page modified date
|
|
||||||
*/
|
|
||||||
protected string $date;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page metadata
|
|
||||||
*/
|
|
||||||
protected Metadata $metadata;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page parent
|
|
||||||
*
|
|
||||||
* @var Page|Site
|
|
||||||
*/
|
|
||||||
protected $parent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PageCollection containing page parents
|
|
||||||
*/
|
|
||||||
protected PageCollection $parents;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page level
|
|
||||||
*/
|
|
||||||
protected int $level;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PageCollection containing page children
|
|
||||||
*/
|
|
||||||
protected PageCollection $children;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PageCollection containing page descendants
|
|
||||||
*/
|
|
||||||
protected PageCollection $descendants;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a URI relative to page
|
|
||||||
*
|
|
||||||
* @param bool|string $includeLanguage
|
|
||||||
*/
|
|
||||||
public function uri(string $path = '', $includeLanguage = true): string
|
|
||||||
{
|
|
||||||
$base = HTTPRequest::root();
|
|
||||||
if ($includeLanguage) {
|
|
||||||
$language = is_string($includeLanguage) ? $includeLanguage : Formwork::instance()->site()->languages()->current();
|
|
||||||
|
|
||||||
$default = Formwork::instance()->site()->languages()->default();
|
|
||||||
$preferred = Formwork::instance()->site()->languages()->preferred();
|
|
||||||
|
|
||||||
if (($language !== null && $language !== $default) || ($preferred !== null && $preferred !== $default)) {
|
|
||||||
$base .= $language . '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $base . ltrim($this->route, '/') . ltrim($path, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the page unique identifier
|
|
||||||
*/
|
|
||||||
public function uid(): string
|
|
||||||
{
|
|
||||||
if (isset($this->uid)) {
|
|
||||||
return $this->uid;
|
|
||||||
}
|
|
||||||
return $this->uid = substr(hash('sha256', $this->relativePath), 0, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get page absolute URI
|
|
||||||
*/
|
|
||||||
public function absoluteUri(): string
|
|
||||||
{
|
|
||||||
if (isset($this->absoluteUri)) {
|
|
||||||
return $this->absoluteUri;
|
|
||||||
}
|
|
||||||
return $this->absoluteUri = Uri::resolveRelative($this->uri());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get page last modified time
|
|
||||||
*/
|
|
||||||
public function lastModifiedTime(): int
|
|
||||||
{
|
|
||||||
if (isset($this->lastModifiedTime)) {
|
|
||||||
return $this->lastModifiedTime;
|
|
||||||
}
|
|
||||||
return $this->lastModifiedTime = FileSystem::lastModifiedTime($this->path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return page date optionally in a given format
|
|
||||||
*
|
|
||||||
* @param string $format
|
|
||||||
*/
|
|
||||||
public function date(string $format = null): string
|
|
||||||
{
|
|
||||||
$format ??= Formwork::instance()->config()->get('date.format');
|
|
||||||
return date($format, $this->lastModifiedTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get parent page
|
|
||||||
*
|
|
||||||
* @return Page|Site|null
|
|
||||||
*/
|
|
||||||
public function parent()
|
|
||||||
{
|
|
||||||
if (isset($this->parent)) {
|
|
||||||
return $this->parent;
|
|
||||||
}
|
|
||||||
$parentPath = dirname($this->path) . DS;
|
|
||||||
if (FileSystem::isDirectory($parentPath) && $parentPath !== Formwork::instance()->config()->get('content.path')) {
|
|
||||||
return $this->parent = Formwork::instance()->site()->retrievePage($parentPath);
|
|
||||||
}
|
|
||||||
// If no parent was found returns the site as first level pages' parent
|
|
||||||
return $this->parent = Formwork::instance()->site();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a PageCollection containing page parents
|
|
||||||
*/
|
|
||||||
public function parents(): PageCollection
|
|
||||||
{
|
|
||||||
if (isset($this->parents)) {
|
|
||||||
return $this->parents;
|
|
||||||
}
|
|
||||||
$parentPages = [];
|
|
||||||
$page = $this;
|
|
||||||
while (($parent = $page->parent()) !== null) {
|
|
||||||
$parentPages[] = $parent;
|
|
||||||
$page = $parent;
|
|
||||||
}
|
|
||||||
return $this->parents = new PageCollection(array_reverse($parentPages));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether page has parents
|
|
||||||
*/
|
|
||||||
public function hasParents(): bool
|
|
||||||
{
|
|
||||||
return !$this->parents()->isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a PageCollection containing page children
|
|
||||||
*/
|
|
||||||
public function children(): PageCollection
|
|
||||||
{
|
|
||||||
if (isset($this->children)) {
|
|
||||||
return $this->children;
|
|
||||||
}
|
|
||||||
return $this->children = PageCollection::fromPath($this->path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether page has children
|
|
||||||
*/
|
|
||||||
public function hasChildren(): bool
|
|
||||||
{
|
|
||||||
return !$this->children()->isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a PageCollection containing page descendants
|
|
||||||
*/
|
|
||||||
public function descendants(): PageCollection
|
|
||||||
{
|
|
||||||
if (isset($this->descendants)) {
|
|
||||||
return $this->descendants;
|
|
||||||
}
|
|
||||||
return $this->descendants = PageCollection::fromPath($this->path, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether page has descendants
|
|
||||||
*/
|
|
||||||
public function hasDescendants(): bool
|
|
||||||
{
|
|
||||||
foreach ($this->children() as $child) {
|
|
||||||
if ($child->hasChildren()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return page level
|
|
||||||
*/
|
|
||||||
public function level(): int
|
|
||||||
{
|
|
||||||
if (isset($this->level)) {
|
|
||||||
return $this->level;
|
|
||||||
}
|
|
||||||
return $this->level = $this->parents()->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether current page is Site
|
|
||||||
*/
|
|
||||||
abstract public function isSite(): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether current page is index page
|
|
||||||
*/
|
|
||||||
abstract public function isIndexPage(): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether current page is error page
|
|
||||||
*/
|
|
||||||
abstract public function isErrorPage(): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether current page is deletable
|
|
||||||
*/
|
|
||||||
abstract public function isDeletable(): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return page metadata
|
|
||||||
*/
|
|
||||||
abstract public function metadata(): Metadata;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get page data by key
|
|
||||||
*/
|
|
||||||
public function get(string $key, $default = null)
|
|
||||||
{
|
|
||||||
if (property_exists($this, $key)) {
|
|
||||||
// Call getter method if exists and property is null
|
|
||||||
if (!isset($this->$key) && method_exists($this, $key)) {
|
|
||||||
return $this->$key();
|
|
||||||
}
|
|
||||||
return $this->$key;
|
|
||||||
}
|
|
||||||
if (Arr::has($this->data, $key)) {
|
|
||||||
return Arr::get($this->data, $key, $default);
|
|
||||||
}
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether page data has a key
|
|
||||||
*/
|
|
||||||
public function has(string $key): bool
|
|
||||||
{
|
|
||||||
return property_exists($this, $key) || Arr::has($this->data, $key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set page data
|
|
||||||
*/
|
|
||||||
public function set(string $key, $value): void
|
|
||||||
{
|
|
||||||
Arr::set($this->data, $key, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __call(string $name, array $arguments)
|
|
||||||
{
|
|
||||||
if ($this->has($name)) {
|
|
||||||
return $this->get($name);
|
|
||||||
}
|
|
||||||
throw new BadMethodCallException(sprintf('Call to undefined method %s::%s()', static::class, $name));
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ use Formwork\Controllers\AbstractController as BaseAbstractController;
|
|||||||
use Formwork\Formwork;
|
use Formwork\Formwork;
|
||||||
use Formwork\Parsers\JSON;
|
use Formwork\Parsers\JSON;
|
||||||
use Formwork\Parsers\PHP;
|
use Formwork\Parsers\PHP;
|
||||||
use Formwork\Site;
|
use Formwork\Pages\Site;
|
||||||
use Formwork\Utils\Date;
|
use Formwork\Utils\Date;
|
||||||
use Formwork\Utils\HTTPRequest;
|
use Formwork\Utils\HTTPRequest;
|
||||||
use Formwork\View\View;
|
use Formwork\View\View;
|
||||||
|
@ -18,8 +18,8 @@ class DashboardController extends AbstractController
|
|||||||
$statistics = new Statistics();
|
$statistics = new Statistics();
|
||||||
|
|
||||||
$this->modal('newPage', [
|
$this->modal('newPage', [
|
||||||
'templates' => $this->site()->templates(),
|
'templates' => $this->site()->templates()->keys(),
|
||||||
'pages' => $this->site()->descendants()->sortBy('path')
|
'pages' => $this->site()->descendants()->sortBy('relativePath')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->modal('deletePage');
|
$this->modal('deletePage');
|
||||||
@ -27,12 +27,12 @@ class DashboardController extends AbstractController
|
|||||||
return new Response($this->view('dashboard.index', [
|
return new Response($this->view('dashboard.index', [
|
||||||
'title' => $this->admin()->translate('admin.dashboard.dashboard'),
|
'title' => $this->admin()->translate('admin.dashboard.dashboard'),
|
||||||
'lastModifiedPages' => $this->view('pages.list', [
|
'lastModifiedPages' => $this->view('pages.list', [
|
||||||
'pages' => $this->site()->descendants()->sortBy('lastModifiedTime', direction: SORT_DESC)->slice(0, 5),
|
'pages' => $this->site()->descendants()->sortBy('lastModifiedTime', direction: SORT_DESC)->slice(0, 5),
|
||||||
'subpages' => false,
|
'subpages' => false,
|
||||||
'class' => 'pages-list-top',
|
'class' => 'pages-list-top',
|
||||||
'parent' => null,
|
'parent' => null,
|
||||||
'sortable' => false,
|
'orderable' => false,
|
||||||
'headers' => true
|
'headers' => true
|
||||||
], true),
|
], true),
|
||||||
'statistics' => JSON::encode($statistics->getChartData())
|
'statistics' => JSON::encode($statistics->getChartData())
|
||||||
], true));
|
], true));
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
namespace Formwork\Admin\Controllers;
|
namespace Formwork\Admin\Controllers;
|
||||||
|
|
||||||
use Formwork\Data\DataGetter;
|
use Formwork\Fields\FieldCollection;
|
||||||
use Formwork\Fields\Fields;
|
|
||||||
use Formwork\Fields\Validator;
|
|
||||||
use Formwork\Formwork;
|
use Formwork\Formwork;
|
||||||
use Formwork\Parsers\JSON;
|
use Formwork\Parsers\JSON;
|
||||||
use Formwork\Parsers\YAML;
|
use Formwork\Parsers\YAML;
|
||||||
@ -38,13 +36,16 @@ class OptionsController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->ensurePermission('options.system');
|
$this->ensurePermission('options.system');
|
||||||
|
|
||||||
$fields = new Fields(Formwork::instance()->schemes()->get('config', 'system')->get('fields'));
|
$scheme = Formwork::instance()->schemes()->get('config', 'system');
|
||||||
|
$fields = $scheme->fields();
|
||||||
|
|
||||||
if (HTTPRequest::method() === 'POST') {
|
if (HTTPRequest::method() === 'POST') {
|
||||||
$data = HTTPRequest::postData();
|
$data = HTTPRequest::postData();
|
||||||
$options = Formwork::instance()->config();
|
$options = Formwork::instance()->config();
|
||||||
$defaults = Formwork::instance()->defaults();
|
$defaults = Formwork::instance()->defaults();
|
||||||
$differ = $this->updateOptions('system', $fields->validate($data), $options->toArray(), $defaults);
|
$fields->validate($data);
|
||||||
|
|
||||||
|
$differ = $this->updateOptions('system', $fields, $options->toArray(), $defaults);
|
||||||
|
|
||||||
// Touch content folder to invalidate cache
|
// Touch content folder to invalidate cache
|
||||||
if ($differ) {
|
if ($differ) {
|
||||||
@ -55,7 +56,7 @@ class OptionsController extends AbstractController
|
|||||||
return $this->admin()->redirect('/options/system/');
|
return $this->admin()->redirect('/options/system/');
|
||||||
}
|
}
|
||||||
|
|
||||||
$fields->validate(new DataGetter(Formwork::instance()->config()->toArray()));
|
$fields->validate(Formwork::instance()->config());
|
||||||
|
|
||||||
$this->modal('changes');
|
$this->modal('changes');
|
||||||
|
|
||||||
@ -76,13 +77,15 @@ class OptionsController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->ensurePermission('options.site');
|
$this->ensurePermission('options.site');
|
||||||
|
|
||||||
$fields = new Fields(Formwork::instance()->schemes()->get('config', 'site')->get('fields'));
|
$scheme = Formwork::instance()->schemes()->get('config', 'site');
|
||||||
|
$fields = $scheme->fields();
|
||||||
|
|
||||||
if (HTTPRequest::method() === 'POST') {
|
if (HTTPRequest::method() === 'POST') {
|
||||||
$data = HTTPRequest::postData();
|
$data = HTTPRequest::postData();
|
||||||
$options = $this->site()->data();
|
$options = $this->site()->data();
|
||||||
$defaults = Formwork::instance()->site()->defaults();
|
$defaults = Formwork::instance()->site()->defaults();
|
||||||
$differ = $this->updateOptions('site', $fields->validate($data), $options, $defaults);
|
$fields->validate($data);
|
||||||
|
$differ = $this->updateOptions('site', $fields, $options, $defaults);
|
||||||
|
|
||||||
// Touch content folder to invalidate cache
|
// Touch content folder to invalidate cache
|
||||||
if ($differ) {
|
if ($differ) {
|
||||||
@ -93,7 +96,7 @@ class OptionsController extends AbstractController
|
|||||||
return $this->admin()->redirect('/options/site/');
|
return $this->admin()->redirect('/options/site/');
|
||||||
}
|
}
|
||||||
|
|
||||||
$fields->validate(new DataGetter($this->site()->data()));
|
$fields->validate($this->site()->data());
|
||||||
|
|
||||||
$this->modal('changes');
|
$this->modal('changes');
|
||||||
|
|
||||||
@ -237,26 +240,20 @@ class OptionsController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* Update options of a given type with given data
|
* Update options of a given type with given data
|
||||||
*
|
*
|
||||||
* @param string $type Options type ('system' or 'site')
|
* @param string $type Options type ('system' or 'site')
|
||||||
* @param Fields $fields Fields object
|
* @param FieldCollection $fields FieldCollection object
|
||||||
* @param array $options Current options
|
* @param array $options Current options
|
||||||
* @param array $defaults Default values
|
* @param array $defaults Default values
|
||||||
*
|
*
|
||||||
* @return bool Whether new values were applied or not
|
* @return bool Whether new values were applied or not
|
||||||
*/
|
*/
|
||||||
protected function updateOptions(string $type, Fields $fields, array $options, array $defaults): bool
|
protected function updateOptions(string $type, FieldCollection $fields, array $options, array $defaults): bool
|
||||||
{
|
{
|
||||||
// Flatten fields
|
|
||||||
$fields = $fields->toArray(true);
|
|
||||||
|
|
||||||
$old = $options;
|
$old = $options;
|
||||||
$options = [];
|
$options = [];
|
||||||
|
|
||||||
// Update options with new values
|
// Update options with new values
|
||||||
foreach ($fields as $field) {
|
foreach ($fields as $field) {
|
||||||
if (in_array($field->type(), Validator::IGNORED_FIELDS, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($field->isRequired() && $field->isEmpty()) {
|
if ($field->isRequired() && $field->isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -5,20 +5,19 @@ namespace Formwork\Admin\Controllers;
|
|||||||
use Formwork\Admin\Uploader;
|
use Formwork\Admin\Uploader;
|
||||||
use Formwork\Data\DataGetter;
|
use Formwork\Data\DataGetter;
|
||||||
use Formwork\Exceptions\TranslatedException;
|
use Formwork\Exceptions\TranslatedException;
|
||||||
use Formwork\Fields\Fields;
|
use Formwork\Fields\FieldCollection;
|
||||||
use Formwork\Files\Image;
|
use Formwork\Files\Image;
|
||||||
use Formwork\Formwork;
|
use Formwork\Formwork;
|
||||||
use Formwork\Languages\LanguageCodes;
|
use Formwork\Languages\LanguageCodes;
|
||||||
use Formwork\Page;
|
use Formwork\Pages\Page;
|
||||||
use Formwork\Parsers\YAML;
|
use Formwork\Parsers\YAML;
|
||||||
use Formwork\Response\JSONResponse;
|
use Formwork\Response\JSONResponse;
|
||||||
use Formwork\Response\RedirectResponse;
|
use Formwork\Response\RedirectResponse;
|
||||||
use Formwork\Response\Response;
|
use Formwork\Response\Response;
|
||||||
use Formwork\Router\RouteParams;
|
use Formwork\Router\RouteParams;
|
||||||
use Formwork\Site;
|
use Formwork\Pages\Site;
|
||||||
use Formwork\Utils\FileSystem;
|
use Formwork\Utils\FileSystem;
|
||||||
use Formwork\Utils\HTTPRequest;
|
use Formwork\Utils\HTTPRequest;
|
||||||
use Formwork\Utils\Session;
|
|
||||||
use Formwork\Utils\Str;
|
use Formwork\Utils\Str;
|
||||||
use Formwork\Utils\Uri;
|
use Formwork\Utils\Uri;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
@ -46,8 +45,8 @@ class PagesController extends AbstractController
|
|||||||
$this->ensurePermission('pages.index');
|
$this->ensurePermission('pages.index');
|
||||||
|
|
||||||
$this->modal('newPage', [
|
$this->modal('newPage', [
|
||||||
'templates' => $this->site()->templates(),
|
'templates' => $this->site()->templates()->keys(),
|
||||||
'pages' => $this->site()->descendants()->sortBy('path')
|
'pages' => $this->site()->descendants()->sortBy('relativePath')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->modal('deletePage');
|
$this->modal('deletePage');
|
||||||
@ -55,12 +54,12 @@ class PagesController extends AbstractController
|
|||||||
return new Response($this->view('pages.index', [
|
return new Response($this->view('pages.index', [
|
||||||
'title' => $this->admin()->translate('admin.pages.pages'),
|
'title' => $this->admin()->translate('admin.pages.pages'),
|
||||||
'pagesList' => $this->view('pages.list', [
|
'pagesList' => $this->view('pages.list', [
|
||||||
'pages' => $this->site()->pages(),
|
'pages' => $this->site()->pages(),
|
||||||
'subpages' => true,
|
'subpages' => true,
|
||||||
'class' => 'pages-list-top',
|
'class' => 'pages-list-top',
|
||||||
'parent' => '.',
|
'parent' => '.',
|
||||||
'sortable' => $this->user()->permissions()->has('pages.reorder'),
|
'orderable' => $this->user()->permissions()->has('pages.reorder'),
|
||||||
'headers' => true
|
'headers' => true
|
||||||
], true)
|
], true)
|
||||||
], true));
|
], true));
|
||||||
}
|
}
|
||||||
@ -77,7 +76,6 @@ class PagesController extends AbstractController
|
|||||||
// Let's create the page
|
// Let's create the page
|
||||||
try {
|
try {
|
||||||
$page = $this->createPage($data);
|
$page = $this->createPage($data);
|
||||||
Session::set('FORMWORK_PAGE_TO_PUBLISH', $page->route());
|
|
||||||
$this->admin()->notify($this->admin()->translate('admin.pages.page.created'), 'success');
|
$this->admin()->notify($this->admin()->translate('admin.pages.page.created'), 'success');
|
||||||
} catch (TranslatedException $e) {
|
} catch (TranslatedException $e) {
|
||||||
$this->admin()->notify($e->getTranslatedMessage(), 'error');
|
$this->admin()->notify($e->getTranslatedMessage(), 'error');
|
||||||
@ -121,21 +119,13 @@ class PagesController extends AbstractController
|
|||||||
return $this->admin()->redirect('/pages/' . trim($page->route(), '/') . '/edit/language/' . $page->language() . '/');
|
return $this->admin()->redirect('/pages/' . trim($page->route(), '/') . '/edit/language/' . $page->language() . '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if page has to be published on next save
|
|
||||||
if (Session::has('FORMWORK_PAGE_TO_PUBLISH')) {
|
|
||||||
if ($page->route() === Session::get('FORMWORK_PAGE_TO_PUBLISH')) {
|
|
||||||
$page->set('published', true);
|
|
||||||
}
|
|
||||||
Session::remove('FORMWORK_PAGE_TO_PUBLISH');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load page fields
|
// Load page fields
|
||||||
$fields = new Fields($page->scheme()->get('fields'));
|
$fields = $page->scheme()->fields();
|
||||||
|
|
||||||
switch (HTTPRequest::method()) {
|
switch (HTTPRequest::method()) {
|
||||||
case 'GET':
|
case 'GET':
|
||||||
// Load data from the page itself
|
// Load data from the page itself
|
||||||
$data = new DataGetter(array_merge($page->data(), ['content' => $page->rawContent()]));
|
$data = $page->data();
|
||||||
|
|
||||||
// Validate fields against data
|
// Validate fields against data
|
||||||
$fields->validate($data);
|
$fields->validate($data);
|
||||||
@ -189,8 +179,8 @@ class PagesController extends AbstractController
|
|||||||
'title' => $this->admin()->translate('admin.pages.edit-page', $page->title()),
|
'title' => $this->admin()->translate('admin.pages.edit-page', $page->title()),
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'fields' => $fields,
|
'fields' => $fields,
|
||||||
'templates' => $this->site()->templates(),
|
'templates' => $this->site()->templates()->keys(),
|
||||||
'parents' => $this->site()->descendants()->sortBy('path'),
|
'parents' => $this->site()->descendants()->sortBy('relativePath'),
|
||||||
'currentLanguage' => $params->get('language', $page->language()),
|
'currentLanguage' => $params->get('language', $page->language()),
|
||||||
'availableLanguages' => $this->availableSiteLanguages()
|
'availableLanguages' => $this->availableSiteLanguages()
|
||||||
], true));
|
], true));
|
||||||
@ -368,7 +358,7 @@ class PagesController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate page template
|
// Validate page template
|
||||||
if (!$this->site()->hasTemplate($data->get('template'))) {
|
if (!$this->site()->templates()->has($data->get('template'))) {
|
||||||
throw new TranslatedException('Invalid page template', 'admin.pages.page.cannot-create.invalid-template');
|
throw new TranslatedException('Invalid page template', 'admin.pages.page.cannot-create.invalid-template');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,7 +391,7 @@ class PagesController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* Update a page
|
* Update a page
|
||||||
*/
|
*/
|
||||||
protected function updatePage(Page $page, DataGetter $data, Fields $fields): Page
|
protected function updatePage(Page $page, DataGetter $data, FieldCollection $fields): Page
|
||||||
{
|
{
|
||||||
// Ensure no required data is missing
|
// Ensure no required data is missing
|
||||||
if (!$data->hasMultiple(['title', 'content'])) {
|
if (!$data->hasMultiple(['title', 'content'])) {
|
||||||
@ -420,7 +410,7 @@ class PagesController extends AbstractController
|
|||||||
$defaults = $page->defaults();
|
$defaults = $page->defaults();
|
||||||
|
|
||||||
// Handle data from fields
|
// Handle data from fields
|
||||||
foreach ($fields->toArray(true) as $field) {
|
foreach ($fields as $field) {
|
||||||
$default = array_key_exists($field->name(), $defaults) && $field->value() === $defaults[$field->name()];
|
$default = array_key_exists($field->name(), $defaults) && $field->value() === $defaults[$field->name()];
|
||||||
|
|
||||||
// Remove empty and default values
|
// Remove empty and default values
|
||||||
@ -442,7 +432,7 @@ class PagesController extends AbstractController
|
|||||||
throw new TranslatedException('Invalid page language', 'admin.pages.page.cannot-edit.invalid-language');
|
throw new TranslatedException('Invalid page language', 'admin.pages.page.cannot-edit.invalid-language');
|
||||||
}
|
}
|
||||||
|
|
||||||
$differ = $frontmatter !== $page->frontmatter() || $content !== $page->rawContent() || $language !== $page->language();
|
$differ = $frontmatter !== $page->frontmatter() || $content !== $page->data()['content'] || $language !== $page->language();
|
||||||
|
|
||||||
if ($differ) {
|
if ($differ) {
|
||||||
$filename = $data->get('template');
|
$filename = $data->get('template');
|
||||||
@ -463,8 +453,8 @@ class PagesController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if page number has to change
|
// Check if page number has to change
|
||||||
if (!empty($page->date()) && $page->scheme()->get('num') === 'date' && $page->num() !== (int) $page->date(self::DATE_NUM_FORMAT)) {
|
if ($page->scheme()->get('num') === 'date' && $page->num() !== ($num = (int) date(self::DATE_NUM_FORMAT, $page->timestamp()))) {
|
||||||
$name = preg_replace(Page::NUM_REGEX, $page->date(self::DATE_NUM_FORMAT) . '-', $page->name());
|
$name = preg_replace(Page::NUM_REGEX, $num . '-', $page->name());
|
||||||
try {
|
try {
|
||||||
$page = $this->changePageName($page, $name);
|
$page = $this->changePageName($page, $name);
|
||||||
} catch (RuntimeException $e) {
|
} catch (RuntimeException $e) {
|
||||||
@ -483,7 +473,7 @@ class PagesController extends AbstractController
|
|||||||
|
|
||||||
// Check if page template has to change
|
// Check if page template has to change
|
||||||
if ($page->template()->name() !== ($template = $data->get('template'))) {
|
if ($page->template()->name() !== ($template = $data->get('template'))) {
|
||||||
if (!$this->site()->hasTemplate($template)) {
|
if (!$this->site()->templates()->has($template)) {
|
||||||
throw new TranslatedException('Invalid page template', 'admin.pages.page.cannot-edit.invalid-template');
|
throw new TranslatedException('Invalid page template', 'admin.pages.page.cannot-edit.invalid-template');
|
||||||
}
|
}
|
||||||
$page = $this->changePageTemplate($page, $template);
|
$page = $this->changePageTemplate($page, $template);
|
||||||
@ -531,10 +521,9 @@ class PagesController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* Make a page num according to 'date' or default mode
|
* Make a page num according to 'date' or default mode
|
||||||
*
|
*
|
||||||
* @param Page|Site $parent
|
* @param string $mode 'date' for pages with a publish date
|
||||||
* @param string $mode 'date' for pages with a publish date
|
|
||||||
*/
|
*/
|
||||||
protected function makePageNum($parent, ?string $mode): string
|
protected function makePageNum(Page|Site $parent, ?string $mode): string
|
||||||
{
|
{
|
||||||
if (!$parent instanceof Page && !$parent instanceof Site) {
|
if (!$parent instanceof Page && !$parent instanceof Site) {
|
||||||
throw new InvalidArgumentException(sprintf('%s() accepts only instances of %s or %s as $parent argument', __METHOD__, Page::class, Site::class));
|
throw new InvalidArgumentException(sprintf('%s() accepts only instances of %s or %s as $parent argument', __METHOD__, Page::class, Site::class));
|
||||||
@ -567,14 +556,9 @@ class PagesController extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the parent of a page
|
* Change the parent of a page
|
||||||
*
|
|
||||||
* @param Page|Site $parent
|
|
||||||
*/
|
*/
|
||||||
protected function changePageParent(Page $page, $parent): Page
|
protected function changePageParent(Page $page, Page|Site $parent): Page
|
||||||
{
|
{
|
||||||
if (!$parent instanceof Page && !$parent instanceof Site) {
|
|
||||||
throw new InvalidArgumentException(sprintf('%s() accepts only instances of %s or %s as $parent argument', __METHOD__, Page::class, Site::class));
|
|
||||||
}
|
|
||||||
$destination = FileSystem::joinPaths($parent->path(), $page->name(), DS);
|
$destination = FileSystem::joinPaths($parent->path(), $page->name(), DS);
|
||||||
FileSystem::moveDirectory($page->path(), $destination);
|
FileSystem::moveDirectory($page->path(), $destination);
|
||||||
return new Page($destination);
|
return new Page($destination);
|
||||||
@ -595,10 +579,8 @@ class PagesController extends AbstractController
|
|||||||
* Resolve parent page helper
|
* Resolve parent page helper
|
||||||
*
|
*
|
||||||
* @param string $parent Page URI or '.' for site
|
* @param string $parent Page URI or '.' for site
|
||||||
*
|
|
||||||
* @return Page|Site|null
|
|
||||||
*/
|
*/
|
||||||
protected function resolveParent(string $parent)
|
protected function resolveParent(string $parent): Page|Site
|
||||||
{
|
{
|
||||||
if ($parent === '.') {
|
if ($parent === '.') {
|
||||||
return $this->site();
|
return $this->site();
|
||||||
|
@ -5,10 +5,7 @@ namespace Formwork\Admin\Controllers;
|
|||||||
use Formwork\Admin\Security\Password;
|
use Formwork\Admin\Security\Password;
|
||||||
use Formwork\Admin\Uploader;
|
use Formwork\Admin\Uploader;
|
||||||
use Formwork\Admin\Users\User;
|
use Formwork\Admin\Users\User;
|
||||||
use Formwork\Data\DataGetter;
|
|
||||||
use Formwork\Data\DataSetter;
|
|
||||||
use Formwork\Exceptions\TranslatedException;
|
use Formwork\Exceptions\TranslatedException;
|
||||||
use Formwork\Fields\Fields;
|
|
||||||
use Formwork\Files\Image;
|
use Formwork\Files\Image;
|
||||||
use Formwork\Formwork;
|
use Formwork\Formwork;
|
||||||
use Formwork\Parsers\YAML;
|
use Formwork\Parsers\YAML;
|
||||||
@ -113,7 +110,9 @@ class UsersController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function profile(RouteParams $params): Response
|
public function profile(RouteParams $params): Response
|
||||||
{
|
{
|
||||||
$fields = new Fields(Formwork::instance()->schemes()->get('admin', 'user')->get('fields'));
|
$scheme = Formwork::instance()->schemes()->get('admin', 'user');
|
||||||
|
|
||||||
|
$fields = $scheme->fields();
|
||||||
|
|
||||||
$user = $this->admin()->users()->get($params->get('user'));
|
$user = $this->admin()->users()->get($params->get('user'));
|
||||||
|
|
||||||
@ -123,13 +122,13 @@ class UsersController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Disable password and/or role fields if they cannot be changed
|
// Disable password and/or role fields if they cannot be changed
|
||||||
$fields->find('password')->set('disabled', !$this->user()->canChangePasswordOf($user));
|
$fields->get('password')->set('disabled', !$this->user()->canChangePasswordOf($user));
|
||||||
$fields->find('role')->set('disabled', !$this->user()->canChangeRoleOf($user));
|
$fields->get('role')->set('disabled', !$this->user()->canChangeRoleOf($user));
|
||||||
|
|
||||||
if (HTTPRequest::method() === 'POST') {
|
if (HTTPRequest::method() === 'POST') {
|
||||||
// Ensure that options can be changed
|
// Ensure that options can be changed
|
||||||
if ($this->user()->canChangeOptionsOf($user)) {
|
if ($this->user()->canChangeOptionsOf($user)) {
|
||||||
$data = DataSetter::fromGetter(HTTPRequest::postData());
|
$data = HTTPRequest::postData()->toArray();
|
||||||
$fields->validate($data);
|
$fields->validate($data);
|
||||||
try {
|
try {
|
||||||
$this->updateUser($user, $data);
|
$this->updateUser($user, $data);
|
||||||
@ -144,7 +143,7 @@ class UsersController extends AbstractController
|
|||||||
return $this->admin()->redirect('/users/' . $user->username() . '/profile/');
|
return $this->admin()->redirect('/users/' . $user->username() . '/profile/');
|
||||||
}
|
}
|
||||||
|
|
||||||
$fields->validate(new DataGetter($user->toArray()));
|
$fields = $fields->validate($user);
|
||||||
|
|
||||||
$this->modal('changes');
|
$this->modal('changes');
|
||||||
|
|
||||||
@ -160,36 +159,36 @@ class UsersController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* Update user data from POST request
|
* Update user data from POST request
|
||||||
*/
|
*/
|
||||||
protected function updateUser(User $user, DataSetter $data): void
|
protected function updateUser(User $user, array $data): void
|
||||||
{
|
{
|
||||||
// Remove CSRF token from $data
|
// Remove CSRF token from $data
|
||||||
$data->remove('csrf-token');
|
unset($data['csrf-token']);
|
||||||
|
|
||||||
if (!empty($data->get('password'))) {
|
if (!empty($data['password'])) {
|
||||||
// Ensure that password can be changed
|
// Ensure that password can be changed
|
||||||
if (!$this->user()->canChangePasswordOf($user)) {
|
if (!$this->user()->canChangePasswordOf($user)) {
|
||||||
throw new TranslatedException(sprintf('Cannot change the password of %s', $user->username()), 'admin.users.user.cannot-change-password');
|
throw new TranslatedException(sprintf('Cannot change the password of %s', $user->username()), 'admin.users.user.cannot-change-password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash the new password
|
// Hash the new password
|
||||||
$data->set('hash', Password::hash($data->get('password')));
|
$data['hash'] = Password::hash($data['password']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove password from $data
|
// Remove password from $data
|
||||||
$data->remove('password');
|
unset($data['password']);
|
||||||
|
|
||||||
// Ensure that user role can be changed
|
// Ensure that user role can be changed
|
||||||
if ($data->get('role', $user->role()) !== $user->role() && !$this->user()->canChangeRoleOf($user)) {
|
if (($data['role'] ?? $user->role()) !== $user->role() && !$this->user()->canChangeRoleOf($user)) {
|
||||||
throw new TranslatedException(sprintf('Cannot change the role of %s', $user->username()), 'admin.users.user.cannot-change-role');
|
throw new TranslatedException(sprintf('Cannot change the role of %s', $user->username()), 'admin.users.user.cannot-change-role');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle incoming files
|
// Handle incoming files
|
||||||
if (HTTPRequest::hasFiles() && ($avatar = $this->uploadAvatar($user)) !== null) {
|
if (HTTPRequest::hasFiles() && ($avatar = $this->uploadAvatar($user)) !== null) {
|
||||||
$data->set('avatar', $avatar);
|
$data['avatar'] = $avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter empty elements from $data and merge them with $user ones
|
// Filter empty elements from $data and merge them with $user ones
|
||||||
$userData = array_merge($user->toArray(), $data->toArray());
|
$userData = array_merge($user->toArray(), $data);
|
||||||
|
|
||||||
YAML::encodeToFile($userData, Formwork::instance()->config()->get('admin.paths.accounts') . $user->username() . '.yml');
|
YAML::encodeToFile($userData, Formwork::instance()->config()->get('admin.paths.accounts') . $user->username() . '.yml');
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,11 @@
|
|||||||
namespace Formwork\Controllers;
|
namespace Formwork\Controllers;
|
||||||
|
|
||||||
use Formwork\Formwork;
|
use Formwork\Formwork;
|
||||||
use Formwork\Page;
|
use Formwork\Pages\Page;
|
||||||
use Formwork\Response\FileResponse;
|
use Formwork\Response\FileResponse;
|
||||||
use Formwork\Response\RedirectResponse;
|
use Formwork\Response\RedirectResponse;
|
||||||
use Formwork\Response\Response;
|
use Formwork\Response\Response;
|
||||||
use Formwork\Router\RouteParams;
|
use Formwork\Router\RouteParams;
|
||||||
use Formwork\Utils\Date;
|
|
||||||
use Formwork\Utils\FileSystem;
|
use Formwork\Utils\FileSystem;
|
||||||
|
|
||||||
class PageController extends AbstractController
|
class PageController extends AbstractController
|
||||||
@ -19,15 +18,16 @@ class PageController extends AbstractController
|
|||||||
|
|
||||||
$route = $params->get('page', $formwork->config()->get('pages.index'));
|
$route = $params->get('page', $formwork->config()->get('pages.index'));
|
||||||
|
|
||||||
if ($formwork->site()->has('aliases') && $alias = $formwork->site()->alias($route)) {
|
if ($resolvedAlias = $formwork->site()->resolveAlias($route)) {
|
||||||
$route = trim($alias, '/');
|
$route = $resolvedAlias;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($page = $formwork->site()->findPage($route)) {
|
if ($page = $formwork->site()->findPage($route)) {
|
||||||
if ($page->has('canonical')) {
|
if ($page->canonical() !== null) {
|
||||||
$canonical = trim($page->canonical(), '/');
|
$canonical = $page->canonical();
|
||||||
if ($params->get('page', '') !== $canonical) {
|
|
||||||
$route = empty($canonical) ? '' : $formwork->router()->rewrite(['page' => $canonical]);
|
if ($params->get('page', '/') !== $canonical) {
|
||||||
|
$route = $formwork->router()->rewrite(['page' => $canonical]);
|
||||||
return new RedirectResponse($formwork->site()->uri($route), 301);
|
return new RedirectResponse($formwork->site()->uri($route), 301);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,15 +37,15 @@ class PageController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($formwork->config()->get('cache.enabled') && ($page->has('publish-date') || $page->has('unpublish-date'))) {
|
if ($formwork->config()->get('cache.enabled') && ($page->has('publish-date') || $page->has('unpublish-date'))) {
|
||||||
if (($page->published() && !$formwork->site()->modifiedSince(Date::toTimestamp($page->get('publish-date'))))
|
if (($page->isPublished() && !$formwork->site()->modifiedSince($page->publishDate()->toTimestamp()))
|
||||||
|| (!$page->published() && !$formwork->site()->modifiedSince(Date::toTimestamp($page->get('unpublish-date'))))) {
|
|| (!$page->isPublished() && !$formwork->site()->modifiedSince($page->unpublishDate()->toTimestamp()))) {
|
||||||
// Clear cache if the site was not modified since the page has been published or unpublished
|
// Clear cache if the site was not modified since the page has been published or unpublished
|
||||||
$formwork->cache()->clear();
|
$formwork->cache()->clear();
|
||||||
FileSystem::touch($formwork->config()->get('content.path'));
|
FileSystem::touch($formwork->config()->get('content.path'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($page->routable() && $page->published()) {
|
if ($page->isPublished() && $page->routable()) {
|
||||||
return $this->getPageResponse($page);
|
return $this->getPageResponse($page);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -91,7 +91,7 @@ class PageController extends AbstractController
|
|||||||
$cache->delete($cacheKey);
|
$cache->delete($cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = new Response($page->renderToString(), (int) $page->get('response_status', 200), $page->headers());
|
$response = new Response($page->render(), $page->responseStatus(), $page->headers());
|
||||||
|
|
||||||
if ($config->get('cache.enabled') && $page->cacheable()) {
|
if ($config->get('cache.enabled') && $page->cacheable()) {
|
||||||
$cache->save($cacheKey, $response);
|
$cache->save($cacheKey, $response);
|
||||||
|
@ -167,6 +167,18 @@ abstract class AbstractCollection implements Arrayable, Countable, Iterator
|
|||||||
return Arr::keyOf($this->data, $value);
|
return Arr::keyOf($this->data, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the keys of all items
|
||||||
|
*/
|
||||||
|
public function keys(): array
|
||||||
|
{
|
||||||
|
if (!$this->isAssociative()) {
|
||||||
|
throw new LogicException('Only associative collections support keys');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys($this->data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return whether the collection contains the given value
|
* Return whether the collection contains the given value
|
||||||
*/
|
*/
|
||||||
@ -274,7 +286,7 @@ abstract class AbstractCollection implements Arrayable, Countable, Iterator
|
|||||||
public function map(callable $callback): static
|
public function map(callable $callback): static
|
||||||
{
|
{
|
||||||
$collection = $this->clone();
|
$collection = $this->clone();
|
||||||
$collection->data = Arr::map($callback, $collection->data);
|
$collection->data = Arr::map($collection->data, $callback);
|
||||||
return $collection;
|
return $collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,6 +340,62 @@ abstract class AbstractCollection implements Arrayable, Countable, Iterator
|
|||||||
return $collection;
|
return $collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group collection items using a callback
|
||||||
|
*/
|
||||||
|
public function group(callable $callback): array
|
||||||
|
{
|
||||||
|
return Arr::group($this->data, $callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value corresponding to the specified key from each item in the collection
|
||||||
|
*
|
||||||
|
* Typed collection should implement their own version of this method, optimised for their data type
|
||||||
|
*/
|
||||||
|
public function pluck(string $key, $default = null): array
|
||||||
|
{
|
||||||
|
return Arr::pluck($this->data, $key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the collection using the key from each item
|
||||||
|
*/
|
||||||
|
public function filterBy(string $key, $value = true, $default = null, bool $strict = null): static
|
||||||
|
{
|
||||||
|
$values = $this->pluck($key, $default);
|
||||||
|
|
||||||
|
if (is_callable($value)) {
|
||||||
|
$values = Arr::map($values, $value);
|
||||||
|
$comparison = true;
|
||||||
|
$strict ??= true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->filter(fn ($v, $k) => Constraint::isEqualTo($values[$k], $comparison ??= $value, $strict ??= false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort the collection using the given key from each item
|
||||||
|
*/
|
||||||
|
public function sortBy(
|
||||||
|
string $key,
|
||||||
|
int $direction = SORT_ASC,
|
||||||
|
int $type = SORT_NATURAL,
|
||||||
|
bool $caseSensitive = false,
|
||||||
|
bool $preserveKeys = true
|
||||||
|
): static {
|
||||||
|
return $this->sort($direction, $type, $this->pluck($key), $caseSensitive, $preserveKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group the collection using the given key from each item
|
||||||
|
*/
|
||||||
|
public function groupBy(string $key, $default = null): array
|
||||||
|
{
|
||||||
|
$values = $this->pluck($key, $default);
|
||||||
|
return $this->group(fn ($v, $k) => $values[$k]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a copy of the collection with the given values
|
* Return a copy of the collection with the given values
|
||||||
|
|
||||||
|
13
formwork/src/Data/Contracts/Paginable.php
Normal file
13
formwork/src/Data/Contracts/Paginable.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Data\Contracts;
|
||||||
|
|
||||||
|
use Formwork\Data\AbstractCollection;
|
||||||
|
use Formwork\Data\Pagination;
|
||||||
|
|
||||||
|
interface Paginable
|
||||||
|
{
|
||||||
|
public function pagination(): Pagination;
|
||||||
|
|
||||||
|
public function paginate(int $length, int $currentPage): AbstractCollection;
|
||||||
|
}
|
152
formwork/src/Data/Pagination.php
Normal file
152
formwork/src/Data/Pagination.php
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Data;
|
||||||
|
|
||||||
|
class Pagination
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Number of all items to paginate
|
||||||
|
*/
|
||||||
|
protected int $count = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of items in each pagination page
|
||||||
|
*/
|
||||||
|
protected int $length = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of pagination pages
|
||||||
|
*/
|
||||||
|
protected int $pages = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current pagination page
|
||||||
|
*/
|
||||||
|
protected int $currentPage = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Pagination instance
|
||||||
|
*/
|
||||||
|
public function __construct(AbstractCollection $collection, int $length)
|
||||||
|
{
|
||||||
|
$this->count = $collection->count();
|
||||||
|
|
||||||
|
$this->length = $length;
|
||||||
|
|
||||||
|
$this->pages = $this->count > 0 ? ceil($this->count / $this->length) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current page
|
||||||
|
*/
|
||||||
|
public function currentPage(): int
|
||||||
|
{
|
||||||
|
return $this->currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCurrentPage(int $currentPage): void
|
||||||
|
{
|
||||||
|
$this->currentPage = $currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pagination length
|
||||||
|
*/
|
||||||
|
public function length(): int
|
||||||
|
{
|
||||||
|
return $this->length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current pagination offset
|
||||||
|
*/
|
||||||
|
public function offset(): int
|
||||||
|
{
|
||||||
|
return ($this->currentPage - 1) * $this->length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pages(): int
|
||||||
|
{
|
||||||
|
return $this->pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether pagination has more than one page
|
||||||
|
*/
|
||||||
|
public function hasPages(): bool
|
||||||
|
{
|
||||||
|
return $this->pages > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether a given page number exists
|
||||||
|
*/
|
||||||
|
public function has(int $pageNumber): bool
|
||||||
|
{
|
||||||
|
return $pageNumber >= 1 && $pageNumber <= $this->pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether current page is the first
|
||||||
|
*/
|
||||||
|
public function isFirstPage(): bool
|
||||||
|
{
|
||||||
|
return $this->currentPage === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function firstPage(): int
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether current page is the last
|
||||||
|
*/
|
||||||
|
public function isLastPage(): bool
|
||||||
|
{
|
||||||
|
return $this->currentPage === $this->pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastPage(): int
|
||||||
|
{
|
||||||
|
return $this->pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether a previous page exists
|
||||||
|
*/
|
||||||
|
public function hasPreviousPage(): bool
|
||||||
|
{
|
||||||
|
return !$this->isFirstPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get previous pagination page number
|
||||||
|
*
|
||||||
|
* @return bool|int
|
||||||
|
*/
|
||||||
|
public function previousPage(): int
|
||||||
|
{
|
||||||
|
return ($previous = $this->currentPage - 1) > 0
|
||||||
|
? $previous
|
||||||
|
: $this->firstPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether a next page exists
|
||||||
|
*/
|
||||||
|
public function hasNextPage(): bool
|
||||||
|
{
|
||||||
|
return !$this->isLastPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next pagination page number
|
||||||
|
*/
|
||||||
|
public function nextPage(): int
|
||||||
|
{
|
||||||
|
return ($next = $this->currentPage + 1) <= $this->pages
|
||||||
|
? $next
|
||||||
|
: $this->lastPage();
|
||||||
|
}
|
||||||
|
}
|
@ -6,14 +6,24 @@ use Formwork\Data\Contracts\Arrayable;
|
|||||||
use Formwork\Data\Traits\DataArrayable;
|
use Formwork\Data\Traits\DataArrayable;
|
||||||
use Formwork\Data\Traits\DataMultipleGetter;
|
use Formwork\Data\Traits\DataMultipleGetter;
|
||||||
use Formwork\Data\Traits\DataMultipleSetter;
|
use Formwork\Data\Traits\DataMultipleSetter;
|
||||||
|
use Formwork\Formwork;
|
||||||
|
use Formwork\Traits\Methods;
|
||||||
|
use Formwork\Utils\Arr;
|
||||||
use Formwork\Utils\Constraint;
|
use Formwork\Utils\Constraint;
|
||||||
|
use Formwork\Utils\FileSystem;
|
||||||
|
use Formwork\Utils\Str;
|
||||||
use UnexpectedValueException;
|
use UnexpectedValueException;
|
||||||
|
|
||||||
class Field implements Arrayable
|
class Field implements Arrayable
|
||||||
{
|
{
|
||||||
use DataArrayable;
|
use DataArrayable;
|
||||||
use DataMultipleGetter;
|
use DataMultipleGetter {
|
||||||
|
get as protected baseGet;
|
||||||
|
}
|
||||||
use DataMultipleSetter;
|
use DataMultipleSetter;
|
||||||
|
use Methods;
|
||||||
|
|
||||||
|
protected const UNTRANSLATABLE_KEYS = ['name', 'type', 'value', 'default', 'translate', 'import'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field name
|
* Field name
|
||||||
@ -27,13 +37,16 @@ class Field implements Arrayable
|
|||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
|
|
||||||
if ($this->has('import')) {
|
if ($this->has('import')) {
|
||||||
$this->importData();
|
$this->importData();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->has('fields')) {
|
if ($this->has('fields')) {
|
||||||
$this->data['fields'] = new Fields($this->data['fields']);
|
throw new UnexpectedValueException('Fields may not have other fields inside');
|
||||||
}
|
}
|
||||||
Translator::translate($this);
|
|
||||||
|
$this->loadMethods();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,12 +62,7 @@ class Field implements Arrayable
|
|||||||
*/
|
*/
|
||||||
public function formName(): string
|
public function formName(): string
|
||||||
{
|
{
|
||||||
$segments = explode('.', $this->name);
|
return Str::dotNotationToBrackets($this->name());
|
||||||
$formName = array_shift($segments);
|
|
||||||
foreach ($segments as $segment) {
|
|
||||||
$formName .= '[' . $segment . ']';
|
|
||||||
}
|
|
||||||
return $formName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,7 +70,7 @@ class Field implements Arrayable
|
|||||||
*/
|
*/
|
||||||
public function type(): string
|
public function type(): string
|
||||||
{
|
{
|
||||||
return $this->get('type');
|
return $this->baseGet('type');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,7 +94,7 @@ class Field implements Arrayable
|
|||||||
*/
|
*/
|
||||||
public function value()
|
public function value()
|
||||||
{
|
{
|
||||||
return $this->get('value', $this->defaultValue());
|
return $this->baseGet('value', $this->defaultValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,7 +102,7 @@ class Field implements Arrayable
|
|||||||
*/
|
*/
|
||||||
public function defaultValue()
|
public function defaultValue()
|
||||||
{
|
{
|
||||||
return $this->get('default');
|
return $this->baseGet('default');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,12 +137,37 @@ class Field implements Arrayable
|
|||||||
return $this->is('visible', true);
|
return $this->is('visible', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isHidden(): bool
|
||||||
|
{
|
||||||
|
return $this->is('visible', false);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a value by key and return whether it is equal to boolean `true`
|
* Get a value by key and return whether it is equal to boolean `true`
|
||||||
*/
|
*/
|
||||||
public function is(string $key, bool $default = false): bool
|
public function is(string $key, bool $default = false): bool
|
||||||
{
|
{
|
||||||
return $this->get($key, $default) === true;
|
return $this->baseGet($key, $default) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key, $default = null)
|
||||||
|
{
|
||||||
|
$value = $this->baseGet($key, $default);
|
||||||
|
|
||||||
|
if ($this->isTranslatable($key)) {
|
||||||
|
return $this->translate($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadMethods()
|
||||||
|
{
|
||||||
|
$config = Formwork::instance()->config()->get('fields.path') . $this->get('type') . '.php';
|
||||||
|
|
||||||
|
if (FileSystem::exists($config)) {
|
||||||
|
$this->methods = include $config;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -146,29 +179,68 @@ class Field implements Arrayable
|
|||||||
if ($key === 'import') {
|
if ($key === 'import') {
|
||||||
throw new UnexpectedValueException('Invalid key for import');
|
throw new UnexpectedValueException('Invalid key for import');
|
||||||
}
|
}
|
||||||
|
|
||||||
$callback = explode('::', $value, 2);
|
$callback = explode('::', $value, 2);
|
||||||
|
|
||||||
if (!is_callable($callback)) {
|
if (!is_callable($callback)) {
|
||||||
throw new UnexpectedValueException(sprintf('Invalid import callback "%s"', $value));
|
throw new UnexpectedValueException(sprintf('Invalid import callback "%s"', $value));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->data[$key] = $callback();
|
$this->data[$key] = $callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __debugInfo(): array
|
/**
|
||||||
|
* Return whether a field key is translatable
|
||||||
|
*/
|
||||||
|
protected function isTranslatable(string $key): bool
|
||||||
{
|
{
|
||||||
$return['name'] = $this->name;
|
if (in_array($key, self::UNTRANSLATABLE_KEYS, true)) {
|
||||||
if ($this->has('type')) {
|
return false;
|
||||||
$return['type'] = $this->type();
|
|
||||||
}
|
}
|
||||||
if ($this->has('default')) {
|
|
||||||
$return['default'] = $this->defaultValue();
|
$translatable = $this->baseGet('translate', true);
|
||||||
|
|
||||||
|
if (is_array($translatable)) {
|
||||||
|
return in_array($key, $translatable, true);
|
||||||
}
|
}
|
||||||
if ($this->has('value')) {
|
|
||||||
$return['value'] = $this->value();
|
return $translatable;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function translate($value)
|
||||||
|
{
|
||||||
|
$translation = Formwork::instance()->translations()->getCurrent();
|
||||||
|
$language = $translation->code();
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
if (isset($value[$language])) {
|
||||||
|
$value = $value[$language];
|
||||||
|
}
|
||||||
|
} elseif (!is_string($value)) {
|
||||||
|
return $value;
|
||||||
}
|
}
|
||||||
if ($this->has('fields')) {
|
|
||||||
$return['fields'] = $this->get('fields');
|
$interpolate = fn ($value) => Str::interpolate($value, fn ($key) => $translation->translate($key));
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
return Arr::map($value, $interpolate);
|
||||||
}
|
}
|
||||||
return $return;
|
|
||||||
|
return $interpolate($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function callMethod(string $method, array $arguments = [])
|
||||||
|
{
|
||||||
|
return $this->methods[$method](...[$this, ...$arguments]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
if ($this->hasMethod('toString')) {
|
||||||
|
return $this->callMethod('toString');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $this->value();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
65
formwork/src/Fields/FieldCollection.php
Normal file
65
formwork/src/Fields/FieldCollection.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Fields;
|
||||||
|
|
||||||
|
use Formwork\Data\AbstractCollection;
|
||||||
|
use Formwork\Fields\Exceptions\ValidationException;
|
||||||
|
use Formwork\Fields\Layout\Layout;
|
||||||
|
use Formwork\Utils\Arr;
|
||||||
|
use Formwork\Utils\Constraint;
|
||||||
|
|
||||||
|
class FieldCollection extends AbstractCollection
|
||||||
|
{
|
||||||
|
protected bool $associative = true;
|
||||||
|
|
||||||
|
protected ?string $dataType = Field::class;
|
||||||
|
|
||||||
|
protected Layout $layout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new FieldCollection instance
|
||||||
|
*
|
||||||
|
* @param array $fields Array of Field objects
|
||||||
|
*/
|
||||||
|
public function __construct(array $fields, Layout $layout)
|
||||||
|
{
|
||||||
|
parent::__construct(Arr::map($fields, fn ($data, $name) => new Field($name, $data)));
|
||||||
|
|
||||||
|
$this->layout = $layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function layout(): Layout
|
||||||
|
{
|
||||||
|
return $this->layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pluck(string $key, $default = null): array
|
||||||
|
{
|
||||||
|
return $this->everyItem()->get($key, $default)->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate fields against data
|
||||||
|
*/
|
||||||
|
public function validate($data): self
|
||||||
|
{
|
||||||
|
$data = Arr::from($data);
|
||||||
|
|
||||||
|
foreach ($this->data as $field) {
|
||||||
|
$value = Arr::get($data, $field->name());
|
||||||
|
|
||||||
|
// TODO: move to field
|
||||||
|
if ($field->isRequired() && Constraint::isEmpty($value)) {
|
||||||
|
throw new ValidationException(sprintf('Required field "%s" of type "%s" cannot be empty', $field->name(), $field->type()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->hasMethod('validate')) {
|
||||||
|
$value = $field->validate($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$field->set('value', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
@ -1,90 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Formwork\Fields;
|
|
||||||
|
|
||||||
use Formwork\Data\AbstractCollection;
|
|
||||||
use Formwork\Data\DataGetter;
|
|
||||||
|
|
||||||
class Fields extends AbstractCollection
|
|
||||||
{
|
|
||||||
protected ?string $dataType = Field::class;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new Fields instance
|
|
||||||
*
|
|
||||||
* @param array $fields Array of Field objects
|
|
||||||
*/
|
|
||||||
public function __construct(array $fields)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
foreach ($fields as $key => $value) {
|
|
||||||
if ($value === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($value instanceof Field) {
|
|
||||||
if (is_int($key)) {
|
|
||||||
$key = $value->name();
|
|
||||||
}
|
|
||||||
$this->data[$key] = $value;
|
|
||||||
} else {
|
|
||||||
$this->data[$key] = new Field($key, $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively find a field by name
|
|
||||||
*
|
|
||||||
* @param string $field Field name
|
|
||||||
*/
|
|
||||||
public function find(string $field): ?Field
|
|
||||||
{
|
|
||||||
foreach ($this->data as $key => $value) {
|
|
||||||
if ($key === $field) {
|
|
||||||
return $this->data[$key];
|
|
||||||
}
|
|
||||||
if ($value->has('fields')) {
|
|
||||||
$found = $value->get('fields')->find($field);
|
|
||||||
if ($found !== null) {
|
|
||||||
return $found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert fields to array
|
|
||||||
*
|
|
||||||
* @param bool $flatten Whether to recursively convert Fields instances
|
|
||||||
*/
|
|
||||||
public function toArray(bool $flatten = false): array
|
|
||||||
{
|
|
||||||
if (!$flatten) {
|
|
||||||
return $this->data;
|
|
||||||
}
|
|
||||||
$result = [];
|
|
||||||
foreach ($this->data as $key => $value) {
|
|
||||||
if ($value->has('fields')) {
|
|
||||||
$result = array_merge($result, $value->get('fields')->toArray(true));
|
|
||||||
} else {
|
|
||||||
$result[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate fields against data
|
|
||||||
*/
|
|
||||||
public function validate(DataGetter $data): self
|
|
||||||
{
|
|
||||||
Validator::validate($this, $data);
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __debugInfo(): array
|
|
||||||
{
|
|
||||||
return $this->data;
|
|
||||||
}
|
|
||||||
}
|
|
32
formwork/src/Fields/Layout/Layout.php
Normal file
32
formwork/src/Fields/Layout/Layout.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Fields\Layout;
|
||||||
|
|
||||||
|
class Layout
|
||||||
|
{
|
||||||
|
protected string $type;
|
||||||
|
|
||||||
|
protected SectionCollection $sections;
|
||||||
|
|
||||||
|
public function __construct(array $data)
|
||||||
|
{
|
||||||
|
$this->type = $data['type'];
|
||||||
|
$this->sections = new SectionCollection($data['sections'] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get layout type
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function type(): string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get layout sections
|
||||||
|
*/
|
||||||
|
public function sections()
|
||||||
|
{
|
||||||
|
return $this->sections;
|
||||||
|
}
|
||||||
|
}
|
34
formwork/src/Fields/Layout/Section.php
Normal file
34
formwork/src/Fields/Layout/Section.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Fields\Layout;
|
||||||
|
|
||||||
|
use Formwork\Data\Traits\DataGetter;
|
||||||
|
use Formwork\Formwork;
|
||||||
|
use Formwork\Utils\Str;
|
||||||
|
|
||||||
|
class Section
|
||||||
|
{
|
||||||
|
use DataGetter;
|
||||||
|
|
||||||
|
public function __construct(array $data)
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value by key and return whether it is equal to boolean `true`
|
||||||
|
*/
|
||||||
|
public function is(string $key, bool $default = false): bool
|
||||||
|
{
|
||||||
|
return $this->get($key, $default) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field label
|
||||||
|
*/
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
$translation = Formwork::instance()->translations()->getCurrent();
|
||||||
|
return Str::interpolate($this->get('label', ''), fn ($key) => $translation->translate($key));
|
||||||
|
}
|
||||||
|
}
|
18
formwork/src/Fields/Layout/SectionCollection.php
Normal file
18
formwork/src/Fields/Layout/SectionCollection.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Fields\Layout;
|
||||||
|
|
||||||
|
use Formwork\Data\AbstractCollection;
|
||||||
|
use Formwork\Utils\Arr;
|
||||||
|
|
||||||
|
class SectionCollection extends AbstractCollection
|
||||||
|
{
|
||||||
|
protected bool $associative = true;
|
||||||
|
|
||||||
|
protected ?string $dataType = Section::class;
|
||||||
|
|
||||||
|
public function __construct(array $sections)
|
||||||
|
{
|
||||||
|
parent::__construct(Arr::map($sections, fn ($section) => new Section($section)));
|
||||||
|
}
|
||||||
|
}
|
@ -1,78 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Formwork\Fields;
|
|
||||||
|
|
||||||
use Formwork\Formwork;
|
|
||||||
|
|
||||||
class Translator
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Language string interpolation regex
|
|
||||||
*/
|
|
||||||
protected const INTERPOLATION_REGEX = '/^{{([\-._a-z]+)}}$/i';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fields not to translate
|
|
||||||
*/
|
|
||||||
protected const IGNORED_PROPERTIES = ['name', 'type', 'import', 'fields'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keys of which array value has to be ignored
|
|
||||||
*/
|
|
||||||
protected const IGNORED_ARRAY_KEYS = ['value', 'options'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate a field
|
|
||||||
*/
|
|
||||||
public static function translate(Field $field): void
|
|
||||||
{
|
|
||||||
$language = Formwork::instance()->translations()->getCurrent()->code();
|
|
||||||
foreach ($field->toArray() as $key => $value) {
|
|
||||||
if (static::isTranslatable($key, $field)) {
|
|
||||||
if (is_array($value)) {
|
|
||||||
if (isset($value[$language])) {
|
|
||||||
$value = $value[$language];
|
|
||||||
} elseif (!in_array($key, self::IGNORED_ARRAY_KEYS, true)) {
|
|
||||||
$value = array_shift($value);
|
|
||||||
}
|
|
||||||
} elseif (!is_string($value)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$field->set($key, static::interpolate($value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether a field key is translatable
|
|
||||||
*/
|
|
||||||
protected static function isTranslatable(string $key, Field $field): bool
|
|
||||||
{
|
|
||||||
if (in_array($key, self::IGNORED_PROPERTIES, true)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$translate = $field->get('translate', true);
|
|
||||||
if (is_array($translate)) {
|
|
||||||
return in_array($key, $translate, true);
|
|
||||||
}
|
|
||||||
return $translate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpolate a string
|
|
||||||
*
|
|
||||||
* @param array|string $value
|
|
||||||
*
|
|
||||||
* @return array|string
|
|
||||||
*/
|
|
||||||
protected static function interpolate($value)
|
|
||||||
{
|
|
||||||
if (is_array($value)) {
|
|
||||||
return array_map(static fn ($value) => static::interpolate($value), $value);
|
|
||||||
}
|
|
||||||
if (is_string($value) && (bool) preg_match(self::INTERPOLATION_REGEX, $value, $matches)) {
|
|
||||||
return Formwork::instance()->translations()->getCurrent()->translate($matches[1]);
|
|
||||||
}
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,296 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Formwork\Fields;
|
|
||||||
|
|
||||||
use Formwork\Data\Contracts\Arrayable;
|
|
||||||
use Formwork\Data\DataGetter;
|
|
||||||
use Formwork\Fields\Exceptions\ValidationException;
|
|
||||||
use Formwork\Utils\Constraint;
|
|
||||||
use Formwork\Utils\Date;
|
|
||||||
use Formwork\Utils\Str;
|
|
||||||
use InvalidArgumentException;
|
|
||||||
|
|
||||||
class Validator
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Field types ignored by the validator
|
|
||||||
*/
|
|
||||||
public const IGNORED_FIELDS = ['column', 'header', 'row', 'rows'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Date format used for date fields
|
|
||||||
*/
|
|
||||||
public const DATE_FORMAT = 'Y-m-d H:i:s';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate all Fields against given data
|
|
||||||
*/
|
|
||||||
public static function validate(Fields $fields, DataGetter $data): void
|
|
||||||
{
|
|
||||||
foreach ($fields as $field) {
|
|
||||||
if ($field->has('fields')) {
|
|
||||||
$field->get('fields')->validate($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($field->type(), self::IGNORED_FIELDS, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$value = $data->get($field->name());
|
|
||||||
|
|
||||||
if ($field->isRequired() && Constraint::isEmpty($value)) {
|
|
||||||
throw new ValidationException(sprintf('Required field "%s" of type "%s" cannot be empty', $field->name(), $field->type()));
|
|
||||||
}
|
|
||||||
|
|
||||||
$method = 'validate' . ucfirst(strtolower($field->type()));
|
|
||||||
|
|
||||||
if (method_exists(static::class, $method)) {
|
|
||||||
$value = static::$method($value, $field);
|
|
||||||
}
|
|
||||||
|
|
||||||
$field->set('value', $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "checkbox" fields
|
|
||||||
*/
|
|
||||||
public static function validateCheckbox($value, Field $field): bool
|
|
||||||
{
|
|
||||||
if (Constraint::isTruthy($value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Constraint::isFalsy($value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($value === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "togglegroup" fields
|
|
||||||
*/
|
|
||||||
public static function validateTogglegroup($value, Field $field)
|
|
||||||
{
|
|
||||||
if (Constraint::isTruthy($value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Constraint::isFalsy($value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return static::parse($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "date" fields
|
|
||||||
*/
|
|
||||||
public static function validateDate($value, Field $field): ?string
|
|
||||||
{
|
|
||||||
if (Constraint::isEmpty($value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return date(self::DATE_FORMAT, Date::toTimestamp($value));
|
|
||||||
} catch (InvalidArgumentException $e) {
|
|
||||||
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s":%s', $field->name(), $field->type(), Str::after($e->getMessage(), ':')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "number" fields
|
|
||||||
*
|
|
||||||
* @return float|int
|
|
||||||
*/
|
|
||||||
public static function validateNumber($value, Field $field)
|
|
||||||
{
|
|
||||||
$value = static::parse($value);
|
|
||||||
|
|
||||||
if (!is_numeric($value)) {
|
|
||||||
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($field->has('min') && $value < $field->get('min')) {
|
|
||||||
throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be greater than or equal to %d', $field->name(), $field->type(), $field->get('min')));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($field->has('max') && $value > $field->get('max')) {
|
|
||||||
throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be less than or equal to %d', $field->name(), $field->type(), $field->get('max')));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($field->has('step') && ($value - $field->get('min', 0)) % $field->get('step') !== 0) {
|
|
||||||
throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not conform to the step value %d', $field->name(), $field->value(), $field->get('step')));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "range" fields
|
|
||||||
*
|
|
||||||
* @return float|int
|
|
||||||
*/
|
|
||||||
public static function validateRange($value, Field $field)
|
|
||||||
{
|
|
||||||
return static::validateNumber($value, $field);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "select" fields
|
|
||||||
*/
|
|
||||||
public static function validateSelect($value, Field $field)
|
|
||||||
{
|
|
||||||
return static::parse($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "tags" fields
|
|
||||||
*/
|
|
||||||
public static function validateTags($value, Field $field): 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')));
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_values(array_filter($value));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "array" fields
|
|
||||||
*/
|
|
||||||
public static function validateArray($value, Field $field): array
|
|
||||||
{
|
|
||||||
if (Constraint::isEmpty($value)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($value instanceof Arrayable) {
|
|
||||||
$value = $value->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_array($value)) {
|
|
||||||
throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($field->get('associative')) {
|
|
||||||
foreach (array_keys($value) as $key) {
|
|
||||||
if (is_int($key)) {
|
|
||||||
unset($value[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_filter($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "text" fields
|
|
||||||
*/
|
|
||||||
public static function validateText($value, Field $field): 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($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($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($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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (string) $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "textarea" fields
|
|
||||||
*/
|
|
||||||
public static function validateTextarea($value, Field $field): string
|
|
||||||
{
|
|
||||||
return static::validateText($value, $field);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "email" fields
|
|
||||||
*/
|
|
||||||
public static function validateEmail($value, Field $field): string
|
|
||||||
{
|
|
||||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
|
||||||
throw new ValidationException(sprintf('The value of field "%s" of type "%s" is not a valid e-mail address', $field->name(), $field->value()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return static::validateText($value, $field);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "password" fields
|
|
||||||
*/
|
|
||||||
public static function validatePassword($value, Field $field): string
|
|
||||||
{
|
|
||||||
return static::validateText($value, $field);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "image" fields
|
|
||||||
*/
|
|
||||||
public static function validateImage($value, Field $field): ?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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate "duration" fields
|
|
||||||
*/
|
|
||||||
public static function validateDuration($value, Field $field)
|
|
||||||
{
|
|
||||||
return static::validateNumber($value, $field);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cast a value to its correct type
|
|
||||||
*/
|
|
||||||
protected static function parse($value)
|
|
||||||
{
|
|
||||||
if (is_numeric($value)) {
|
|
||||||
// This reliably casts numeric values to int or float
|
|
||||||
return $value + 0;
|
|
||||||
}
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,6 +6,7 @@ use Formwork\Admin\Admin;
|
|||||||
use Formwork\Admin\Statistics;
|
use Formwork\Admin\Statistics;
|
||||||
use Formwork\Cache\FilesCache;
|
use Formwork\Cache\FilesCache;
|
||||||
use Formwork\Languages\Languages;
|
use Formwork\Languages\Languages;
|
||||||
|
use Formwork\Pages\Site;
|
||||||
use Formwork\Parsers\PHP;
|
use Formwork\Parsers\PHP;
|
||||||
use Formwork\Parsers\YAML;
|
use Formwork\Parsers\YAML;
|
||||||
use Formwork\Router\Router;
|
use Formwork\Router\Router;
|
||||||
|
@ -2,30 +2,90 @@
|
|||||||
|
|
||||||
namespace Formwork\Metadata;
|
namespace Formwork\Metadata;
|
||||||
|
|
||||||
use Formwork\Data\AbstractCollection;
|
use Formwork\Utils\Str;
|
||||||
|
|
||||||
class Metadata extends AbstractCollection
|
class Metadata
|
||||||
{
|
{
|
||||||
protected bool $associative = true;
|
protected const HTTP_EQUIV_NAMES = ['content-type', 'default-style', 'refresh'];
|
||||||
|
|
||||||
protected ?string $dataType = Metadatum::class;
|
/**
|
||||||
|
* Metadata name
|
||||||
|
*/
|
||||||
|
protected string $name;
|
||||||
|
|
||||||
protected bool $mutable = true;
|
/**
|
||||||
|
* Metadata content
|
||||||
|
*/
|
||||||
|
protected string $content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata prefix
|
||||||
|
*/
|
||||||
|
protected string $prefix;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Metadata instance
|
* Create a new Metadata instance
|
||||||
*/
|
*/
|
||||||
public function __construct(array $data)
|
public function __construct(string $name, string $content)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
$this->name = strtolower($name);
|
||||||
$this->setMultiple($data);
|
$this->content = $content;
|
||||||
|
|
||||||
|
if ($prefix = Str::before($name, ':')) {
|
||||||
|
$this->prefix = $prefix;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a metadatum
|
* Return metadata name
|
||||||
*/
|
*/
|
||||||
public function set(string $key, $value)
|
public function name(): string
|
||||||
{
|
{
|
||||||
$this->data[$key] = new Metadatum($key, $value);
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the metadata is a charset declaration
|
||||||
|
*/
|
||||||
|
public function isCharset(): bool
|
||||||
|
{
|
||||||
|
return $this->name === 'charset';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the metadata is an http-equiv directive
|
||||||
|
*/
|
||||||
|
public function isHTTPEquiv(): bool
|
||||||
|
{
|
||||||
|
return in_array($this->name, self::HTTP_EQUIV_NAMES, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return metadata content
|
||||||
|
*/
|
||||||
|
public function content(): string
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return metadata prefix
|
||||||
|
*/
|
||||||
|
public function prefix(): ?string
|
||||||
|
{
|
||||||
|
return $this->prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the metadata has a prefix (e.g. 'twitter' for 'twitter:card', 'og' for 'og:image')
|
||||||
|
*/
|
||||||
|
public function hasPrefix(): bool
|
||||||
|
{
|
||||||
|
return $this->prefix !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->content();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
formwork/src/Metadata/MetadataCollection.php
Normal file
31
formwork/src/Metadata/MetadataCollection.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Metadata;
|
||||||
|
|
||||||
|
use Formwork\Data\AbstractCollection;
|
||||||
|
|
||||||
|
class MetadataCollection extends AbstractCollection
|
||||||
|
{
|
||||||
|
protected bool $associative = true;
|
||||||
|
|
||||||
|
protected ?string $dataType = Metadata::class;
|
||||||
|
|
||||||
|
protected bool $mutable = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Metadata instance
|
||||||
|
*/
|
||||||
|
public function __construct(array $data)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->setMultiple($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a metadata
|
||||||
|
*/
|
||||||
|
public function set(string $key, $value)
|
||||||
|
{
|
||||||
|
$this->data[$key] = new Metadata($key, $value);
|
||||||
|
}
|
||||||
|
}
|
@ -1,91 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Formwork\Metadata;
|
|
||||||
|
|
||||||
use Formwork\Utils\Str;
|
|
||||||
|
|
||||||
class Metadatum
|
|
||||||
{
|
|
||||||
protected const HTTP_EQUIV_NAMES = ['content-type', 'default-style', 'refresh'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadatum name
|
|
||||||
*/
|
|
||||||
protected string $name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadatum content
|
|
||||||
*/
|
|
||||||
protected string $content;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadatum prefix
|
|
||||||
*/
|
|
||||||
protected string $prefix;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new Metadatum instance
|
|
||||||
*/
|
|
||||||
public function __construct(string $name, string $content)
|
|
||||||
{
|
|
||||||
$this->name = strtolower($name);
|
|
||||||
$this->content = $content;
|
|
||||||
|
|
||||||
if ($prefix = Str::before($name, ':')) {
|
|
||||||
$this->prefix = $prefix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return metadatum name
|
|
||||||
*/
|
|
||||||
public function name(): string
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether the metadatum is a charset declaration
|
|
||||||
*/
|
|
||||||
public function isCharset(): bool
|
|
||||||
{
|
|
||||||
return $this->name === 'charset';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether the metadatum is an http-equiv directive
|
|
||||||
*/
|
|
||||||
public function isHTTPEquiv(): bool
|
|
||||||
{
|
|
||||||
return in_array($this->name, self::HTTP_EQUIV_NAMES, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return metadatum content
|
|
||||||
*/
|
|
||||||
public function content(): string
|
|
||||||
{
|
|
||||||
return $this->content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return metadatum prefix
|
|
||||||
*/
|
|
||||||
public function prefix(): ?string
|
|
||||||
{
|
|
||||||
return $this->prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether the metadatum has a prefix (e.g. 'twitter' for 'twitter:card', 'og' for 'og:image')
|
|
||||||
*/
|
|
||||||
public function hasPrefix(): bool
|
|
||||||
{
|
|
||||||
return $this->prefix !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString(): string
|
|
||||||
{
|
|
||||||
return $this->content();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,573 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Formwork;
|
|
||||||
|
|
||||||
use Formwork\Files\Files;
|
|
||||||
use Formwork\Metadata\Metadata;
|
|
||||||
use Formwork\Parsers\Markdown;
|
|
||||||
use Formwork\Parsers\YAML;
|
|
||||||
use Formwork\Schemes\Scheme;
|
|
||||||
use Formwork\Template\Template;
|
|
||||||
use Formwork\Utils\Date;
|
|
||||||
use Formwork\Utils\FileSystem;
|
|
||||||
use Formwork\Utils\Header;
|
|
||||||
use Formwork\Utils\Str;
|
|
||||||
use Formwork\Utils\Uri;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
class Page extends AbstractPage
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Page num regex
|
|
||||||
*/
|
|
||||||
public const NUM_REGEX = '/^(\d+)-/';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page 'published' status
|
|
||||||
*/
|
|
||||||
public const PAGE_STATUS_PUBLISHED = 'published';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page 'not published' status
|
|
||||||
*/
|
|
||||||
public const PAGE_STATUS_NOT_PUBLISHED = 'not-published';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page 'not routable' status
|
|
||||||
*/
|
|
||||||
public const PAGE_STATUS_NOT_ROUTABLE = 'not-routable';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page id
|
|
||||||
*
|
|
||||||
* @deprecated Use the name property
|
|
||||||
*/
|
|
||||||
protected string $id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page name (the name of the containing directory)
|
|
||||||
*/
|
|
||||||
protected string $name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page slug
|
|
||||||
*/
|
|
||||||
protected string $slug;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page language
|
|
||||||
*/
|
|
||||||
protected ?string $language = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Available page languages
|
|
||||||
*/
|
|
||||||
protected array $availableLanguages = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page filename
|
|
||||||
*/
|
|
||||||
protected string $filename;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page template
|
|
||||||
*/
|
|
||||||
protected Template $template;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page scheme
|
|
||||||
*/
|
|
||||||
protected Scheme $scheme;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page files
|
|
||||||
*/
|
|
||||||
protected Files $files;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unprocessed page frontmatter
|
|
||||||
*/
|
|
||||||
protected string $rawFrontmatter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page frontmatter
|
|
||||||
*/
|
|
||||||
protected array $frontmatter = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unprocessed page content
|
|
||||||
*/
|
|
||||||
protected string $rawContent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page content
|
|
||||||
*/
|
|
||||||
protected string $content;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unprocessed page summary
|
|
||||||
*/
|
|
||||||
protected string $rawSummary;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page summary
|
|
||||||
*/
|
|
||||||
protected string $summary;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unprocessed page body text
|
|
||||||
*/
|
|
||||||
protected string $rawBody;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page body text
|
|
||||||
*/
|
|
||||||
protected string $body;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page status
|
|
||||||
*/
|
|
||||||
protected string $status;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether page is published
|
|
||||||
*/
|
|
||||||
protected bool $published;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether page is routable
|
|
||||||
*/
|
|
||||||
protected bool $routable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether page is visible
|
|
||||||
*/
|
|
||||||
protected bool $visible;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether page is sortable
|
|
||||||
*/
|
|
||||||
protected bool $sortable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PageCollection containing page siblings
|
|
||||||
*/
|
|
||||||
protected PageCollection $siblings;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new Page instance
|
|
||||||
*/
|
|
||||||
public function __construct(string $path)
|
|
||||||
{
|
|
||||||
$this->path = FileSystem::normalizePath($path . DS);
|
|
||||||
$this->relativePath = Str::wrap(Str::removeStart($this->path, Formwork::instance()->site()->path()), DS);
|
|
||||||
$this->route = Uri::normalize(preg_replace('~[/\\\\](\d+-)~', '/', $this->relativePath));
|
|
||||||
$this->name = basename($this->relativePath);
|
|
||||||
$this->slug = basename($this->route);
|
|
||||||
$this->loadFiles();
|
|
||||||
if (!$this->isEmpty()) {
|
|
||||||
$this->parse();
|
|
||||||
$this->processData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return page default data
|
|
||||||
*/
|
|
||||||
public function defaults(): array
|
|
||||||
{
|
|
||||||
$defaults = [
|
|
||||||
'published' => true,
|
|
||||||
'routable' => true,
|
|
||||||
'visible' => true,
|
|
||||||
'searchable' => true,
|
|
||||||
'cacheable' => true,
|
|
||||||
'sortable' => true,
|
|
||||||
'headers' => [],
|
|
||||||
'metadata' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
// Merge with scheme default field values
|
|
||||||
$defaults = array_merge($defaults, $this->scheme->defaultFieldValues());
|
|
||||||
|
|
||||||
// If the page hasn't a num, by default it won't be visible
|
|
||||||
if ($this->num() === null) {
|
|
||||||
$defaults['visible'] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $defaults;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload page
|
|
||||||
*/
|
|
||||||
public function reload(): void
|
|
||||||
{
|
|
||||||
$vars = ['filename', 'template', 'rawContent', 'rawSummary', 'summary', 'rawBody', 'body', 'status', 'absoluteUri', 'lastModifiedTime', 'parent', 'parents', 'level', 'children', 'descendants', 'siblings'];
|
|
||||||
foreach ($vars as $var) {
|
|
||||||
unset($this->$var);
|
|
||||||
}
|
|
||||||
$this->__construct($this->path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether page is empty
|
|
||||||
*/
|
|
||||||
public function isEmpty(): bool
|
|
||||||
{
|
|
||||||
return !isset($this->filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function lastModifiedTime(): int
|
|
||||||
{
|
|
||||||
return max(FileSystem::lastModifiedTime($this->path . $this->filename), parent::lastModifiedTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get page num
|
|
||||||
*/
|
|
||||||
public function num(): ?int
|
|
||||||
{
|
|
||||||
preg_match(self::NUM_REGEX, $this->name, $matches);
|
|
||||||
return isset($matches[1]) ? (int) $matches[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether this is the currently active page
|
|
||||||
*/
|
|
||||||
public function isCurrent(): bool
|
|
||||||
{
|
|
||||||
return Formwork::instance()->site()->currentPage() === $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function date(string $format = null): string
|
|
||||||
{
|
|
||||||
$format ??= Formwork::instance()->config()->get('date.format');
|
|
||||||
if ($this->has('publish-date')) {
|
|
||||||
return date($format, Date::toTimestamp($this->data['publish-date']));
|
|
||||||
}
|
|
||||||
return parent::date($format);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get page status
|
|
||||||
*/
|
|
||||||
public function status(): string
|
|
||||||
{
|
|
||||||
if (isset($this->status)) {
|
|
||||||
return $this->status;
|
|
||||||
}
|
|
||||||
if ($this->published()) {
|
|
||||||
$status = self::PAGE_STATUS_PUBLISHED;
|
|
||||||
}
|
|
||||||
if (!$this->routable()) {
|
|
||||||
$status = self::PAGE_STATUS_NOT_ROUTABLE;
|
|
||||||
}
|
|
||||||
if (!$this->published()) {
|
|
||||||
$status = self::PAGE_STATUS_NOT_PUBLISHED;
|
|
||||||
}
|
|
||||||
return $this->status = $status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a PageCollection containing page siblings
|
|
||||||
*/
|
|
||||||
public function siblings(): PageCollection
|
|
||||||
{
|
|
||||||
if (isset($this->siblings)) {
|
|
||||||
return $this->siblings;
|
|
||||||
}
|
|
||||||
$parentPath = dirname($this->path) . DS;
|
|
||||||
return $this->siblings = PageCollection::fromPath($parentPath)->without($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether page has siblings
|
|
||||||
*/
|
|
||||||
public function hasSiblings(): bool
|
|
||||||
{
|
|
||||||
return !$this->siblings()->isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function isSite(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function isIndexPage(): bool
|
|
||||||
{
|
|
||||||
return trim($this->route(), '/') === Formwork::instance()->config()->get('pages.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function isErrorPage(): bool
|
|
||||||
{
|
|
||||||
return trim($this->route(), '/') === Formwork::instance()->config()->get('pages.error');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function isDeletable(): bool
|
|
||||||
{
|
|
||||||
return !($this->hasChildren() || $this->isSite() || $this->isIndexPage() || $this->isErrorPage());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function metadata(): Metadata
|
|
||||||
{
|
|
||||||
if (isset($this->metadata)) {
|
|
||||||
return $this->metadata;
|
|
||||||
}
|
|
||||||
$metadata = clone Formwork::instance()->site()->metadata();
|
|
||||||
$metadata->setMultiple($this->data['metadata']);
|
|
||||||
return $this->metadata = $metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether page has a language
|
|
||||||
*/
|
|
||||||
public function hasLanguage(string $language): bool
|
|
||||||
{
|
|
||||||
return in_array($language, $this->availableLanguages, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set page language
|
|
||||||
*/
|
|
||||||
public function setLanguage(string $language): void
|
|
||||||
{
|
|
||||||
if (!$this->hasLanguage($language)) {
|
|
||||||
throw new RuntimeException(sprintf('Invalid page language "%s"', $language));
|
|
||||||
}
|
|
||||||
$this->language = $language;
|
|
||||||
$this->__construct($this->path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a Files collection containing only images
|
|
||||||
*/
|
|
||||||
public function images(): Files
|
|
||||||
{
|
|
||||||
return $this->files()->filterByType('image');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render page to string
|
|
||||||
*
|
|
||||||
* @param array $vars Variables to pass to the page
|
|
||||||
*/
|
|
||||||
public function renderToString(array $vars = []): string
|
|
||||||
{
|
|
||||||
return $this->template()->render(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render page and return rendered content
|
|
||||||
*
|
|
||||||
* @param array $vars Variables to pass to the page
|
|
||||||
* @param bool $sendHeaders Whether to send headers before rendering
|
|
||||||
*/
|
|
||||||
public function render(array $vars = [], bool $sendHeaders = true): string
|
|
||||||
{
|
|
||||||
if ($sendHeaders) {
|
|
||||||
$this->sendHeaders();
|
|
||||||
}
|
|
||||||
echo $renderedPage = $this->renderToString($vars);
|
|
||||||
return $renderedPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an array containing page data
|
|
||||||
*/
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'route' => $this->route(),
|
|
||||||
'uri' => $this->uri(),
|
|
||||||
'name' => $this->name(),
|
|
||||||
'slug' => $this->slug(),
|
|
||||||
'template' => $this->template()->name(),
|
|
||||||
'num' => $this->num(),
|
|
||||||
'data' => $this->data()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return raw content
|
|
||||||
*/
|
|
||||||
public function rawContent(): string
|
|
||||||
{
|
|
||||||
if (isset($this->rawContent)) {
|
|
||||||
return $this->rawContent;
|
|
||||||
}
|
|
||||||
return $this->rawContent = str_replace("\r\n", "\n", empty($this->rawSummary) ? $this->rawBody : $this->rawSummary . "\n\n===\n\n" . $this->rawBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return summary text
|
|
||||||
*/
|
|
||||||
public function summary(): string
|
|
||||||
{
|
|
||||||
if (isset($this->summary)) {
|
|
||||||
return $this->summary;
|
|
||||||
}
|
|
||||||
return $this->summary = Markdown::parse($this->rawSummary, ['baseRoute' => $this->route]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return body text
|
|
||||||
*/
|
|
||||||
public function body(): string
|
|
||||||
{
|
|
||||||
if (isset($this->body)) {
|
|
||||||
return $this->body;
|
|
||||||
}
|
|
||||||
return $this->body = Markdown::parse($this->rawBody, ['baseRoute' => $this->route]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return page content (summary and body text)
|
|
||||||
*/
|
|
||||||
public function content(): string
|
|
||||||
{
|
|
||||||
return $this->summary() . $this->body();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load files related to page
|
|
||||||
*/
|
|
||||||
protected function loadFiles(): void
|
|
||||||
{
|
|
||||||
$contentFiles = [];
|
|
||||||
$files = [];
|
|
||||||
|
|
||||||
foreach (FileSystem::listFiles($this->path) as $file) {
|
|
||||||
$name = FileSystem::name($file);
|
|
||||||
$extension = '.' . FileSystem::extension($file);
|
|
||||||
if ($extension === Formwork::instance()->config()->get('content.extension')) {
|
|
||||||
$language = null;
|
|
||||||
if (preg_match('/([a-z0-9]+)\.([a-z]+)/', $name, $matches)) {
|
|
||||||
// Parse double extension
|
|
||||||
[$match, $name, $language] = $matches;
|
|
||||||
}
|
|
||||||
if (Formwork::instance()->site()->hasTemplate($name)) {
|
|
||||||
$contentFiles[$language] = [
|
|
||||||
'filename' => $file,
|
|
||||||
'template' => $name
|
|
||||||
];
|
|
||||||
if ($language !== null && !in_array($language, $this->availableLanguages, true)) {
|
|
||||||
$this->availableLanguages[] = $language;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif (in_array($extension, Formwork::instance()->config()->get('files.allowed_extensions'), true)) {
|
|
||||||
$files[] = $file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($contentFiles)) {
|
|
||||||
// Get correct content file based on current language
|
|
||||||
ksort($contentFiles);
|
|
||||||
$currentLanguage = $this->language ?: Formwork::instance()->site()->languages()->current();
|
|
||||||
$key = isset($contentFiles[$currentLanguage]) ? $currentLanguage : array_keys($contentFiles)[0];
|
|
||||||
|
|
||||||
// Set actual language
|
|
||||||
$this->language = $key ?: null;
|
|
||||||
|
|
||||||
$this->filename = $contentFiles[$key]['filename'];
|
|
||||||
$this->template = new Template($contentFiles[$key]['template'], $this);
|
|
||||||
$this->scheme = Formwork::instance()->schemes()->get('pages', $this->template);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->files = Files::fromPath($this->path, $files);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse page content
|
|
||||||
*/
|
|
||||||
protected function parse(): void
|
|
||||||
{
|
|
||||||
$contents = FileSystem::read($this->path . $this->filename);
|
|
||||||
if (!preg_match('/(?:\s|^)-{3}\s*(.+?)\s*-{3}\s*(?:(.+?)\s+={3}\s+)?(.*?)\s*$/s', $contents, $matches)) {
|
|
||||||
throw new RuntimeException('Invalid page format');
|
|
||||||
}
|
|
||||||
[$match, $this->rawFrontmatter, $this->rawSummary, $this->rawBody] = $matches;
|
|
||||||
$this->frontmatter = YAML::parse($this->rawFrontmatter);
|
|
||||||
$this->data = array_merge($this->defaults(), $this->frontmatter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process page data
|
|
||||||
*/
|
|
||||||
protected function processData(): void
|
|
||||||
{
|
|
||||||
$this->published = $this->data['published'];
|
|
||||||
|
|
||||||
if ($this->has('publish-date')) {
|
|
||||||
$this->published = $this->published && Date::toTimestamp($this->get('publish-date')) < time();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->has('unpublish-date')) {
|
|
||||||
$this->published = $this->published && Date::toTimestamp($this->get('unpublish-date')) > time();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->routable = $this->data['routable'];
|
|
||||||
|
|
||||||
$this->visible = $this->data['visible'];
|
|
||||||
|
|
||||||
// If the page isn't published, it won't also be visible
|
|
||||||
if (!$this->published) {
|
|
||||||
$this->visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->sortable = $this->data['sortable'];
|
|
||||||
|
|
||||||
if ($this->num() === null || $this->scheme->get('num') === 'date') {
|
|
||||||
$this->sortable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default 404 Not Found status to error page
|
|
||||||
if ($this->isErrorPage() && !$this->has('response_status')) {
|
|
||||||
$this->set('response_status', 404);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send page headers
|
|
||||||
*/
|
|
||||||
protected function sendHeaders(): void
|
|
||||||
{
|
|
||||||
if ($this->has('response_status')) {
|
|
||||||
Header::status((int) $this->get('response_status'));
|
|
||||||
}
|
|
||||||
foreach ($this->headers() as $name => $value) {
|
|
||||||
Header::send($name, $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString(): string
|
|
||||||
{
|
|
||||||
return $this->name();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __debugInfo(): array
|
|
||||||
{
|
|
||||||
return $this->toArray();
|
|
||||||
}
|
|
||||||
}
|
|
464
formwork/src/Pages/Page.php
Normal file
464
formwork/src/Pages/Page.php
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Pages;
|
||||||
|
|
||||||
|
use Formwork\Data\Contracts\Arrayable;
|
||||||
|
use Formwork\Fields\FieldCollection;
|
||||||
|
use Formwork\Files\Files;
|
||||||
|
use Formwork\Formwork;
|
||||||
|
use Formwork\Metadata\MetadataCollection;
|
||||||
|
use Formwork\Pages\Traits\PageStatus;
|
||||||
|
use Formwork\Pages\Traits\PageTraversal;
|
||||||
|
use Formwork\Pages\Traits\PageUid;
|
||||||
|
use Formwork\Pages\Traits\PageUri;
|
||||||
|
use Formwork\Parsers\YAML;
|
||||||
|
use Formwork\Schemes\Scheme;
|
||||||
|
use Formwork\Pages\Templates\Template;
|
||||||
|
use Formwork\Pages\Traits\PageData;
|
||||||
|
use Formwork\Utils\Arr;
|
||||||
|
use Formwork\Utils\Date;
|
||||||
|
use Formwork\Utils\FileSystem;
|
||||||
|
use Formwork\Utils\Path;
|
||||||
|
use Formwork\Utils\Str;
|
||||||
|
use Formwork\Utils\Uri;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class Page implements Arrayable
|
||||||
|
{
|
||||||
|
use PageData;
|
||||||
|
use PageStatus;
|
||||||
|
use PageTraversal;
|
||||||
|
use PageUid;
|
||||||
|
use PageUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page num regex
|
||||||
|
*/
|
||||||
|
public const NUM_REGEX = '/^(\d+)-/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page 'published' status
|
||||||
|
*/
|
||||||
|
public const PAGE_STATUS_PUBLISHED = 'published';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page 'not published' status
|
||||||
|
*/
|
||||||
|
public const PAGE_STATUS_NOT_PUBLISHED = 'not-published';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page 'not routable' status
|
||||||
|
*/
|
||||||
|
public const PAGE_STATUS_NOT_ROUTABLE = 'not-routable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page path relative to content path
|
||||||
|
*/
|
||||||
|
protected string $relativePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page unique identifier
|
||||||
|
*/
|
||||||
|
protected string $uid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page route
|
||||||
|
*/
|
||||||
|
protected string $route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page data
|
||||||
|
*/
|
||||||
|
protected array $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page uri
|
||||||
|
*/
|
||||||
|
protected string $uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page absolute uri
|
||||||
|
*/
|
||||||
|
protected string $absoluteUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page last modified time
|
||||||
|
*/
|
||||||
|
protected int $lastModifiedTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page modified date
|
||||||
|
*/
|
||||||
|
protected string $timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page name (the name of the containing directory)
|
||||||
|
*/
|
||||||
|
protected string $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page slug
|
||||||
|
*/
|
||||||
|
protected string $slug;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page language
|
||||||
|
*/
|
||||||
|
protected ?string $language;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available page languages
|
||||||
|
*/
|
||||||
|
protected array $availableLanguages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page filename
|
||||||
|
*/
|
||||||
|
protected string $filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page template
|
||||||
|
*/
|
||||||
|
protected Template $template;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page scheme
|
||||||
|
*/
|
||||||
|
protected Scheme $scheme;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page files
|
||||||
|
*/
|
||||||
|
protected Files $files;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page frontmatter
|
||||||
|
*/
|
||||||
|
protected array $frontmatter;
|
||||||
|
|
||||||
|
protected FieldCollection $fields;
|
||||||
|
|
||||||
|
protected ?string $canonical;
|
||||||
|
|
||||||
|
protected MetadataCollection $metadata;
|
||||||
|
|
||||||
|
protected int $responseStatus;
|
||||||
|
|
||||||
|
protected ?int $num;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Page instance
|
||||||
|
*/
|
||||||
|
public function __construct(string $path)
|
||||||
|
{
|
||||||
|
$this->path = FileSystem::normalizePath($path . DS);
|
||||||
|
|
||||||
|
$this->relativePath = Str::prepend(Path::makeRelative($this->path, Formwork::instance()->site()->path(), DS), DS);
|
||||||
|
|
||||||
|
$this->route = Uri::normalize(preg_replace('~[/\\\\](\d+-)~', '/', $this->relativePath));
|
||||||
|
|
||||||
|
$this->name = basename($this->relativePath);
|
||||||
|
|
||||||
|
$this->slug = basename($this->route);
|
||||||
|
|
||||||
|
$this->language = null;
|
||||||
|
|
||||||
|
$this->availableLanguages = [];
|
||||||
|
|
||||||
|
$this->loadFiles();
|
||||||
|
|
||||||
|
if (!$this->isEmpty()) {
|
||||||
|
$this->loadContents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return page default data
|
||||||
|
*/
|
||||||
|
public function defaults(): array
|
||||||
|
{
|
||||||
|
$defaults = [
|
||||||
|
'published' => true,
|
||||||
|
'routable' => true,
|
||||||
|
'listed' => true,
|
||||||
|
'searchable' => true,
|
||||||
|
'cacheable' => true,
|
||||||
|
'orderable' => true,
|
||||||
|
'canonical' => null,
|
||||||
|
'headers' => [],
|
||||||
|
'responseStatus' => 200,
|
||||||
|
'metadata' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
// Merge with scheme default field values
|
||||||
|
$defaults = array_merge($defaults, Arr::reject($this->fields()->pluck('default'), fn ($value) => $value === null));
|
||||||
|
|
||||||
|
// If the page hasn't a num, by default it won't be listed
|
||||||
|
if ($this->num() === null) {
|
||||||
|
$defaults['listed'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the page hasn't a num or numbering is 'date', by default it won't be orderable
|
||||||
|
if ($this->num() === null || $this->scheme->get('num') === 'date') {
|
||||||
|
$defaults['orderable'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canonical(): ?string
|
||||||
|
{
|
||||||
|
if (isset($this->canonical)) {
|
||||||
|
return $this->canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->canonical = !empty($this->data['canonical'])
|
||||||
|
? Path::normalize($this->data['canonical'])
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function metadata(): MetadataCollection
|
||||||
|
{
|
||||||
|
if (isset($this->metadata)) {
|
||||||
|
return $this->metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata = Formwork::instance()->site()->metadata()->clone();
|
||||||
|
$metadata->setMultiple($this->data['metadata']);
|
||||||
|
return $this->metadata = $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function responseStatus(): int
|
||||||
|
{
|
||||||
|
if (isset($this->responseStatus)) {
|
||||||
|
return $this->responseStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize response status
|
||||||
|
$this->responseStatus = (int) $this->data['responseStatus'];
|
||||||
|
|
||||||
|
// Set default 404 Not Found status to error page
|
||||||
|
if ($this->isErrorPage() && $this->responseStatus() === 200 && !isset($this->frontmatter['responseStatus'])) {
|
||||||
|
$this->responseStatus = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->responseStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastModifiedTime(): int
|
||||||
|
{
|
||||||
|
if (isset($this->lastModifiedTime)) {
|
||||||
|
return $this->lastModifiedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->lastModifiedTime = FileSystem::lastModifiedTime($this->path . $this->filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function timestamp(): int
|
||||||
|
{
|
||||||
|
if (isset($this->timestamp)) {
|
||||||
|
return $this->timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->timestamp = isset($this->data['publishDate'])
|
||||||
|
? Date::toTimestamp($this->data['publishDate'])
|
||||||
|
: $this->lastModifiedTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page num
|
||||||
|
*/
|
||||||
|
public function num(): ?int
|
||||||
|
{
|
||||||
|
if (isset($this->num)) {
|
||||||
|
return $this->num;
|
||||||
|
}
|
||||||
|
|
||||||
|
preg_match(self::NUM_REGEX, $this->name, $matches);
|
||||||
|
return $this->num = isset($matches[1]) ? (int) $matches[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set page language
|
||||||
|
*/
|
||||||
|
public function setLanguage(string $language): void
|
||||||
|
{
|
||||||
|
if (!$this->hasLanguage($language)) {
|
||||||
|
throw new RuntimeException(sprintf('Invalid page language "%s"', $language));
|
||||||
|
}
|
||||||
|
$this->resetProperties();
|
||||||
|
$this->language = $language;
|
||||||
|
$this->__construct($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function files(): Files
|
||||||
|
{
|
||||||
|
return $this->files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a Files collection containing only images
|
||||||
|
*/
|
||||||
|
public function images(): Files
|
||||||
|
{
|
||||||
|
return $this->files()->filterByType('image');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render page to string
|
||||||
|
*/
|
||||||
|
public function render(): string
|
||||||
|
{
|
||||||
|
return $this->template()->render(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether page is empty
|
||||||
|
*/
|
||||||
|
public function isEmpty(): bool
|
||||||
|
{
|
||||||
|
return !isset($this->filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPublished(): bool
|
||||||
|
{
|
||||||
|
return $this->status() === self::PAGE_STATUS_PUBLISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether this is the currently active page
|
||||||
|
*/
|
||||||
|
public function isCurrent(): bool
|
||||||
|
{
|
||||||
|
return Formwork::instance()->site()->currentPage() === $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSite(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isIndexPage(): bool
|
||||||
|
{
|
||||||
|
return trim($this->route(), '/') === Formwork::instance()->config()->get('pages.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isErrorPage(): bool
|
||||||
|
{
|
||||||
|
return trim($this->route(), '/') === Formwork::instance()->config()->get('pages.error');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDeletable(): bool
|
||||||
|
{
|
||||||
|
return !($this->hasChildren() || $this->isIndexPage() || $this->isErrorPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasLanguage(string $language): bool
|
||||||
|
{
|
||||||
|
return in_array($language, $this->availableLanguages, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload page
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function reload(): void
|
||||||
|
{
|
||||||
|
$path = $this->path;
|
||||||
|
$this->resetProperties();
|
||||||
|
$this->__construct($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load files related to page
|
||||||
|
*/
|
||||||
|
protected function loadFiles(): void
|
||||||
|
{
|
||||||
|
$contentFiles = [];
|
||||||
|
$files = [];
|
||||||
|
|
||||||
|
foreach (FileSystem::listFiles($this->path) as $file) {
|
||||||
|
$name = FileSystem::name($file);
|
||||||
|
|
||||||
|
$extension = '.' . FileSystem::extension($file);
|
||||||
|
|
||||||
|
if ($extension === Formwork::instance()->config()->get('content.extension')) {
|
||||||
|
$language = null;
|
||||||
|
|
||||||
|
if (preg_match('/([a-z0-9]+)\.([a-z]+)/', $name, $matches)) {
|
||||||
|
// Parse double extension
|
||||||
|
[$match, $name, $language] = $matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Formwork::instance()->site()->templates()->has($name)) {
|
||||||
|
$contentFiles[$language] = [
|
||||||
|
'filename' => $file,
|
||||||
|
'template' => $name
|
||||||
|
];
|
||||||
|
if ($language !== null && !in_array($language, $this->availableLanguages, true)) {
|
||||||
|
$this->availableLanguages[] = $language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (in_array($extension, Formwork::instance()->config()->get('files.allowed_extensions'), true)) {
|
||||||
|
$files[] = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($contentFiles)) {
|
||||||
|
// Get correct content file based on current language
|
||||||
|
ksort($contentFiles);
|
||||||
|
|
||||||
|
$currentLanguage = $this->language ?? Formwork::instance()->site()->languages()->current();
|
||||||
|
|
||||||
|
$key = isset($contentFiles[$currentLanguage]) ? $currentLanguage : array_keys($contentFiles)[0];
|
||||||
|
|
||||||
|
// Set actual language
|
||||||
|
$this->language = $key ?: null;
|
||||||
|
|
||||||
|
$this->filename = $contentFiles[$key]['filename'];
|
||||||
|
|
||||||
|
$this->template = new Template($contentFiles[$key]['template'], $this);
|
||||||
|
|
||||||
|
$this->scheme = Formwork::instance()->schemes()->get('pages', $this->template);
|
||||||
|
|
||||||
|
$this->fields = $this->scheme()->fields();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->files = Files::fromPath($this->path, $files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse page content
|
||||||
|
*/
|
||||||
|
protected function loadContents(): void
|
||||||
|
{
|
||||||
|
$contents = FileSystem::read($this->path . $this->filename);
|
||||||
|
|
||||||
|
if (!preg_match('/(?:\s|^)-{3}\s*(.+?)\s*-{3}\s*(.*?)\s*$/s', $contents, $matches)) {
|
||||||
|
throw new RuntimeException('Invalid page format');
|
||||||
|
}
|
||||||
|
|
||||||
|
[, $rawFrontmatter, $rawContent] = $matches;
|
||||||
|
|
||||||
|
$this->frontmatter = YAML::parse($rawFrontmatter);
|
||||||
|
|
||||||
|
$rawContent = str_replace("\r\n", "\n", $rawContent);
|
||||||
|
|
||||||
|
$this->data = array_merge($this->defaults(), $this->frontmatter, ['content' => $rawContent]);
|
||||||
|
|
||||||
|
$this->fields->validate($this->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resetProperties(): void
|
||||||
|
{
|
||||||
|
foreach (array_keys(get_class_vars(static::class)) as $property) {
|
||||||
|
unset($this->$property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->title() ?? $this->slug();
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Formwork;
|
namespace Formwork\Pages;
|
||||||
|
|
||||||
use Formwork\Data\AbstractCollection;
|
use Formwork\Data\AbstractCollection;
|
||||||
use Formwork\Data\Collection;
|
use Formwork\Data\Contracts\Paginable;
|
||||||
|
use Formwork\Formwork;
|
||||||
use Formwork\Utils\FileSystem;
|
use Formwork\Utils\FileSystem;
|
||||||
use Formwork\Utils\Str;
|
use Formwork\Utils\Str;
|
||||||
|
|
||||||
class PageCollection extends AbstractCollection
|
class PageCollection extends AbstractCollection implements Paginable
|
||||||
{
|
{
|
||||||
/**
|
protected ?string $dataType = Page::class . '|' . Site::class;
|
||||||
* Default property used to sort pages
|
|
||||||
*/
|
|
||||||
protected const DEFAULT_SORT_PROPERTY = 'relativePath';
|
|
||||||
|
|
||||||
protected ?string $dataType = AbstractPage::class;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination related to the collection
|
* Pagination related to the collection
|
||||||
@ -34,67 +30,29 @@ class PageCollection extends AbstractCollection
|
|||||||
*
|
*
|
||||||
* @param int $length Number of items in the pagination
|
* @param int $length Number of items in the pagination
|
||||||
*/
|
*/
|
||||||
public function paginate(int $length): static
|
public function paginate(int $length, int $currentPage): self
|
||||||
{
|
{
|
||||||
$pagination = new Pagination($this->count(), $length);
|
$pagination = new Pagination($this, $length);
|
||||||
|
$pagination->setCurrentPage($currentPage);
|
||||||
|
|
||||||
$pageCollection = $this->slice($pagination->offset(), $pagination->length());
|
$pageCollection = $this->slice($pagination->offset(), $pagination->length());
|
||||||
$pageCollection->pagination = $pagination;
|
$pageCollection->pagination = $pagination;
|
||||||
return $pageCollection;
|
return $pageCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function pluck(string $key, $default = null): array
|
||||||
* Return an array containing the specified property of each collection item
|
|
||||||
*/
|
|
||||||
public function pluck(string $property): array
|
|
||||||
{
|
{
|
||||||
$result = [];
|
return $this->everyItem()->get($key, $default)->toArray();
|
||||||
|
|
||||||
foreach ($this->data as $page) {
|
|
||||||
$result[] = $page->get($property);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function listed(): static
|
||||||
* Filter collection items
|
|
||||||
*
|
|
||||||
* @param string $property Property to find in filtered items
|
|
||||||
* @param $value Value to check in filtered items (default: true)
|
|
||||||
* @param callable $process Callable to process items before filtering
|
|
||||||
*/
|
|
||||||
public function filterBy(string $property, $value = true, callable $process = null): static
|
|
||||||
{
|
{
|
||||||
return $this->filter(static function (Page $item) use ($property, $value, $process): bool {
|
return $this->filterBy('listed');
|
||||||
if ($item->has($property)) {
|
|
||||||
$propertyValue = $item->get($property);
|
|
||||||
|
|
||||||
if (is_callable($process)) {
|
|
||||||
$propertyValue = is_array($propertyValue) ? array_map($process, $propertyValue) : $process($propertyValue);
|
|
||||||
$value = $process($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($propertyValue)) {
|
|
||||||
return in_array($value, $propertyValue);
|
|
||||||
}
|
|
||||||
return $propertyValue == $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function published(): static
|
||||||
* Sort collection items
|
{
|
||||||
*/
|
return $this->filterBy('status', 'published');
|
||||||
public function sortBy(
|
|
||||||
string $property = self::DEFAULT_SORT_PROPERTY,
|
|
||||||
int $direction = SORT_ASC,
|
|
||||||
int $type = SORT_NATURAL,
|
|
||||||
bool $caseSensitive = false,
|
|
||||||
bool $preserveKeys = true
|
|
||||||
): static {
|
|
||||||
return parent::sort($direction, $type, $this->pluck($property), $caseSensitive, $preserveKeys);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,7 +70,7 @@ class PageCollection extends AbstractCollection
|
|||||||
|
|
||||||
$keywords = explode(' ', $query);
|
$keywords = explode(' ', $query);
|
||||||
$keywords = array_diff($keywords, (array) Formwork::instance()->config()->get('search.stopwords'));
|
$keywords = array_diff($keywords, (array) Formwork::instance()->config()->get('search.stopwords'));
|
||||||
$keywords = array_filter($keywords, static fn (string $item): bool => strlen($item) > $min);
|
$keywords = array_filter($keywords, fn (string $item): bool => strlen($item) > $min);
|
||||||
|
|
||||||
$queryRegex = '/\b' . preg_quote($query, '/') . '\b/iu';
|
$queryRegex = '/\b' . preg_quote($query, '/') . '\b/iu';
|
||||||
$keywordsRegex = '/(?:\b' . implode('\b|\b', $keywords) . '\b)/iu';
|
$keywordsRegex = '/(?:\b' . implode('\b|\b', $keywords) . '\b)/iu';
|
||||||
@ -173,6 +131,6 @@ class PageCollection extends AbstractCollection
|
|||||||
|
|
||||||
$pages = new static($pages);
|
$pages = new static($pages);
|
||||||
|
|
||||||
return $pages->sortBy('path');
|
return $pages->sortBy('relativePath');
|
||||||
}
|
}
|
||||||
}
|
}
|
16
formwork/src/Pages/Pagination.php
Normal file
16
formwork/src/Pages/Pagination.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Pages;
|
||||||
|
|
||||||
|
use Formwork\Data\Pagination as BasePagination;
|
||||||
|
use Formwork\Pages\Traits\PaginationUri;
|
||||||
|
|
||||||
|
class Pagination extends BasePagination
|
||||||
|
{
|
||||||
|
use PaginationUri;
|
||||||
|
|
||||||
|
public function __construct(PageCollection $collection, int $length)
|
||||||
|
{
|
||||||
|
parent::__construct($collection, $length);
|
||||||
|
}
|
||||||
|
}
|
@ -1,44 +1,69 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Formwork;
|
namespace Formwork\Pages;
|
||||||
|
|
||||||
|
use Formwork\Data\Contracts\Arrayable;
|
||||||
|
use Formwork\Fields\FieldCollection;
|
||||||
|
use Formwork\Formwork;
|
||||||
use Formwork\Languages\Languages;
|
use Formwork\Languages\Languages;
|
||||||
use Formwork\Metadata\Metadata;
|
use Formwork\Metadata\MetadataCollection;
|
||||||
|
use Formwork\Pages\Templates\TemplateCollection;
|
||||||
|
use Formwork\Pages\Traits\PageData;
|
||||||
|
use Formwork\Pages\Traits\PageTraversal;
|
||||||
|
use Formwork\Pages\Traits\PageUid;
|
||||||
|
use Formwork\Pages\Traits\PageUri;
|
||||||
|
use Formwork\Schemes\Scheme;
|
||||||
use Formwork\Utils\FileSystem;
|
use Formwork\Utils\FileSystem;
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
class Site extends AbstractPage
|
class Site implements Arrayable
|
||||||
{
|
{
|
||||||
/**
|
use PageData;
|
||||||
* Array containing all loaded pages
|
use PageTraversal;
|
||||||
*/
|
use PageUid;
|
||||||
|
use PageUri;
|
||||||
|
|
||||||
|
protected string $relativePath;
|
||||||
|
|
||||||
|
protected string $route;
|
||||||
|
|
||||||
|
protected int $lastModifiedTime;
|
||||||
|
|
||||||
protected array $storage = [];
|
protected array $storage = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* Current page
|
|
||||||
*/
|
|
||||||
protected ?Page $currentPage = null;
|
protected ?Page $currentPage = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Array containing all available templates
|
|
||||||
*/
|
|
||||||
protected array $templates = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Site languages
|
|
||||||
*/
|
|
||||||
protected Languages $languages;
|
protected Languages $languages;
|
||||||
|
|
||||||
|
protected Scheme $scheme;
|
||||||
|
|
||||||
|
protected FieldCollection $fields;
|
||||||
|
|
||||||
|
protected TemplateCollection $templates;
|
||||||
|
|
||||||
|
protected array $aliases;
|
||||||
|
|
||||||
|
protected MetadataCollection $metadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Site instance
|
* Create a new Site instance
|
||||||
*/
|
*/
|
||||||
public function __construct(array $data)
|
public function __construct(array $data)
|
||||||
{
|
{
|
||||||
$this->path = FileSystem::normalizePath(Formwork::instance()->config()->get('content.path'));
|
$this->path = FileSystem::normalizePath(Formwork::instance()->config()->get('content.path'));
|
||||||
|
|
||||||
$this->relativePath = DS;
|
$this->relativePath = DS;
|
||||||
|
|
||||||
$this->route = '/';
|
$this->route = '/';
|
||||||
|
|
||||||
|
$this->scheme = Formwork::instance()->schemes()->get('config', 'site');
|
||||||
|
|
||||||
$this->data = array_replace_recursive($this->defaults(), $data);
|
$this->data = array_replace_recursive($this->defaults(), $data);
|
||||||
$this->loadTemplates();
|
|
||||||
|
$this->fields = $this->scheme->fields()->validate($this->data);
|
||||||
|
|
||||||
|
$this->templates = TemplateCollection::fromPath(Formwork::instance()->config()->get('templates.path'));
|
||||||
|
|
||||||
|
$this->loadAliases();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,38 +71,21 @@ class Site extends AbstractPage
|
|||||||
*/
|
*/
|
||||||
public function defaults(): array
|
public function defaults(): array
|
||||||
{
|
{
|
||||||
|
// Formwork::instance()->schemes()->get('config', 'site')->fields();
|
||||||
return [
|
return [
|
||||||
'title' => 'Formwork',
|
'title' => 'Formwork',
|
||||||
'aliases' => [],
|
'aliases' => [],
|
||||||
'metadata' => []
|
'metadata' => [],
|
||||||
|
'canonical' => null
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function lastModifiedTime(): int
|
||||||
* Get all available templates
|
|
||||||
*/
|
|
||||||
public function templates(): array
|
|
||||||
{
|
{
|
||||||
return array_map('strval', array_keys($this->templates));
|
if (isset($this->lastModifiedTime)) {
|
||||||
}
|
return $this->lastModifiedTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether a template exists
|
|
||||||
*/
|
|
||||||
public function hasTemplate(string $template): bool
|
|
||||||
{
|
|
||||||
return array_key_exists($template, $this->templates);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return template filename
|
|
||||||
*/
|
|
||||||
public function template(string $name): string
|
|
||||||
{
|
|
||||||
if (!$this->hasTemplate($name)) {
|
|
||||||
throw new RuntimeException(sprintf('Invalid template "%s"', $name));
|
|
||||||
}
|
}
|
||||||
return $this->templates[$name];
|
return $this->lastModifiedTime = FileSystem::lastModifiedTime($this->path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,14 +96,6 @@ class Site extends AbstractPage
|
|||||||
return FileSystem::directoryModifiedSince($this->path, $time);
|
return FileSystem::directoryModifiedSince($this->path, $time);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function parent()
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a PageCollection containing site pages
|
* Return a PageCollection containing site pages
|
||||||
*/
|
*/
|
||||||
@ -109,121 +109,18 @@ class Site extends AbstractPage
|
|||||||
*/
|
*/
|
||||||
public function hasPages(): bool
|
public function hasPages(): bool
|
||||||
{
|
{
|
||||||
return !$this->children()->isEmpty();
|
return $this->hasChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return alias of a given route
|
* Retrieve page from the storage creating a new one if not existing
|
||||||
*
|
|
||||||
* @return string|void
|
|
||||||
*/
|
*/
|
||||||
public function alias(string $route)
|
public function retrievePage(string $path): Page
|
||||||
{
|
{
|
||||||
if ($this->has('aliases')) {
|
if (isset($this->storage[$path])) {
|
||||||
$route = trim($route, '/');
|
return $this->storage[$path];
|
||||||
if (isset($this->data['aliases'][$route])) {
|
|
||||||
return $this->data['aliases'][$route];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return $this->storage[$path] = new Page($path);
|
||||||
|
|
||||||
/**
|
|
||||||
* Set and return site current page
|
|
||||||
*/
|
|
||||||
public function setCurrentPage(Page $page): Page
|
|
||||||
{
|
|
||||||
return $this->currentPage = $page;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to and return a page from its route, setting then the current page
|
|
||||||
*/
|
|
||||||
public function navigate(string $route): Page
|
|
||||||
{
|
|
||||||
return $this->currentPage = $this->findPage($route);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set site languages
|
|
||||||
*/
|
|
||||||
public function setLanguages(Languages $languages): void
|
|
||||||
{
|
|
||||||
$this->languages = $languages;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get site index page
|
|
||||||
*/
|
|
||||||
public function indexPage(): ?Page
|
|
||||||
{
|
|
||||||
return $this->findPage(Formwork::instance()->config()->get('pages.index'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return or render site error page
|
|
||||||
*
|
|
||||||
* @param bool $navigate Whether to navigate to the error page or not
|
|
||||||
*/
|
|
||||||
public function errorPage(bool $navigate = false): ?Page
|
|
||||||
{
|
|
||||||
$errorPage = $this->findPage(Formwork::instance()->config()->get('pages.error'));
|
|
||||||
if ($navigate) {
|
|
||||||
$this->currentPage = $errorPage;
|
|
||||||
}
|
|
||||||
return $errorPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function isSite(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function isIndexPage(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function isErrorPage(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function isDeletable(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function metadata(): Metadata
|
|
||||||
{
|
|
||||||
if (isset($this->metadata)) {
|
|
||||||
return $this->metadata;
|
|
||||||
}
|
|
||||||
$defaults = [
|
|
||||||
'charset' => Formwork::instance()->config()->get('charset'),
|
|
||||||
'author' => $this->get('author'),
|
|
||||||
'description' => $this->get('description'),
|
|
||||||
'generator' => 'Formwork'
|
|
||||||
];
|
|
||||||
$data = array_filter(array_merge($defaults, $this->data['metadata']));
|
|
||||||
if (!Formwork::instance()->config()->get('metadata.set_generator')) {
|
|
||||||
unset($data['generator']);
|
|
||||||
}
|
|
||||||
return $this->metadata = new Metadata($data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -258,31 +155,96 @@ class Site extends AbstractPage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve page from the storage creating a new one if not existing
|
* Set and return site current page
|
||||||
*/
|
*/
|
||||||
public function retrievePage(string $path): Page
|
public function setCurrentPage(Page $page): Page
|
||||||
{
|
{
|
||||||
if (isset($this->storage[$path])) {
|
return $this->currentPage = $page;
|
||||||
return $this->storage[$path];
|
|
||||||
}
|
|
||||||
return $this->storage[$path] = new Page($path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all available templates
|
* Set site languages
|
||||||
*/
|
*/
|
||||||
protected function loadTemplates(): void
|
public function setLanguages(Languages $languages): void
|
||||||
{
|
{
|
||||||
$templatesPath = Formwork::instance()->config()->get('templates.path');
|
$this->languages = $languages;
|
||||||
$templates = [];
|
|
||||||
foreach (FileSystem::listFiles($templatesPath) as $file) {
|
|
||||||
$templates[FileSystem::name($file)] = $templatesPath . $file;
|
|
||||||
}
|
|
||||||
$this->templates = $templates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __toString(): string
|
/**
|
||||||
|
* Return alias of a given route
|
||||||
|
*/
|
||||||
|
public function resolveAlias(string $route): ?string
|
||||||
{
|
{
|
||||||
return 'site';
|
return $this->aliases[$route] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function metadata(): MetadataCollection
|
||||||
|
{
|
||||||
|
if (isset($this->metadata)) {
|
||||||
|
return $this->metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaults = [
|
||||||
|
'charset' => Formwork::instance()->config()->get('charset'),
|
||||||
|
'author' => $this->get('author'),
|
||||||
|
'description' => $this->get('description'),
|
||||||
|
'generator' => 'Formwork'
|
||||||
|
];
|
||||||
|
|
||||||
|
$data = array_filter(array_merge($defaults, $this->data['metadata']));
|
||||||
|
|
||||||
|
if (!Formwork::instance()->config()->get('metadata.set_generator')) {
|
||||||
|
unset($data['generator']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->metadata = new MetadataCollection($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get site index page
|
||||||
|
*/
|
||||||
|
public function indexPage(): ?Page
|
||||||
|
{
|
||||||
|
return $this->findPage(Formwork::instance()->config()->get('pages.index'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return or render site error page
|
||||||
|
*/
|
||||||
|
public function errorPage(): ?Page
|
||||||
|
{
|
||||||
|
return $this->findPage(Formwork::instance()->config()->get('pages.error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSite(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isIndexPage(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isErrorPage(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDeletable(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadAliases(): void
|
||||||
|
{
|
||||||
|
foreach ($this->data['aliases'] as $from => $to) {
|
||||||
|
$this->aliases[trim($from, '/')] = trim($to, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return $this->title();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,12 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Formwork\Template;
|
namespace Formwork\Pages\Templates;
|
||||||
|
|
||||||
use Formwork\Assets;
|
use Formwork\Assets;
|
||||||
use Formwork\Formwork;
|
use Formwork\Formwork;
|
||||||
use Formwork\Page;
|
use Formwork\Pages\Page;
|
||||||
use Formwork\Utils\FileSystem;
|
use Formwork\Utils\FileSystem;
|
||||||
use Formwork\Utils\Str;
|
|
||||||
use Formwork\View\Renderer;
|
use Formwork\View\Renderer;
|
||||||
use Formwork\View\View;
|
use Formwork\View\View;
|
||||||
|
|
||||||
@ -33,7 +32,7 @@ class Template extends View
|
|||||||
public function __construct(string $name, Page $page)
|
public function __construct(string $name, Page $page)
|
||||||
{
|
{
|
||||||
$this->page = $page;
|
$this->page = $page;
|
||||||
parent::__construct($name, [], Formwork::instance()->config()->get('templates.path'));
|
parent::__construct($name, [], Formwork::instance()->config()->get('templates.path'), $this->defaultMethods());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,22 +49,10 @@ class Template extends View
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a template
|
|
||||||
*/
|
|
||||||
public function insert(string $name, array $vars = []): void
|
|
||||||
{
|
|
||||||
if (Str::startsWith($name, '_')) {
|
|
||||||
$name = 'partials' . DS . Str::removeStart($name, '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
parent::insert($name, $vars);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render template
|
* Render template
|
||||||
*/
|
*/
|
||||||
public function render(bool $return = false)
|
public function render(): string
|
||||||
{
|
{
|
||||||
$isCurrentPage = $this->page->isCurrent();
|
$isCurrentPage = $this->page->isCurrent();
|
||||||
|
|
||||||
@ -73,10 +60,10 @@ class Template extends View
|
|||||||
|
|
||||||
// Render correct page if the controller has changed the current one
|
// Render correct page if the controller has changed the current one
|
||||||
if ($isCurrentPage && !$this->page->isCurrent()) {
|
if ($isCurrentPage && !$this->page->isCurrent()) {
|
||||||
return Formwork::instance()->site()->currentPage()->template()->render($return);
|
return Formwork::instance()->site()->currentPage()->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::render($return);
|
return parent::render();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,11 +79,13 @@ class Template extends View
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* Default template methods
|
||||||
*/
|
*/
|
||||||
protected function createLayoutView(string $name): View
|
protected function defaultMethods(): array
|
||||||
{
|
{
|
||||||
return new static('layouts' . DS . $name, $this->page);
|
return [
|
||||||
|
'assets' => fn () => $this->assets()
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,7 +96,9 @@ class Template extends View
|
|||||||
$controllerFile = $this->path . 'controllers' . DS . $this->name . '.php';
|
$controllerFile = $this->path . 'controllers' . DS . $this->name . '.php';
|
||||||
|
|
||||||
if (FileSystem::exists($controllerFile)) {
|
if (FileSystem::exists($controllerFile)) {
|
||||||
|
$this->allowMethods = true;
|
||||||
$this->vars = array_merge($this->vars, (array) Renderer::load($controllerFile, $this->vars, $this));
|
$this->vars = array_merge($this->vars, (array) Renderer::load($controllerFile, $this->vars, $this));
|
||||||
|
$this->allowMethods = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
35
formwork/src/Pages/Templates/TemplateCollection.php
Normal file
35
formwork/src/Pages/Templates/TemplateCollection.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Pages\Templates;
|
||||||
|
|
||||||
|
use Formwork\Data\AbstractCollection;
|
||||||
|
use Formwork\Utils\FileSystem;
|
||||||
|
|
||||||
|
class TemplateCollection extends AbstractCollection
|
||||||
|
{
|
||||||
|
protected bool $associative = true;
|
||||||
|
|
||||||
|
protected ?string $dataType = Template::class;
|
||||||
|
|
||||||
|
public function load(string $path): void
|
||||||
|
{
|
||||||
|
if (FileSystem::isReadable($path) && FileSystem::extension($path) === 'php') {
|
||||||
|
$name = FileSystem::name($path);
|
||||||
|
$this->data[$name] = $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadFromPath(string $path): void
|
||||||
|
{
|
||||||
|
foreach (FileSystem::listFiles($path) as $file) {
|
||||||
|
$this->load(FileSystem::joinPaths($path, $file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromPath(string $path): self
|
||||||
|
{
|
||||||
|
$instance = new static();
|
||||||
|
$instance->loadFromPath($path);
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
}
|
79
formwork/src/Pages/Traits/PageData.php
Normal file
79
formwork/src/Pages/Traits/PageData.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Pages\Traits;
|
||||||
|
|
||||||
|
use BadMethodCallException;
|
||||||
|
use Formwork\Data\Traits\DataMultipleGetter;
|
||||||
|
use Formwork\Data\Traits\DataMultipleSetter;
|
||||||
|
use Formwork\Utils\Arr;
|
||||||
|
|
||||||
|
trait PageData
|
||||||
|
{
|
||||||
|
use DataMultipleGetter;
|
||||||
|
use DataMultipleSetter;
|
||||||
|
|
||||||
|
public function has(string $key): bool
|
||||||
|
{
|
||||||
|
return property_exists($this, $key) || $this->fields->has($key) || Arr::has($this->data, $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key, $default = null)
|
||||||
|
{
|
||||||
|
// Get values from property
|
||||||
|
if (property_exists($this, $key)) {
|
||||||
|
// Call getter method if exists. We check property existence before
|
||||||
|
// to avoid using get to call methods arbitrarily
|
||||||
|
if (method_exists($this, $key)) {
|
||||||
|
return $this->$key();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->$key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get values from fields
|
||||||
|
if ($this->fields->has($key)) {
|
||||||
|
$field = $this->fields->get($key);
|
||||||
|
|
||||||
|
// If defined use the value returned by `return()`
|
||||||
|
if ($field->hasMethod('return')) {
|
||||||
|
return $field->return();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $field->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get values from data
|
||||||
|
return Arr::get($this->data, $key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, $value): void
|
||||||
|
{
|
||||||
|
if (property_exists($this, $key)) {
|
||||||
|
$this->$key = $value;
|
||||||
|
} else {
|
||||||
|
Arr::set($this->data, $key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
$properties = array_keys(get_class_vars($this::class));
|
||||||
|
|
||||||
|
Arr::pull($properties, 'data');
|
||||||
|
|
||||||
|
$data = array_merge($this->data, $this->getMultiple($properties));
|
||||||
|
|
||||||
|
ksort($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __call(string $name, array $arguments)
|
||||||
|
{
|
||||||
|
if ($this->has($name)) {
|
||||||
|
return $this->get($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadMethodCallException(sprintf('Call to undefined method %s::%s()', static::class, $name));
|
||||||
|
}
|
||||||
|
}
|
44
formwork/src/Pages/Traits/PageStatus.php
Normal file
44
formwork/src/Pages/Traits/PageStatus.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Pages\Traits;
|
||||||
|
|
||||||
|
use Formwork\Pages\Page;
|
||||||
|
use Formwork\Utils\Date;
|
||||||
|
|
||||||
|
trait PageStatus
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Page status
|
||||||
|
*/
|
||||||
|
protected string $status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page status
|
||||||
|
*/
|
||||||
|
public function status(): string
|
||||||
|
{
|
||||||
|
if (isset($this->status)) {
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
$published = $this->data['published'];
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
if ($publishDate = $this->data['publishDate'] ?? null) {
|
||||||
|
$published = $published && Date::toTimestamp($publishDate) < $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($unpublishDate = $this->data['unpublishDate'] ?? null) {
|
||||||
|
$published = $published && Date::toTimestamp($unpublishDate) > $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->status = match (true) {
|
||||||
|
$published => Page::PAGE_STATUS_PUBLISHED,
|
||||||
|
!$this->routable() => Page::PAGE_STATUS_NOT_ROUTABLE,
|
||||||
|
!$published => Page::PAGE_STATUS_NOT_PUBLISHED
|
||||||
|
};
|
||||||
|
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
}
|
178
formwork/src/Pages/Traits/PageTraversal.php
Normal file
178
formwork/src/Pages/Traits/PageTraversal.php
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Pages\Traits;
|
||||||
|
|
||||||
|
use Formwork\Formwork;
|
||||||
|
use Formwork\Pages\Page;
|
||||||
|
use Formwork\Pages\PageCollection;
|
||||||
|
use Formwork\Pages\Site;
|
||||||
|
|
||||||
|
trait PageTraversal
|
||||||
|
{
|
||||||
|
protected string $path;
|
||||||
|
|
||||||
|
protected Page|Site|null $parent;
|
||||||
|
|
||||||
|
protected PageCollection $children;
|
||||||
|
|
||||||
|
protected PageCollection $descendants;
|
||||||
|
|
||||||
|
protected PageCollection $ancestors;
|
||||||
|
|
||||||
|
protected PageCollection $siblings;
|
||||||
|
|
||||||
|
protected PageCollection $inclusiveSiblings;
|
||||||
|
|
||||||
|
abstract public function isSite(): bool;
|
||||||
|
|
||||||
|
public function parent(): Page|Site|null
|
||||||
|
{
|
||||||
|
if (isset($this->parent)) {
|
||||||
|
return $this->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isSite()) {
|
||||||
|
return $this->parent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentPath = dirname($this->path) . DS;
|
||||||
|
|
||||||
|
if ($parentPath === Formwork::instance()->config()->get('content.path')) {
|
||||||
|
return $this->parent = Formwork::instance()->site();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parent = Formwork::instance()->site()->retrievePage($parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasParent(): bool
|
||||||
|
{
|
||||||
|
return $this->parent() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isParentOf(Page|Site $page): bool
|
||||||
|
{
|
||||||
|
return $page->parent() === $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function children(): PageCollection
|
||||||
|
{
|
||||||
|
if (isset($this->children)) {
|
||||||
|
return $this->children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->children = PageCollection::fromPath($this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasChildren(): bool
|
||||||
|
{
|
||||||
|
return !$this->children()->isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isChildOf(Page|Site $page): bool
|
||||||
|
{
|
||||||
|
return $page->children()->contains($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function descendants(): PageCollection
|
||||||
|
{
|
||||||
|
if (isset($this->descendants)) {
|
||||||
|
return $this->descendants;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->descendants = PageCollection::fromPath($this->path, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasDescendants(): bool
|
||||||
|
{
|
||||||
|
return !$this->descendants()->isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDescendantOf(Page|Site $page): bool
|
||||||
|
{
|
||||||
|
return $page->descendants()->contains($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ancestors(): PageCollection
|
||||||
|
{
|
||||||
|
if (isset($this->ancestors)) {
|
||||||
|
return $this->ancestors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ancestors = [];
|
||||||
|
|
||||||
|
$page = $this;
|
||||||
|
|
||||||
|
while (($parent = $page->parent()) !== null) {
|
||||||
|
$ancestors[] = $parent;
|
||||||
|
$page = $parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->ancestors = new PageCollection($ancestors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasAncestors(): bool
|
||||||
|
{
|
||||||
|
return !$this->ancestors()->isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAncestorOf(Page|Site $page): bool
|
||||||
|
{
|
||||||
|
return $page->ancestors()->contains($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function siblings(): PageCollection
|
||||||
|
{
|
||||||
|
if (isset($this->siblings)) {
|
||||||
|
return $this->siblings;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isSite()) {
|
||||||
|
return $this->siblings = new PageCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->siblings = $this->inclusiveSiblings()->without($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inclusiveSiblings(): PageCollection
|
||||||
|
{
|
||||||
|
if (isset($this->inclusiveSiblings)) {
|
||||||
|
return $this->inclusiveSiblings;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isSite()) {
|
||||||
|
return $this->inclusiveSiblings = new PageCollection([$this]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->inclusiveSiblings = $this->parent()->children();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasSiblings(): bool
|
||||||
|
{
|
||||||
|
return !$this->siblings()->isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSiblingOf(Page|Site $page): bool
|
||||||
|
{
|
||||||
|
return !$page->siblings()->contains($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function previousSibling(): ?Page
|
||||||
|
{
|
||||||
|
return $this->inclusiveSiblings()->nth($this->index() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextSibling(): ?Page
|
||||||
|
{
|
||||||
|
return $this->inclusiveSiblings()->nth($this->index() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): int
|
||||||
|
{
|
||||||
|
return $this->inclusiveSiblings()->indexOf($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function level(): int
|
||||||
|
{
|
||||||
|
return $this->ancestors()->count();
|
||||||
|
}
|
||||||
|
}
|
18
formwork/src/Pages/Traits/PageUid.php
Normal file
18
formwork/src/Pages/Traits/PageUid.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Pages\Traits;
|
||||||
|
|
||||||
|
use Formwork\Utils\Str;
|
||||||
|
|
||||||
|
trait PageUid
|
||||||
|
{
|
||||||
|
protected string $uid;
|
||||||
|
|
||||||
|
public function uid(): string
|
||||||
|
{
|
||||||
|
if (isset($this->uid)) {
|
||||||
|
return $this->uid;
|
||||||
|
}
|
||||||
|
return $this->uid = Str::chunk(substr(hash('sha256', $this->relativePath), 0, 32), 8, '-');
|
||||||
|
}
|
||||||
|
}
|
49
formwork/src/Pages/Traits/PageUri.php
Normal file
49
formwork/src/Pages/Traits/PageUri.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Pages\Traits;
|
||||||
|
|
||||||
|
use Formwork\Formwork;
|
||||||
|
use Formwork\Utils\HTTPRequest;
|
||||||
|
use Formwork\Utils\Path;
|
||||||
|
use Formwork\Utils\Uri;
|
||||||
|
|
||||||
|
trait PageUri
|
||||||
|
{
|
||||||
|
protected string $route;
|
||||||
|
|
||||||
|
protected string $absoluteUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a URI relative to page
|
||||||
|
*/
|
||||||
|
public function uri(string $path = '', bool|string $includeLanguage = true): string
|
||||||
|
{
|
||||||
|
$base = HTTPRequest::root();
|
||||||
|
|
||||||
|
$route = $this->canonical() ?? $this->route;
|
||||||
|
|
||||||
|
if ($includeLanguage) {
|
||||||
|
$language = is_string($includeLanguage) ? $includeLanguage : Formwork::instance()->site()->languages()->current();
|
||||||
|
|
||||||
|
$default = Formwork::instance()->site()->languages()->default();
|
||||||
|
$preferred = Formwork::instance()->site()->languages()->preferred();
|
||||||
|
|
||||||
|
if (($language !== null && $language !== $default) || ($preferred !== null && $preferred !== $default)) {
|
||||||
|
return Path::join([$base, $language, $route, $path]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path::join([$base, $route, $path]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page absolute URI
|
||||||
|
*/
|
||||||
|
public function absoluteUri(): string
|
||||||
|
{
|
||||||
|
if (isset($this->absoluteUri)) {
|
||||||
|
return $this->absoluteUri;
|
||||||
|
}
|
||||||
|
return $this->absoluteUri = Uri::resolveRelative($this->uri());
|
||||||
|
}
|
||||||
|
}
|
126
formwork/src/Pages/Traits/PaginationUri.php
Normal file
126
formwork/src/Pages/Traits/PaginationUri.php
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Formwork\Pages\Traits;
|
||||||
|
|
||||||
|
use Formwork\Formwork;
|
||||||
|
use Formwork\Router\Route;
|
||||||
|
use Formwork\Utils\Str;
|
||||||
|
use RuntimeException;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
|
trait PaginationUri
|
||||||
|
{
|
||||||
|
protected static string $routeParam = 'paginationPage';
|
||||||
|
|
||||||
|
protected static string $routeSuffix = '.pagination';
|
||||||
|
|
||||||
|
protected Route $baseRoute;
|
||||||
|
|
||||||
|
protected Route $paginationRoute;
|
||||||
|
|
||||||
|
public function route(int $pageNumber): string
|
||||||
|
{
|
||||||
|
if (!$this->has($pageNumber)) {
|
||||||
|
throw new UnexpectedValueException(sprintf('Cannot get the route for page %d, the pagination has only %d pages', $pageNumber, $this->length));
|
||||||
|
}
|
||||||
|
|
||||||
|
$router = Formwork::instance()->router();
|
||||||
|
|
||||||
|
if ($pageNumber === 1) {
|
||||||
|
return $router->generateWith($this->baseRoute()->getName(), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $router->generateWith($this->paginationRoute()->getName(), [
|
||||||
|
static::$routeParam => $pageNumber
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uri(int $pageNumber): string
|
||||||
|
{
|
||||||
|
return Formwork::instance()->site()->uri($this->route($pageNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function firstPageRoute(): string
|
||||||
|
{
|
||||||
|
return $this->route($this->firstPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function firstPageUri(): string
|
||||||
|
{
|
||||||
|
return $this->uri($this->firstPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastPageRoute(): string
|
||||||
|
{
|
||||||
|
return $this->route($this->lastPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastPageUri(): string
|
||||||
|
{
|
||||||
|
return $this->uri($this->lastPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route of the next pagination page
|
||||||
|
*/
|
||||||
|
public function previousPageRoute(): string
|
||||||
|
{
|
||||||
|
return $this->route($this->previousPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URI of the next pagination page
|
||||||
|
*/
|
||||||
|
public function previousPageUri(): string
|
||||||
|
{
|
||||||
|
return $this->uri($this->previousPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route of the next pagination page
|
||||||
|
*/
|
||||||
|
public function nextPageRoute(): string
|
||||||
|
{
|
||||||
|
return $this->route($this->nextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URI of the next pagination page
|
||||||
|
*/
|
||||||
|
public function nextPageUri(): string
|
||||||
|
{
|
||||||
|
return $this->uri($this->nextPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function baseRoute(): ?Route
|
||||||
|
{
|
||||||
|
if (isset($this->baseRoute)) {
|
||||||
|
return $this->baseRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
$router = Formwork::instance()->router();
|
||||||
|
$routeName = Str::removeEnd($router->current()->getName(), static::$routeSuffix);
|
||||||
|
|
||||||
|
if (!$router->routes()->has($routeName)) {
|
||||||
|
throw new RuntimeException(sprintf('Cannot generate pagination routes, base route "%s" is not defined', $this->routeName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->baseRoute = $router->routes()->get($routeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function paginationRoute(): ?Route
|
||||||
|
{
|
||||||
|
if (isset($this->paginationRoute)) {
|
||||||
|
return $this->paginationRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
$router = Formwork::instance()->router();
|
||||||
|
$routeName = $this->baseRoute()->getName() . static::$routeSuffix;
|
||||||
|
|
||||||
|
if (!$router->routes()->has($routeName)) {
|
||||||
|
throw new RuntimeException(sprintf('Cannot generate pagination for route "%s", route "%s" is not defined', $this->baseRoute()->getName(), $routeName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->paginationRoute = $router->routes()->get($routeName);
|
||||||
|
}
|
||||||
|
}
|
@ -1,182 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Formwork;
|
|
||||||
|
|
||||||
use Formwork\Utils\Header;
|
|
||||||
use Formwork\Utils\Uri;
|
|
||||||
|
|
||||||
class Pagination
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Number of all items to paginate
|
|
||||||
*/
|
|
||||||
protected int $count = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of items in each pagination page
|
|
||||||
*/
|
|
||||||
protected int $length = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of pagination pages
|
|
||||||
*/
|
|
||||||
protected int $pages = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base URI to which append pagination page number
|
|
||||||
*/
|
|
||||||
protected string $baseUri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current pagination page
|
|
||||||
*/
|
|
||||||
protected int $currentPage = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new Pagination instance
|
|
||||||
*/
|
|
||||||
public function __construct(int $count, int $length)
|
|
||||||
{
|
|
||||||
$router = Formwork::instance()->router();
|
|
||||||
|
|
||||||
$this->count = $count;
|
|
||||||
$this->length = $length;
|
|
||||||
$this->pages = $count > 0 ? (int) ceil($count / $length) : 1;
|
|
||||||
|
|
||||||
$this->baseUri = Formwork::instance()->site()->uri(preg_replace('~/page/[0-9]+/?$~', '/', $router->request()));
|
|
||||||
|
|
||||||
$this->currentPage = (int) $router->params()->get('paginationPage', 1);
|
|
||||||
|
|
||||||
if ($router->params()->get('paginationPage') == 1) {
|
|
||||||
Header::redirect($this->baseUri, 301);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->currentPage > $this->pages || $this->currentPage < 1) {
|
|
||||||
Formwork::instance()->site()->errorPage(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current page
|
|
||||||
*/
|
|
||||||
public function currentPage(): int
|
|
||||||
{
|
|
||||||
return $this->currentPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get pagination length
|
|
||||||
*/
|
|
||||||
public function length(): int
|
|
||||||
{
|
|
||||||
return $this->length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current pagination offset
|
|
||||||
*/
|
|
||||||
public function offset(): int
|
|
||||||
{
|
|
||||||
return ($this->currentPage - 1) * $this->length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether a given page number exists
|
|
||||||
*/
|
|
||||||
public function hasPage(int $number): bool
|
|
||||||
{
|
|
||||||
return (int) $number > 1 && (int) $number <= $this->pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether current page is the first
|
|
||||||
*/
|
|
||||||
public function firstPage(): bool
|
|
||||||
{
|
|
||||||
return $this->currentPage === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether current page is the last
|
|
||||||
*/
|
|
||||||
public function lastPage(): bool
|
|
||||||
{
|
|
||||||
return $this->currentPage === $this->pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether pagination has more than one page
|
|
||||||
*/
|
|
||||||
public function hasPages(): bool
|
|
||||||
{
|
|
||||||
return $this->pages > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether a previous page exists
|
|
||||||
*/
|
|
||||||
public function hasPreviousPage(): bool
|
|
||||||
{
|
|
||||||
return !$this->firstPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether a next page exists
|
|
||||||
*/
|
|
||||||
public function hasNextPage(): bool
|
|
||||||
{
|
|
||||||
return !$this->lastPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get previous pagination page number
|
|
||||||
*
|
|
||||||
* @return bool|int
|
|
||||||
*/
|
|
||||||
public function previousPage()
|
|
||||||
{
|
|
||||||
$previous = $this->currentPage - 1;
|
|
||||||
return $previous > 0 ? $previous : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get next pagination page number
|
|
||||||
*
|
|
||||||
* @return bool|int
|
|
||||||
*/
|
|
||||||
public function nextPage()
|
|
||||||
{
|
|
||||||
$next = $this->currentPage + 1;
|
|
||||||
return $next > $this->pages ? false : $next;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URI of the next pagination page
|
|
||||||
*/
|
|
||||||
public function nextPageUri(): string
|
|
||||||
{
|
|
||||||
return Uri::make(['host' => '', 'path' => $this->baseUri . 'page/' . $this->nextPage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URI of the previous pagination page
|
|
||||||
*/
|
|
||||||
public function previousPageUri(): string
|
|
||||||
{
|
|
||||||
if ($this->previousPage() === 1) {
|
|
||||||
return Uri::make(['host' => '', 'path' => $this->baseUri]);
|
|
||||||
}
|
|
||||||
return Uri::make(['host' => '', 'path' => $this->baseUri . 'page/' . $this->previousPage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __debugInfo(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'count' => $this->count,
|
|
||||||
'length' => $this->length,
|
|
||||||
'pages' => $this->pages,
|
|
||||||
'currentPage' => $this->currentPage,
|
|
||||||
'previousPage' => $this->previousPageUri()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,9 +3,12 @@
|
|||||||
namespace Formwork\Router;
|
namespace Formwork\Router;
|
||||||
|
|
||||||
use Formwork\Data\Traits\DataGetter;
|
use Formwork\Data\Traits\DataGetter;
|
||||||
|
use Formwork\Data\Contracts\Arrayable;
|
||||||
|
use Formwork\Data\Traits\DataArrayable;
|
||||||
|
|
||||||
class RouteParams
|
class RouteParams implements Arrayable
|
||||||
{
|
{
|
||||||
|
use DataArrayable;
|
||||||
use DataGetter;
|
use DataGetter;
|
||||||
|
|
||||||
public function __construct(array $data)
|
public function __construct(array $data)
|
||||||
|
@ -72,7 +72,9 @@ class Router
|
|||||||
{
|
{
|
||||||
$this->routes = new RouteCollection();
|
$this->routes = new RouteCollection();
|
||||||
$this->filters = new RouteFilterCollection();
|
$this->filters = new RouteFilterCollection();
|
||||||
$this->request = $request ?? Str::wrap(Uri::path(HTTPRequest::uri()), '/');
|
|
||||||
|
// Ensure requested route is wrapped in slashes
|
||||||
|
$this->request = Str::wrap($request ?? Uri::path(HTTPRequest::uri()), '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -188,6 +190,14 @@ class Router
|
|||||||
return $this->generateRoute($this->routes->get($name), $params);
|
return $this->generateRoute($this->routes->get($name), $params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a route with given params overriding the current ones
|
||||||
|
*/
|
||||||
|
public function generateWith(string $name, array $params): string
|
||||||
|
{
|
||||||
|
return $this->generateRoute($this->routes->get($name), $params + $this->params->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rewrite current route with given params
|
* Rewrite current route with given params
|
||||||
*/
|
*/
|
||||||
|
@ -5,6 +5,8 @@ namespace Formwork\Schemes;
|
|||||||
use Formwork\Data\Contracts\Arrayable;
|
use Formwork\Data\Contracts\Arrayable;
|
||||||
use Formwork\Data\Traits\DataArrayable;
|
use Formwork\Data\Traits\DataArrayable;
|
||||||
use Formwork\Data\Traits\DataGetter;
|
use Formwork\Data\Traits\DataGetter;
|
||||||
|
use Formwork\Fields\FieldCollection;
|
||||||
|
use Formwork\Fields\Layout\Layout;
|
||||||
use Formwork\Formwork;
|
use Formwork\Formwork;
|
||||||
use Formwork\Parsers\YAML;
|
use Formwork\Parsers\YAML;
|
||||||
use Formwork\Utils\FileSystem;
|
use Formwork\Utils\FileSystem;
|
||||||
@ -69,30 +71,11 @@ class Scheme implements Arrayable
|
|||||||
/**
|
/**
|
||||||
* Get scheme fields
|
* Get scheme fields
|
||||||
*/
|
*/
|
||||||
public function fields(): array
|
public function fields(): FieldCollection
|
||||||
{
|
{
|
||||||
return $this->get('fields', []);
|
return new FieldCollection(
|
||||||
}
|
$this->get('fields', []),
|
||||||
|
new Layout($this->get('layout', ['type' => 'default', 'sections' => []]))
|
||||||
/**
|
);
|
||||||
* Get scheme data
|
|
||||||
*/
|
|
||||||
public function data(): array
|
|
||||||
{
|
|
||||||
return $this->get('data', []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return default field values
|
|
||||||
*/
|
|
||||||
public function defaultFieldValues(): array
|
|
||||||
{
|
|
||||||
$result = [];
|
|
||||||
foreach ($this->fields() as $name => $value) {
|
|
||||||
if (isset($value['default'])) {
|
|
||||||
$result[$name] = $value['default'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ class Schemes
|
|||||||
*/
|
*/
|
||||||
protected array $storage = [];
|
protected array $storage = [];
|
||||||
|
|
||||||
|
protected array $data = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a scheme
|
* Load a scheme
|
||||||
*/
|
*/
|
||||||
|
@ -14,6 +14,8 @@ class Translations
|
|||||||
*/
|
*/
|
||||||
protected array $storage = [];
|
protected array $storage = [];
|
||||||
|
|
||||||
|
protected array $data = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current translation
|
* Current translation
|
||||||
*/
|
*/
|
||||||
|
@ -195,5 +195,4 @@ class Str
|
|||||||
{
|
{
|
||||||
return implode($delimiter, str_split($string, $length));
|
return implode($delimiter, str_split($string, $length));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,91 +1,46 @@
|
|||||||
title: Site
|
title: Site
|
||||||
|
|
||||||
|
layout:
|
||||||
|
type: sections
|
||||||
|
sections:
|
||||||
|
info:
|
||||||
|
label: '{{admin.options.site.info}}'
|
||||||
|
collapsible: true
|
||||||
|
fields: [title, author, description]
|
||||||
|
|
||||||
|
advanced:
|
||||||
|
label: '{{admin.options.site.advanced}}'
|
||||||
|
collapsible: true
|
||||||
|
collapsed: true
|
||||||
|
fields: [metadata, aliases]
|
||||||
|
|
||||||
fields:
|
fields:
|
||||||
section1:
|
title:
|
||||||
type: header
|
type: text
|
||||||
label: '{{admin.options.site.info}}'
|
label: '{{admin.options.site.info.title}}'
|
||||||
|
required: true
|
||||||
|
|
||||||
rows1:
|
author:
|
||||||
type: rows
|
type: text
|
||||||
fields:
|
label: '{{admin.options.site.info.author}}'
|
||||||
row1:
|
default: null
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.site.info.title}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
title:
|
|
||||||
type: text
|
|
||||||
required: true
|
|
||||||
|
|
||||||
row2:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.site.info.author}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
author:
|
|
||||||
type: text
|
|
||||||
|
|
||||||
row3:
|
description:
|
||||||
type: row
|
type: textarea
|
||||||
fields:
|
label: '{{admin.options.site.info.description}}'
|
||||||
column1:
|
default: null
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.site.info.description}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
description:
|
|
||||||
type: textarea
|
|
||||||
|
|
||||||
section2:
|
metadata:
|
||||||
type: header
|
type: array
|
||||||
label: '{{admin.options.site.advanced}}'
|
label: '{{admin.options.site.advanced.metadata}}'
|
||||||
|
associative: true
|
||||||
|
placeholder_key: '{{admin.options.site.advanced.metadata.name}}'
|
||||||
|
placeholder_value: '{{admin.options.site.advanced.metadata.content}}'
|
||||||
|
|
||||||
rows2:
|
aliases:
|
||||||
type: rows
|
type: array
|
||||||
fields:
|
label: '{{admin.options.site.advanced.aliases}}'
|
||||||
row1:
|
associative: true
|
||||||
type: row
|
placeholder_key: '{{admin.options.site.advanced.aliases.alias}}'
|
||||||
fields:
|
placeholder_value: '{{admin.options.site.advanced.aliases.route}}'
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.site.advanced.metadata}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
metadata:
|
|
||||||
type: array
|
|
||||||
associative: true
|
|
||||||
placeholder_key: '{{admin.options.site.advanced.metadata.name}}'
|
|
||||||
placeholder_value: '{{admin.options.site.advanced.metadata.content}}'
|
|
||||||
row2:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.site.advanced.aliases}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
aliases:
|
|
||||||
type: array
|
|
||||||
associative: true
|
|
||||||
placeholder_key: '{{admin.options.site.advanced.aliases.alias}}'
|
|
||||||
placeholder_value: '{{admin.options.site.advanced.aliases.route}}'
|
|
||||||
|
@ -1,364 +1,176 @@
|
|||||||
title: System
|
title: System
|
||||||
|
|
||||||
|
layout:
|
||||||
|
type: sections
|
||||||
|
sections:
|
||||||
|
dateTime:
|
||||||
|
collapsible: true
|
||||||
|
label: '{{admin.options.system.date-and-time}}'
|
||||||
|
fields: [date.format, date.time_format, date.timezone, date.week_starts]
|
||||||
|
|
||||||
|
languages:
|
||||||
|
collapsible: true
|
||||||
|
label: '{{admin.options.system.languages}}'
|
||||||
|
fields: [languages.available, languages.http_preferred]
|
||||||
|
|
||||||
|
files:
|
||||||
|
collapsible: true
|
||||||
|
label: '{{admin.options.system.files}}'
|
||||||
|
fields: [files.allowed_extensions]
|
||||||
|
|
||||||
|
cache:
|
||||||
|
collapsible: true
|
||||||
|
label: '{{admin.options.system.cache}}'
|
||||||
|
fields: [cache.enabled, cache.time]
|
||||||
|
|
||||||
|
admin:
|
||||||
|
collapsible: true
|
||||||
|
label: '{{admin.options.system.admin-panel}}'
|
||||||
|
fields: [admin.lang, admin.logout_redirect, admin.session_timeout, admin.color_scheme]
|
||||||
|
|
||||||
|
images:
|
||||||
|
collapsible: true
|
||||||
|
label: '{{admin.options.system.images}}'
|
||||||
|
fields: [images.jpeg_quality, images.png_compression, images.webp_quality, images.jpeg_progressive, images.process_uploads]
|
||||||
|
|
||||||
|
backup:
|
||||||
|
collapsible: true
|
||||||
|
label: '{{admin.options.system.backup}}'
|
||||||
|
fields: [backup.max_files]
|
||||||
|
|
||||||
fields:
|
fields:
|
||||||
section1:
|
date.format:
|
||||||
type: header
|
type: select
|
||||||
label: '{{admin.options.system.date-and-time}}'
|
label: '{{admin.options.system.date-and-time.date-format}}'
|
||||||
|
import:
|
||||||
|
options: 'Formwork\Admin\Utils\DateFormats::date'
|
||||||
|
|
||||||
rows1:
|
date.time_format:
|
||||||
type: rows
|
type: select
|
||||||
fields:
|
label: '{{admin.options.system.date-and-time.hour-format}}'
|
||||||
row1:
|
import:
|
||||||
type: row
|
options: 'Formwork\Admin\Utils\DateFormats::hour'
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.date-and-time.date-format}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
date.format:
|
|
||||||
type: select
|
|
||||||
import:
|
|
||||||
options: 'Formwork\Admin\Utils\DateFormats::date'
|
|
||||||
row2:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.date-and-time.hour-format}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
date.time_format:
|
|
||||||
type: select
|
|
||||||
import:
|
|
||||||
options: 'Formwork\Admin\Utils\DateFormats::hour'
|
|
||||||
row3:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.date-and-time.timezone}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
date.timezone:
|
|
||||||
type: select
|
|
||||||
import:
|
|
||||||
options: 'Formwork\Admin\Utils\DateFormats::timezones'
|
|
||||||
row4:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.date-and-time.first-weekday}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
date.week_starts:
|
|
||||||
type: select
|
|
||||||
options:
|
|
||||||
0: '{{admin.options.system.date-and-time.first-weekday.sunday}}'
|
|
||||||
1: '{{admin.options.system.date-and-time.first-weekday.monday}}'
|
|
||||||
|
|
||||||
section2:
|
date.timezone:
|
||||||
type: header
|
type: select
|
||||||
label: '{{admin.options.system.languages}}'
|
label: '{{admin.options.system.date-and-time.timezone}}'
|
||||||
|
import:
|
||||||
|
options: 'Formwork\Admin\Utils\DateFormats::timezones'
|
||||||
|
|
||||||
rows2:
|
date.week_starts:
|
||||||
type: rows
|
type: select
|
||||||
fields:
|
label: '{{admin.options.system.date-and-time.first-weekday}}'
|
||||||
row1:
|
options:
|
||||||
type: row
|
0: '{{admin.options.system.date-and-time.first-weekday.sunday}}'
|
||||||
fields:
|
1: '{{admin.options.system.date-and-time.first-weekday.monday}}'
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.languages.available-languages}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
languages.available:
|
|
||||||
type: tags
|
|
||||||
placeholder: '{{admin.options.system.languages.available-languages.no-languages}}'
|
|
||||||
pattern: '^[a-z]{2,3}$'
|
|
||||||
translate: [placeholder]
|
|
||||||
import:
|
|
||||||
options: 'Formwork\Languages\LanguageCodes::names'
|
|
||||||
row2:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.languages.preferred-language}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
languages.http_preferred:
|
|
||||||
type: togglegroup
|
|
||||||
options:
|
|
||||||
1: '{{admin.options.system.languages.preferred-language.enabled}}'
|
|
||||||
0: '{{admin.options.system.languages.preferred-language.disabled}}'
|
|
||||||
|
|
||||||
section3:
|
languages.available:
|
||||||
type: header
|
type: tags
|
||||||
label: '{{admin.options.system.files}}'
|
label: '{{admin.options.system.languages.available-languages}}'
|
||||||
|
placeholder: '{{admin.options.system.languages.available-languages.no-languages}}'
|
||||||
|
pattern: '^[a-z]{2,3}$'
|
||||||
|
translate: [label, placeholder]
|
||||||
|
import:
|
||||||
|
options: 'Formwork\Languages\LanguageCodes::names'
|
||||||
|
|
||||||
rows3:
|
languages.http_preferred:
|
||||||
type: rows
|
type: togglegroup
|
||||||
fields:
|
label: '{{admin.options.system.languages.preferred-language}}'
|
||||||
row1:
|
options:
|
||||||
type: row
|
1: '{{admin.options.system.languages.preferred-language.enabled}}'
|
||||||
fields:
|
0: '{{admin.options.system.languages.preferred-language.disabled}}'
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.files.allowed-extensions}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
files.allowed_extensions:
|
|
||||||
type: tags
|
|
||||||
pattern: '^\.[a-zA-Z0-9]+$'
|
|
||||||
required: true
|
|
||||||
|
|
||||||
section4:
|
files.allowed_extensions:
|
||||||
type: header
|
type: tags
|
||||||
|
label: '{{admin.options.system.files.allowed-extensions}}'
|
||||||
|
pattern: '^\.[a-zA-Z0-9]+$'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
cache.enabled:
|
||||||
|
type: togglegroup
|
||||||
label: '{{admin.options.system.cache}}'
|
label: '{{admin.options.system.cache}}'
|
||||||
|
options:
|
||||||
|
1: '{{admin.options.system.cache.enabled}}'
|
||||||
|
0: '{{admin.options.system.cache.disabled}}'
|
||||||
|
|
||||||
rows4:
|
cache.time:
|
||||||
type: rows
|
type: duration
|
||||||
fields:
|
label: '{{admin.options.system.cache.time}}'
|
||||||
row1:
|
min: 900
|
||||||
type: row
|
step: 900
|
||||||
fields:
|
intervals: [weeks, days, hours, minutes]
|
||||||
column1:
|
translate: [label]
|
||||||
type: column
|
required: true
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.cache}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
cache.enabled:
|
|
||||||
type: togglegroup
|
|
||||||
options:
|
|
||||||
1: '{{admin.options.system.cache.enabled}}'
|
|
||||||
0: '{{admin.options.system.cache.disabled}}'
|
|
||||||
row2:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.cache.time}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
cache.time:
|
|
||||||
type: duration
|
|
||||||
min: 900
|
|
||||||
step: 900
|
|
||||||
intervals: [weeks, days, hours, minutes]
|
|
||||||
translate: false
|
|
||||||
required: true
|
|
||||||
|
|
||||||
section5:
|
admin.lang:
|
||||||
type: header
|
type: select
|
||||||
label: '{{admin.options.system.admin-panel}}'
|
label: '{{admin.options.system.admin-panel.default-language}}'
|
||||||
|
translate: [label]
|
||||||
|
import:
|
||||||
|
options: 'Formwork\Admin\Admin::availableTranslations'
|
||||||
|
|
||||||
rows5:
|
admin.logout_redirect:
|
||||||
type: rows
|
type: togglegroup
|
||||||
fields:
|
label: '{{admin.options.system.admin-panel.logout-redirects-to}}'
|
||||||
row1:
|
options:
|
||||||
type: row
|
login: '{{admin.options.system.admin-panel.logout-redirects-to.login}}'
|
||||||
fields:
|
home: '{{admin.options.system.admin-panel.logout-redirects-to.home}}'
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.admin-panel.default-language}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
admin.lang:
|
|
||||||
type: select
|
|
||||||
translate: false
|
|
||||||
import:
|
|
||||||
options: 'Formwork\Admin\Admin::availableTranslations'
|
|
||||||
row2:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.admin-panel.logout-redirects-to}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
admin.logout_redirect:
|
|
||||||
type: togglegroup
|
|
||||||
options:
|
|
||||||
login: '{{admin.options.system.admin-panel.logout-redirects-to.login}}'
|
|
||||||
home: '{{admin.options.system.admin-panel.logout-redirects-to.home}}'
|
|
||||||
row3:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.admin-panel.session-timeout}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
admin.session_timeout:
|
|
||||||
type: duration
|
|
||||||
min: 0
|
|
||||||
unit: minutes
|
|
||||||
intervals: [hours, minutes]
|
|
||||||
translate: false
|
|
||||||
required: true
|
|
||||||
row4:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.admin-panel.default-color-scheme}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
admin.color_scheme:
|
|
||||||
type: togglegroup
|
|
||||||
options:
|
|
||||||
light: '{{admin.options.system.admin-panel.default-color-scheme.light}}'
|
|
||||||
dark: '{{admin.options.system.admin-panel.default-color-scheme.dark}}'
|
|
||||||
|
|
||||||
section6:
|
admin.session_timeout:
|
||||||
type: header
|
type: duration
|
||||||
label: '{{admin.options.system.images}}'
|
label: '{{admin.options.system.admin-panel.session-timeout}}'
|
||||||
|
min: 0
|
||||||
|
unit: minutes
|
||||||
|
intervals: [hours, minutes]
|
||||||
|
translate: [label]
|
||||||
|
required: true
|
||||||
|
|
||||||
rows6:
|
admin.color_scheme:
|
||||||
type: rows
|
type: togglegroup
|
||||||
fields:
|
label: '{{admin.options.system.admin-panel.default-color-scheme}}'
|
||||||
row1:
|
options:
|
||||||
type: row
|
light: '{{admin.options.system.admin-panel.default-color-scheme.light}}'
|
||||||
fields:
|
dark: '{{admin.options.system.admin-panel.default-color-scheme.dark}}'
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.images.jpeg-quality}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
images.jpeg_quality:
|
|
||||||
type: range
|
|
||||||
min: 0
|
|
||||||
max: 100
|
|
||||||
step: 5
|
|
||||||
row2:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.images.png-compression-level}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
images.png_compression:
|
|
||||||
type: range
|
|
||||||
min: 0
|
|
||||||
max: 9
|
|
||||||
row3:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.images.webp-quality}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
images.webp_quality:
|
|
||||||
type: range
|
|
||||||
min: 0
|
|
||||||
max: 100
|
|
||||||
step: 5
|
|
||||||
row4:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.images.jpeg-save-progressive}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
images.jpeg_progressive:
|
|
||||||
type: togglegroup
|
|
||||||
options:
|
|
||||||
1: '{{admin.options.system.images.jpeg-save-progressive.enabled}}'
|
|
||||||
0: '{{admin.options.system.images.jpeg-save-progressive.disabled}}'
|
|
||||||
row5:
|
|
||||||
type: row
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
width: 1-3
|
|
||||||
label: '{{admin.options.system.images.process-uploads}}'
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
width: 2-3
|
|
||||||
fields:
|
|
||||||
images.process_uploads:
|
|
||||||
type: togglegroup
|
|
||||||
options:
|
|
||||||
1: '{{admin.options.system.images.process-uploads.enabled}}'
|
|
||||||
0: '{{admin.options.system.images.process-uploads.disabled}}'
|
|
||||||
|
|
||||||
section7:
|
images.jpeg_quality:
|
||||||
type: header
|
type: range
|
||||||
label: '{{admin.options.system.backup}}'
|
label: '{{admin.options.system.images.jpeg-quality}}'
|
||||||
|
min: 0
|
||||||
|
max: 100
|
||||||
|
step: 5
|
||||||
|
|
||||||
rows7:
|
images.png_compression:
|
||||||
type: rows
|
type: range
|
||||||
fields:
|
label: '{{admin.options.system.images.png-compression-level}}'
|
||||||
row1:
|
min: 0
|
||||||
type: row
|
max: 9
|
||||||
fields:
|
|
||||||
column1:
|
images.webp_quality:
|
||||||
type: column
|
type: range
|
||||||
width: 1-3
|
label: '{{admin.options.system.images.webp-quality}}'
|
||||||
label: '{{admin.options.system.backup.backup-files-to-keep}}'
|
min: 0
|
||||||
column2:
|
max: 100
|
||||||
type: column
|
step: 5
|
||||||
width: 2-3
|
|
||||||
fields:
|
images.jpeg_progressive:
|
||||||
backup.max_files:
|
type: togglegroup
|
||||||
type: select
|
label: '{{admin.options.system.images.jpeg-save-progressive}}'
|
||||||
options:
|
options:
|
||||||
5: 5
|
1: '{{admin.options.system.images.jpeg-save-progressive.enabled}}'
|
||||||
10: 10
|
0: '{{admin.options.system.images.jpeg-save-progressive.disabled}}'
|
||||||
15: 15
|
|
||||||
20: 20
|
images.process_uploads:
|
||||||
|
type: togglegroup
|
||||||
|
label: '{{admin.options.system.images.process-uploads}}'
|
||||||
|
options:
|
||||||
|
1: '{{admin.options.system.images.process-uploads.enabled}}'
|
||||||
|
0: '{{admin.options.system.images.process-uploads.disabled}}'
|
||||||
|
|
||||||
|
backup.max_files:
|
||||||
|
type: select
|
||||||
|
label: '{{admin.options.system.backup.backup-files-to-keep}}'
|
||||||
|
options:
|
||||||
|
5: 5
|
||||||
|
10: 10
|
||||||
|
15: 15
|
||||||
|
20: 20
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
author: ''
|
||||||
|
description: ''
|
||||||
|
aliases:
|
||||||
|
/hello-world/: /blog/hello-world/
|
@ -0,0 +1,4 @@
|
|||||||
|
languages:
|
||||||
|
available: [it, en]
|
||||||
|
admin:
|
||||||
|
logout_redirect: home
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Hello World!
|
title: 'Hello World!'
|
||||||
publish-date: '2018-06-15'
|
publishDate: '2018-06-15 00:00:00'
|
||||||
---
|
---
|
||||||
This is an example of blog post.
|
This is an example of blog post.
|
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: 'Another Blog Post'
|
title: 'Another Blog Post'
|
||||||
publish-date: '2018-06-16'
|
publishDate: '2018-06-16 00:00:00'
|
||||||
---
|
---
|
||||||
This is the summary of another blog post.
|
This is the summary of another blog post.
|
||||||
|
|
||||||
|
@ -1,22 +1,32 @@
|
|||||||
title: Blog
|
title: Blog
|
||||||
|
|
||||||
|
# Extend the `page` scheme
|
||||||
extend: page
|
extend: page
|
||||||
|
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
|
# Specify the page type, in this case `listing`, which means it can show children
|
||||||
type: listing
|
type: listing
|
||||||
|
|
||||||
|
# This controls which template can have children, their order and other attributes
|
||||||
children:
|
children:
|
||||||
templates: [post, blog]
|
templates: [post, blog]
|
||||||
reverse: true
|
reverse: true
|
||||||
sortable: true
|
orderable: false
|
||||||
|
|
||||||
|
layout:
|
||||||
|
type: sections
|
||||||
|
sections:
|
||||||
|
options:
|
||||||
|
fields: [postsPerPage, published, publishDate, unpublishDate, routable, listed, cacheable]
|
||||||
|
|
||||||
fields:
|
fields:
|
||||||
tabs:
|
postsPerPage:
|
||||||
fields:
|
type: select
|
||||||
options-section:
|
label: '{{admin.pages.page.posts-per-page}}'
|
||||||
fields:
|
options:
|
||||||
posts-per-page:
|
5: 5
|
||||||
type: select
|
10: 10
|
||||||
options:
|
15: 15
|
||||||
5: 5
|
20: 20
|
||||||
10: 10
|
default: 5
|
||||||
15: 15
|
|
||||||
20: 20
|
|
||||||
default: 5
|
|
||||||
label: '{{admin.pages.page.posts-per-page}}'
|
|
||||||
|
@ -1,79 +1,91 @@
|
|||||||
title: Page
|
title: Page
|
||||||
|
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
|
layout:
|
||||||
|
type: sections
|
||||||
|
|
||||||
|
sections:
|
||||||
|
content:
|
||||||
|
label: '{{admin.pages.content}}'
|
||||||
|
active: true
|
||||||
|
fields: [title, content]
|
||||||
|
|
||||||
|
options:
|
||||||
|
collapsible: true
|
||||||
|
label: '{{admin.pages.options}}'
|
||||||
|
fields: [published, publishDate, unpublishDate, routable, listed, cacheable]
|
||||||
|
|
||||||
|
attributes:
|
||||||
|
collapsible: true
|
||||||
|
collapsed: true
|
||||||
|
label: '{{admin.pages.attributes}}'
|
||||||
|
fields: [parent, template]
|
||||||
|
|
||||||
|
files:
|
||||||
|
collapsible: true
|
||||||
|
collapsed: false
|
||||||
|
label: '{{admin.pages.files}}'
|
||||||
|
fields: [uploadedFile, files]
|
||||||
|
|
||||||
fields:
|
fields:
|
||||||
tabs:
|
title:
|
||||||
type: sections
|
type: text
|
||||||
fields:
|
class: input-large
|
||||||
|
required: true
|
||||||
|
|
||||||
content-section:
|
content:
|
||||||
type: section
|
type: markdown
|
||||||
label: '{{admin.pages.content}}'
|
label: '{{admin.pages.text}}'
|
||||||
active: true
|
|
||||||
fields:
|
|
||||||
title:
|
|
||||||
type: text
|
|
||||||
class: input-large
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
type: editor
|
|
||||||
|
|
||||||
options-section:
|
published:
|
||||||
type: section
|
type: checkbox
|
||||||
collapsible: true
|
label: '{{admin.pages.status.published}}'
|
||||||
collapsed: true
|
default: true
|
||||||
label: '{{admin.pages.options}}'
|
|
||||||
fields:
|
|
||||||
published:
|
|
||||||
type: checkbox
|
|
||||||
default: true
|
|
||||||
label: '{{admin.pages.status.published}}'
|
|
||||||
publish-date:
|
|
||||||
type: date
|
|
||||||
default: null
|
|
||||||
label: '{{admin.pages.page.publish-date}}'
|
|
||||||
placeholder: '{{admin.pages.page.no-date}}'
|
|
||||||
unpublish-date:
|
|
||||||
type: date
|
|
||||||
default: null
|
|
||||||
label: '{{admin.pages.page.unpublish-date}}'
|
|
||||||
placeholder: '{{admin.pages.page.no-date}}'
|
|
||||||
routable:
|
|
||||||
type: checkbox
|
|
||||||
default: true
|
|
||||||
label: '{{admin.pages.status.routable}}'
|
|
||||||
visible:
|
|
||||||
type: checkbox
|
|
||||||
default: true
|
|
||||||
label: '{{admin.pages.page.visible}}'
|
|
||||||
cacheable:
|
|
||||||
type: checkbox
|
|
||||||
default: true
|
|
||||||
label: '{{admin.pages.page.cacheable}}'
|
|
||||||
|
|
||||||
attributes-section:
|
publishDate:
|
||||||
type: section
|
type: date
|
||||||
collapsible: true
|
label: '{{admin.pages.page.publish-date}}'
|
||||||
collapsed: true
|
placeholder: '{{admin.pages.page.no-date}}'
|
||||||
label: '{{admin.pages.attributes}}'
|
default: null
|
||||||
fields:
|
|
||||||
parent:
|
|
||||||
type: page-parents
|
|
||||||
label: '{{admin.pages.parent}}'
|
|
||||||
template:
|
|
||||||
type: page.template
|
|
||||||
label: '{{admin.pages.template}}'
|
|
||||||
|
|
||||||
|
unpublishDate:
|
||||||
|
type: date
|
||||||
|
label: '{{admin.pages.page.unpublish-date}}'
|
||||||
|
placeholder: '{{admin.pages.page.no-date}}'
|
||||||
|
default: null
|
||||||
|
|
||||||
files-section:
|
routable:
|
||||||
type: section
|
type: checkbox
|
||||||
collapsible: true
|
label: '{{admin.pages.status.routable}}'
|
||||||
collapsed: false
|
default: true
|
||||||
label: '{{admin.pages.files}}'
|
|
||||||
fields:
|
listed:
|
||||||
uploaded-file:
|
type: checkbox
|
||||||
type: file
|
label: '{{admin.pages.page.listed}}'
|
||||||
auto-upload: true
|
default: true
|
||||||
multiple: true
|
|
||||||
files:
|
cacheable:
|
||||||
type: page-files
|
type: checkbox
|
||||||
|
label: '{{admin.pages.page.cacheable}}'
|
||||||
|
default: true
|
||||||
|
|
||||||
|
parent:
|
||||||
|
type: page.parents
|
||||||
|
access: panel
|
||||||
|
label: '{{admin.pages.parent}}'
|
||||||
|
|
||||||
|
template:
|
||||||
|
type: page.template
|
||||||
|
access: panel
|
||||||
|
label: '{{admin.pages.template}}'
|
||||||
|
|
||||||
|
uploadedFile:
|
||||||
|
type: file
|
||||||
|
access: panel
|
||||||
|
auto-upload: true
|
||||||
|
multiple: true
|
||||||
|
|
||||||
|
files:
|
||||||
|
type: page.files
|
||||||
|
access: panel
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user