├── .gitignore
├── sortable
├── actions
│ ├── delete
│ │ ├── views
│ │ │ └── delete.php
│ │ ├── forms
│ │ │ └── delete.php
│ │ ├── delete.php
│ │ └── controller.php
│ ├── add
│ │ ├── views
│ │ │ └── add.php
│ │ ├── add.php
│ │ ├── forms
│ │ │ └── add.php
│ │ └── controller.php
│ ├── copy
│ │ ├── views
│ │ │ └── copy.php
│ │ ├── copy.php
│ │ ├── controller.php
│ │ └── forms
│ │ │ └── copy.php
│ ├── paste
│ │ ├── views
│ │ │ └── paste.php
│ │ ├── paste.php
│ │ ├── forms
│ │ │ └── paste.php
│ │ └── controller.php
│ ├── edit
│ │ └── edit.php
│ ├── duplicate
│ │ ├── controller.php
│ │ └── duplicate.php
│ ├── toggle
│ │ ├── toggle.php
│ │ └── controller.php
│ └── base
│ │ └── base.php
├── helpers.php
├── src
│ ├── sortable
│ │ ├── roots.php
│ │ ├── registry
│ │ │ ├── translation.php
│ │ │ ├── action.php
│ │ │ ├── layout.php
│ │ │ └── variant.php
│ │ ├── controllers
│ │ │ ├── action.php
│ │ │ └── field.php
│ │ └── registry.php
│ └── sortable.php
├── layouts
│ ├── base
│ │ ├── template.php
│ │ └── base.php
│ └── module
│ │ ├── template.php
│ │ └── module.php
├── variants
│ ├── modules
│ │ ├── en.php
│ │ ├── sv_SE.php
│ │ ├── pt_BR.php
│ │ ├── de.php
│ │ └── fr.php
│ └── sections
│ │ ├── en.php
│ │ ├── pt_BR.php
│ │ ├── sv_SE.php
│ │ ├── de.php
│ │ └── fr.php
├── bootstrap.php
└── translations
│ ├── en.php
│ ├── sv_SE.php
│ ├── pt_BR.php
│ ├── fr.php
│ └── de.php
├── fields
├── modules
│ ├── controller.php
│ ├── assets
│ │ └── css
│ │ │ └── modules.css
│ ├── modules.php
│ ├── template.php
│ └── readme.md
├── sortable
│ ├── controller.php
│ ├── template.php
│ ├── assets
│ │ ├── css
│ │ │ └── sortable.css
│ │ └── js
│ │ │ └── sortable.js
│ └── sortable.php
├── redirect
│ ├── redirect.php
│ └── readme.md
└── options
│ └── options.php
├── package.json
├── sortable.php
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/sortable/actions/delete/views/delete.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/sortable/actions/add/views/add.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/sortable/helpers.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/sortable/actions/paste/views/paste.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/fields/modules/controller.php:
--------------------------------------------------------------------------------
1 | attr('href', $this->page()->url('edit'));
12 |
13 | return $content;
14 |
15 | }
16 |
17 | public function disabled() {
18 | return $this->disabled || $this->page()->ui()->read() === false;
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/fields/redirect/redirect.php:
--------------------------------------------------------------------------------
1 | redirect();
10 |
11 | if(is_null($redirect)) {
12 | return go(purl($this->page()->parent(), 'edit'));
13 | }
14 |
15 | $page = panel()->page($redirect);
16 |
17 | if(is_null($page)) {
18 | return go(purl());
19 | }
20 |
21 | return go(purl($page));
22 |
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/sortable/actions/delete/forms/delete.php:
--------------------------------------------------------------------------------
1 | array(
7 | 'label' => $field->l('field.sortable.delete.page.label'),
8 | 'type' => 'text',
9 | 'readonly' => true,
10 | 'default' => $page->title(),
11 | 'help' => $page->id(),
12 | )
13 | ));
14 |
15 | $form->cancel($model);
16 | $form->style('delete');
17 |
18 | return $form;
19 |
20 | };
21 |
--------------------------------------------------------------------------------
/sortable/src/sortable/roots.php:
--------------------------------------------------------------------------------
1 | index = $root;
14 |
15 | $this->translations = $this->index . DS . 'translations';
16 | $this->variants = $this->index . DS . 'variants';
17 | $this->layouts = $this->index . DS . 'layouts';
18 | $this->actions = $this->index . DS . 'actions';
19 |
20 | }
21 |
22 | public function __debuginfo() {
23 | return parent::__debuginfo();
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/sortable.php:
--------------------------------------------------------------------------------
1 | register();
9 |
10 | $kirby->set('field', 'sortable', __DIR__ . DS . 'fields' . DS . 'sortable');
11 | $kirby->set('field', 'options' , __DIR__ . DS . 'fields' . DS . 'options');
12 |
13 | if(c::get('sortable.field.redirect', true)) {
14 | $kirby->set('field', 'redirect', __DIR__ . DS . 'fields' . DS . 'redirect');
15 | }
16 |
17 | if(c::get('sortable.field.modules', true)) {
18 | $kirby->set('field', 'modules', __DIR__ . DS . 'fields' . DS . 'modules');
19 | }
20 |
--------------------------------------------------------------------------------
/sortable/actions/copy/copy.php:
--------------------------------------------------------------------------------
1 | '/',
13 | 'method' => 'POST|GET',
14 | 'action' => 'save',
15 | 'filter' => 'auth',
16 | ),
17 | );
18 | }
19 |
20 | public function content() {
21 |
22 | $content = parent::content();
23 | $content->data('modal', true);
24 |
25 | return $content;
26 |
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/sortable/layouts/base/template.php:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
--------------------------------------------------------------------------------
/fields/modules/modules.php:
--------------------------------------------------------------------------------
1 | array(
19 | 'modules.css',
20 | ),
21 | );
22 |
23 | public function parent() {
24 | return Kirby\Modules\Settings::parentUid();
25 | }
26 |
27 | public function prefix() {
28 | return Kirby\Modules\Settings::templatePrefix();
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/sortable/variants/modules/en.php:
--------------------------------------------------------------------------------
1 | 'No modules yet.',
5 | 'field.sortable.add.first' => 'Add the first module',
6 |
7 | 'field.sortable.add.template.label' => 'Add a new module',
8 |
9 | 'field.sortable.copy.info.label' => 'No modules yet',
10 | 'field.sortable.copy.info.text' => 'There are no modules you can store in the clipboard yet.',
11 | 'field.sortable.copy.error.uri' => 'Select at least one module',
12 |
13 | 'field.sortable.delete.page.label' => 'Do you really want to delete this module?',
14 |
15 | 'field.sortable.paste.info.text' => 'There are no modules stored in the clipboard at the moment.',
16 | 'field.sortable.paste.error.uri' => 'Select at least one module',
17 | );
18 |
--------------------------------------------------------------------------------
/sortable/variants/modules/sv_SE.php:
--------------------------------------------------------------------------------
1 | 'Inga moduler är skapade ännu.',
5 | 'field.sortable.add.first' => 'Lägg till första modulen',
6 |
7 | 'field.sortable.add.template.label' => 'Lägg till en ny modul',
8 |
9 | 'field.sortable.copy.info.label' => 'Inga moduler ännu',
10 | 'field.sortable.copy.info.text' => 'Det finns inga moduler som du kan spara i urklipp ännu.',
11 | 'field.sortable.copy.error.uri' => 'Välj minst en modul',
12 |
13 | 'field.sortable.delete.page.label' => 'Är du säker på att du vill ta bort den här modulen?',
14 |
15 | 'field.sortable.paste.info.text' => 'Det finns inga moduler sparade i urklipp.',
16 | 'field.sortable.paste.error.uri' => 'Välj minst en modul',
17 | );
18 |
--------------------------------------------------------------------------------
/sortable/variants/sections/en.php:
--------------------------------------------------------------------------------
1 | 'No sections yet.',
5 | 'field.sortable.add.first' => 'Add the first section',
6 |
7 | 'field.sortable.add.template.label' => 'Add a new section',
8 |
9 | 'field.sortable.copy.info.label' => 'No sections yet',
10 | 'field.sortable.copy.info.text' => 'There are no sections you can store in the clipboard yet.',
11 | 'field.sortable.copy.error.uri' => 'Select at least one section',
12 |
13 | 'field.sortable.delete.page.label' => 'Do you really want to delete this section?',
14 |
15 | 'field.sortable.paste.info.text' => 'There are no sections stored in the clipboard at the moment.',
16 | 'field.sortable.paste.error.uri' => 'Select at least one section',
17 | );
18 |
--------------------------------------------------------------------------------
/sortable/variants/modules/pt_BR.php:
--------------------------------------------------------------------------------
1 | 'Nenhum módulo até o momento.',
5 | 'field.sortable.add.first' => 'Adicione o primeiro módulo',
6 |
7 | 'field.sortable.add.template.label' => 'Adicionar um novo módulo',
8 |
9 | 'field.sortable.copy.info.label' => 'Nenhum módulo até o momento',
10 | 'field.sortable.copy.info.text' => 'Ainda não há nenhum módulo para você copiar.',
11 | 'field.sortable.copy.error.uri' => 'Selecione pelo menos um módulo',
12 |
13 | 'field.sortable.delete.page.label' => 'Você realmente deseja excluir este módulo?',
14 |
15 | 'field.sortable.paste.info.text' => 'Ainda não há nenhum módulo para você colar.',
16 | 'field.sortable.paste.error.uri' => 'Selecione pelo menos um módulo',
17 | );
18 |
--------------------------------------------------------------------------------
/sortable/variants/sections/pt_BR.php:
--------------------------------------------------------------------------------
1 | 'Nenhuma seção até o momento.',
5 | 'field.sortable.add.first' => 'Adicione a primeira seção',
6 |
7 | 'field.sortable.add.template.label' => 'Adicionar uma nova seção',
8 |
9 | 'field.sortable.copy.info.label' => 'Nenhuma seção até o momento',
10 | 'field.sortable.copy.info.text' => 'Ainda não há nenhuma seção para você copiar.',
11 | 'field.sortable.copy.error.uri' => 'Selecione pelo menos uma seção',
12 |
13 | 'field.sortable.delete.page.label' => 'Você realmente deseja excluir esta seção?',
14 |
15 | 'field.sortable.paste.info.text' => 'Ainda não há nenhuma seção para você colar.',
16 | 'field.sortable.paste.error.uri' => 'Selecione pelo menos uma seção',
17 | );
18 |
--------------------------------------------------------------------------------
/sortable/actions/add/add.php:
--------------------------------------------------------------------------------
1 | '/',
13 | 'method' => 'POST|GET',
14 | 'action' => 'add',
15 | 'filter' => 'auth',
16 | ),
17 | );
18 | }
19 |
20 | public function content() {
21 |
22 | $content = parent::content();
23 | $content->data('modal', true);
24 |
25 | return $content;
26 |
27 | }
28 |
29 | public function disabled() {
30 | return $this->disabled || $this->parent()->ui()->create() === false;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/sortable/variants/sections/sv_SE.php:
--------------------------------------------------------------------------------
1 | 'Inga sektioner är skapade ännu.',
5 | 'field.sortable.add.first' => 'Lägg till första sektionen',
6 |
7 | 'field.sortable.add.template.label' => 'Lägg till en ny sektion',
8 |
9 | 'field.sortable.copy.info.label' => 'Inga sektioner ännu',
10 | 'field.sortable.copy.info.text' => 'Det finns inga sektioner som du kan spara i urklipp ännu.',
11 | 'field.sortable.copy.error.uri' => 'Välj minst en sektion',
12 |
13 | 'field.sortable.delete.page.label' => 'Är du säker på att du vill ta bort den här sektionen?',
14 |
15 | 'field.sortable.paste.info.text' => 'Det finns inga sektioner sparade i urklipp.',
16 | 'field.sortable.paste.error.uri' => 'Välj minst en sektion',
17 | );
18 |
--------------------------------------------------------------------------------
/sortable/actions/paste/paste.php:
--------------------------------------------------------------------------------
1 | '/',
13 | 'method' => 'POST|GET',
14 | 'action' => 'paste',
15 | 'filter' => 'auth',
16 | ),
17 | );
18 | }
19 |
20 | public function content() {
21 |
22 | $content = parent::content();
23 | $content->data('modal', true);
24 |
25 | return $content;
26 |
27 | }
28 |
29 | public function disabled() {
30 | return $this->disabled || $this->parent()->ui()->create() === false;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/sortable/actions/duplicate/controller.php:
--------------------------------------------------------------------------------
1 | field()->entries();
14 | $parent = $this->field()->origin();
15 | $page = $entries->find($uid);
16 |
17 | if($parent->ui()->create() === false) {
18 | throw new Kirby\Panel\Exceptions\PermissionsException();
19 | }
20 |
21 | $page = $this->copy($page, $parent);
22 |
23 | $entries->add($page->uid());
24 | $this->sort($page->uid(), $to);
25 | $this->notify(':)');
26 | $this->redirect($this->model());
27 |
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/sortable/variants/modules/de.php:
--------------------------------------------------------------------------------
1 | 'Keine Module vorhanden.',
5 | 'field.sortable.add.first' => 'Füge das erste Modul hinzu',
6 |
7 | 'field.sortable.add.template.label' => 'Ein neues Module hinzufügen',
8 |
9 | 'field.sortable.copy.info.label' => 'Keine Module vorhanden',
10 | 'field.sortable.copy.info.text' => 'Es gibt noch keine Module die du in der Zwischenablage speichern kannst.',
11 | 'field.sortable.copy.error.uri' => 'Wähle mindestens ein Modul aus',
12 |
13 | 'field.sortable.delete.page.label' => 'Willst du dieses Modul wirklich löschen?',
14 |
15 | 'field.sortable.paste.info.text' => 'In der Zwischenablage befinden sich derzeit keine Module.',
16 | 'field.sortable.paste.error.uri' => 'Wähle mindestens ein Modul aus',
17 | );
18 |
--------------------------------------------------------------------------------
/sortable/layouts/module/template.php:
--------------------------------------------------------------------------------
1 |
2 | preview === true || $layout->preview === 'top') echo $layout->preview(); ?>
3 |
15 | preview === 'bottom') echo $layout->preview(); ?>
16 |
17 |
--------------------------------------------------------------------------------
/sortable/variants/sections/de.php:
--------------------------------------------------------------------------------
1 | 'Keine Sektionen vorhanden.',
5 | 'field.sortable.add.first' => 'Füge die erste Sektion hinzu',
6 |
7 | 'field.sortable.add.template.label' => 'Eine neue Sektion hinzufügen',
8 |
9 | 'field.sortable.copy.info.label' => 'Keine Sektionen vorhanden',
10 | 'field.sortable.copy.info.text' => 'Es gibt noch keine Sektionen die du in der Zwischenablage speichern kannst.',
11 | 'field.sortable.copy.error.uri' => 'Wähle mindestens eine Sektion aus',
12 |
13 | 'field.sortable.delete.page.label' => 'Willst du diese Sektion wirklich löschen?',
14 |
15 | 'field.sortable.paste.info.text' => 'In der Zwischenablage befinden sich derzeit keine Seiten.',
16 | 'field.sortable.paste.error.uri' => 'Wähle mindestens eine Seite aus',
17 | );
18 |
--------------------------------------------------------------------------------
/fields/sortable/template.php:
--------------------------------------------------------------------------------
1 |
6 |
7 | entries()->count()): ?>
8 | = $field->layouts(); ?>
9 |
10 |
11 | = $field->l('field.sortable.empty'); ?>
12 | = $field->action('add', ['label' => $field->l('field.sortable.add.first'), 'icon' => '', 'class' => '']); ?>
13 | = $field->l('field.sortable.or'); ?>
14 | = $field->action('paste', ['label' => $field->l('field.sortable.paste.first'), 'icon' => '', 'class' => '']); ?>
15 |
16 |
17 |
18 |
19 | = $field->action('copy'); ?>
20 | = $field->action('paste'); ?>
21 | = $field->action('add'); ?>
22 |
23 |
--------------------------------------------------------------------------------
/sortable/variants/modules/fr.php:
--------------------------------------------------------------------------------
1 | 'Aucun module pour le moment.',
5 | 'field.sortable.add.first' => 'Ajouter le premier module',
6 |
7 | 'field.sortable.add.template.label' => 'Ajouter un nouveau module',
8 |
9 | 'field.sortable.copy.info.label' => 'Aucun module pour le moment',
10 | 'field.sortable.copy.info.text' => 'Il n\'y a pas encore de modules que vous puissiez stocker dans le presse-papiers.',
11 | 'field.sortable.copy.error.uri' => 'Selectionner au moins un module',
12 |
13 | 'field.sortable.delete.page.label' => 'Voulez-vous vraiment effacer ce module ?',
14 |
15 | 'field.sortable.paste.info.text' => 'Il n\'y a pas de modules stockés dans le presse-papiers pour le moment.',
16 | 'field.sortable.paste.error.uri' => 'Selectionner au moins un module',
17 | );
18 |
--------------------------------------------------------------------------------
/sortable/variants/sections/fr.php:
--------------------------------------------------------------------------------
1 | 'Aucune section pour le moment.',
5 | 'field.sortable.add.first' => 'Ajouter la première section',
6 |
7 | 'field.sortable.add.template.label' => 'Ajouter une nouvelle section',
8 |
9 | 'field.sortable.copy.info.label' => 'Aucune section pour le moment',
10 | 'field.sortable.copy.info.text' => 'Il n\'y a pas encore de sections que vous puissiez stocker dans le presse-papiers.',
11 | 'field.sortable.copy.error.uri' => 'Selectionner au moins une section',
12 |
13 | 'field.sortable.delete.page.label' => 'Voulez-vous vraiment effacer cette section ?',
14 |
15 | 'field.sortable.paste.info.text' => 'Il n\'y a pas de sections stockées dans le presse-papiers pour le moment.',
16 | 'field.sortable.paste.error.uri' => 'Selectionner au moins une section',
17 | );
18 |
--------------------------------------------------------------------------------
/sortable/actions/duplicate/duplicate.php:
--------------------------------------------------------------------------------
1 | '(:any)/(:any)',
12 | 'method' => 'POST|GET',
13 | 'action' => 'duplicate',
14 | 'filter' => 'auth',
15 | ),
16 | );
17 | }
18 |
19 | public function content() {
20 |
21 | $content = parent::content();
22 | $content->attr('href', $this->url() . '/' . $this->page()->uid() . '/' . ($this->layout()->num() + 1));
23 | $content->data('action', true);
24 |
25 | return $content;
26 |
27 | }
28 |
29 | public function disabled() {
30 | return $this->disabled || $this->field()->origin()->ui()->create() === false;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/sortable/actions/delete/delete.php:
--------------------------------------------------------------------------------
1 | '(:any)',
12 | 'method' => 'POST|GET',
13 | 'action' => 'delete',
14 | 'filter' => 'auth',
15 | ),
16 | );
17 | }
18 |
19 | public function content() {
20 |
21 | $content = parent::content();
22 | $content->attr('href', $this->url() . '/' . $this->page()->uid());
23 | $content->data('modal', true);
24 |
25 | return $content;
26 |
27 | }
28 |
29 | public function disabled() {
30 | $page = $this->page();
31 | return $this->disabled || $page->blueprint()->options()->delete() === false || $page->ui()->delete() === false;
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/sortable/actions/delete/controller.php:
--------------------------------------------------------------------------------
1 | field()->entries()->find($uid);
14 |
15 | if($page->ui()->delete() === false) {
16 | throw new Kirby\Panel\Exceptions\PermissionsException();
17 | }
18 |
19 | $form = $this->form('delete', array($page, $this->model(), $this->field()), function($form) use($page, $self) {
20 |
21 | try {
22 |
23 | $page->delete();
24 | $self->update($self->field()->entries()->not($page)->pluck('uid'));
25 | $self->notify(':)');
26 | $self->redirect($self->model());
27 |
28 | } catch(Exception $e) {
29 | $form->alert($e->getMessage());
30 | }
31 |
32 | });
33 |
34 | return $this->modal('delete', compact('form'));
35 |
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/sortable/bootstrap.php:
--------------------------------------------------------------------------------
1 | 'sortable.php',
7 |
8 | // global stuff
9 | 'lukaskleinschmidt\\sortable\\registry' => 'sortable' . DS . 'registry.php',
10 | 'lukaskleinschmidt\\sortable\\roots' => 'sortable' . DS . 'roots.php',
11 |
12 | // controllers
13 | 'lukaskleinschmidt\\sortable\\controllers\\field' => 'sortable' . DS . 'controllers' . DS . 'field.php',
14 | 'lukaskleinschmidt\\sortable\\controllers\\action' => 'sortable' . DS . 'controllers' . DS . 'action.php',
15 |
16 | ], __DIR__ . DS . 'src' );
17 |
18 | class_alias('LukasKleinschmidt\\Sortable\\Sortable', 'LukasKleinschmidt\\Sortable');
19 |
20 | // TEMP: Added for convenience because those two classes and namespaces were used in v2.3.1 and below
21 | class_alias('LukasKleinschmidt\\Sortable\\Controllers\\Field', 'Kirby\\Sortable\\Controllers\\Field');
22 | class_alias('LukasKleinschmidt\\Sortable\\Controllers\\Action', 'Kirby\\Sortable\\Controllers\\Action');
23 |
24 | include(__DIR__ . DS . 'helpers.php');
25 |
--------------------------------------------------------------------------------
/fields/modules/template.php:
--------------------------------------------------------------------------------
1 |
6 |
7 | entries()->count()): ?>
8 | = $field->layouts(); ?>
9 |
10 |
11 | l('field.sortable.empty');
13 | if($field->add()) {
14 | echo $field->action('add', ['label' => $field->l('field.sortable.add.first'), 'icon' => '', 'class' => '']);
15 | if($field->paste()) {
16 | echo $field->l('field.sortable.or');
17 | echo $field->action('paste', ['label' => $field->l('field.sortable.paste.first'), 'icon' => '', 'class' => '']);
18 | }
19 | }
20 | ?>
21 |
22 |
23 |
24 |
25 | copy()) echo $field->action('copy');
27 | if($field->add()) {
28 | if($field->paste()) echo $field->action('paste');
29 | echo $field->action('add');
30 | }
31 | ?>
32 |
33 |
--------------------------------------------------------------------------------
/sortable/actions/add/forms/add.php:
--------------------------------------------------------------------------------
1 | blueprint()->pages()->template();
7 |
8 | if($prefix = $field->prefix()) {
9 | $templates = $templates->filter(function($template) use($prefix) {
10 | return str::startsWith($template, $prefix);
11 | });
12 | }
13 |
14 | foreach($templates as $template) {
15 | $options[$template->name()] = $template->title();
16 | }
17 |
18 | $form = new Kirby\Panel\Form(array(
19 | 'template' => array(
20 | 'label' => $field->l('field.sortable.add.template.label'),
21 | 'type' => 'select',
22 | 'options' => $options,
23 | 'default' => key($options),
24 | 'required' => true,
25 | 'readonly' => count($options) == 1 ? true : false,
26 | 'icon' => count($options) == 1 ? $page->blueprint()->pages()->template()->first()->icon() : 'chevron-down',
27 | )
28 | ));
29 |
30 | $form->cancel($model);
31 | $form->buttons->submit->val($field->l('field.sortable.add'));
32 |
33 | return $form;
34 |
35 | };
36 |
--------------------------------------------------------------------------------
/fields/options/options.php:
--------------------------------------------------------------------------------
1 | error) {
13 | $input->attr('checked', v::accepted($this->value()) || v::accepted($data->checked()));
14 | }
15 |
16 | if($data->readonly()) {
17 | $input->attr('disabled', true);
18 | $input->attr('readonly', true);
19 | $input->addClass('input-is-readonly');
20 | }
21 |
22 | return $input;
23 |
24 | }
25 |
26 | public function item($value, $data) {
27 |
28 | $data = new Obj($data);
29 | $input = $this->input($value, $data);
30 |
31 | $label = new Brick('label', '' . $this->i18n($data->label()) . '');
32 | $label->addClass('input input-with-checkbox');
33 | $label->attr('data-focus', 'true');
34 | $label->prepend($input);
35 |
36 | if($data->readonly()) {
37 | $label->addClass('input-is-readonly');
38 | }
39 |
40 | return $label;
41 |
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/sortable/actions/copy/controller.php:
--------------------------------------------------------------------------------
1 | field()->origin();
12 | $entries = $this->field()->entries();
13 |
14 | $form = $this->form('copy', array($page, $entries, $this->model(), $this->field()), function($form) use($page, $self) {
15 |
16 | try {
17 |
18 | $form->validate();
19 |
20 | if(!$form->isValid()) {
21 | throw new Exception($self->field()->l('field.sortable.copy.error.uri'));
22 | }
23 |
24 | $data = $form->serialize();
25 |
26 | site()->user()->update(array(
27 | 'clipboard' => str::split($data['uri']),
28 | ));
29 |
30 | $self->notify(':)');
31 | $self->redirect($this->model());
32 |
33 | } catch(Exception $e) {
34 | $form->alert($e->getMessage());
35 | }
36 |
37 | });
38 |
39 | return $this->modal('copy', compact('form'));
40 |
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/sortable/src/sortable/registry/translation.php:
--------------------------------------------------------------------------------
1 | kirby->option('debug') || is_file($path)) {
24 | return static::$translations[$name] = $path;
25 | }
26 |
27 | throw new Exception('The translation does not exist at the specified path: ' . $root);
28 |
29 | }
30 |
31 | /**
32 | * Retreives a registered translation file
33 | * If called without params, retrieves all registered translations
34 | *
35 | * @param string $name
36 | * @return mixed
37 | */
38 | public function get($name = null) {
39 |
40 | if(is_null($name)) {
41 | return static::$translations;
42 | }
43 |
44 | return a::get(static::$translations, $name);
45 |
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/fields/redirect/readme.md:
--------------------------------------------------------------------------------
1 | # Kirby Redirect Field
2 |
3 | This field redirects a user to the parent of the currently visited panel page.
4 | Useful for pages that act like a container.
5 |
6 | ## Blueprint
7 | ```yml
8 | fields:
9 | title:
10 | label: Title
11 | type: text
12 |
13 | redirect:
14 | label: Redirect
15 | type: redirect
16 | ...
17 | ```
18 |
19 | ## Redirect to a specific page
20 |
21 | Redirect to `panel/projects/project-a/edit`.
22 | When the page is not found you will get redirected to the dashboard.
23 |
24 | ```yml
25 | redirect:
26 | label: Redirect
27 | type: redirect
28 |
29 | redirect: projects/project-a
30 | ```
31 |
32 | ## Hide pages
33 | There are several ways to hide pages in the panel. The easyiest way would be to set `hide: true` in the pages [blueprint](https://getkirby.com/docs/panel/blueprints/page-settings#hide-page). This would hide a page in the sidebar. To get rid of the breadcrumb link you can put something like this to your [custom panel css](https://getkirby.com/docs/developer-guide/panel/css).
34 |
35 | ```css
36 | .breadcrumb-link[title="your-page-title"] {
37 | display: none;
38 | }
39 | ```
40 |
--------------------------------------------------------------------------------
/sortable/src/sortable/controllers/action.php:
--------------------------------------------------------------------------------
1 | model = $model;
13 | $this->field = $field;
14 | $this->action = $action;
15 |
16 | }
17 |
18 | public function form($id, $data = array(), $submit = null) {
19 | $file = $this->action->root() . DS . 'forms' . DS . $id . '.php';
20 | return panel()->form($file, $data, $submit);
21 | }
22 |
23 | public function view($file, $data = array()) {
24 |
25 | $view = new View($file, $data);
26 | $root = $this->action->root() . DS . 'views';
27 |
28 | if(file_exists($root . DS . $file . '.php')) {
29 | $view->_root = $root;
30 | }
31 |
32 | return $view;
33 |
34 | }
35 |
36 | public function snippet($file, $data = array()) {
37 |
38 | $snippet = new Snippet($file, $data);
39 | $root = $this->action->root() . DS . 'snippets';
40 |
41 | if(file_exists($root . DS . $file . '.php')) {
42 | $snippet->_root = $root;
43 | }
44 |
45 | return $snippet;
46 |
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/sortable/actions/copy/forms/copy.php:
--------------------------------------------------------------------------------
1 | count()) {
6 |
7 | $options = [];
8 |
9 | foreach($entries as $entry) {
10 | $options[$entry->uri()] = array(
11 | 'label' => $entry->title(),
12 | 'checked' => true,
13 | 'readonly' => false,
14 | );
15 | }
16 |
17 | $form = new Kirby\Panel\Form(array(
18 | 'uri' => array(
19 | 'label' => $field->l('field.sortable.copy.uri.label'),
20 | 'type' => 'options',
21 | 'columns' => 1,
22 | 'required' => true,
23 | 'options' => $options,
24 | )
25 | ));
26 |
27 | } else {
28 |
29 | $form = new Kirby\Panel\Form(array(
30 | 'info' => array(
31 | 'label' => $field->l('field.sortable.copy.info.label'),
32 | 'type' => 'info',
33 | 'text' => $field->l('field.sortable.copy.info.text')
34 | )
35 | ));
36 |
37 | }
38 |
39 | $form->cancel($model);
40 | $form->buttons->submit->val($field->l('field.sortable.copy'));
41 |
42 | if(!$entries->count()) {
43 | $form->buttons->submit = $form->buttons->cancel;
44 | $form->style('centered');
45 | }
46 |
47 | return $form;
48 |
49 | };
50 |
--------------------------------------------------------------------------------
/sortable/layouts/module/module.php:
--------------------------------------------------------------------------------
1 | page();
15 |
16 | if(!$preview = $this->preview) {
17 | return;
18 | }
19 |
20 | $entry = Kirby\Modules\Modules::instance()->get($page);
21 | $template = $entry->path() . DS . $entry->name() . '.preview.php';
22 |
23 | if(!is_file($template)) {
24 | return;
25 | }
26 |
27 | $position = $preview === true ? 'top' : $preview;
28 |
29 | $preview = new Brick('div');
30 | $preview->addClass('modules-layout__preview modules-layout__preview--' . $position);
31 | $preview->data('module', $entry->name());
32 | $preview->data('handle', true);
33 | $preview->html(tpl::load($template, array('page' => $this->origin(), 'module' => $page, 'moduleName' => $entry->name())));
34 |
35 | return $preview;
36 |
37 | }
38 |
39 | public function action($type, $data = array()) {
40 |
41 | $data = a::update($data, array(
42 | 'disabled' => $this->$type() === false
43 | ));
44 |
45 | return parent::action($type, $data);
46 |
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/sortable/src/sortable/registry/action.php:
--------------------------------------------------------------------------------
1 | kirby->option('debug') || (is_dir($root) && is_file($file))) {
27 | return static::$actions[$name] = new Obj([
28 | 'root' => $root,
29 | 'file' => $file,
30 | 'name' => $name,
31 | 'class' => $name . 'action',
32 | ]);
33 | }
34 |
35 | throw new Exception('The action does not exist at the specified path: ' . $root);
36 |
37 | }
38 |
39 | /**
40 | * Retreives a registered action file
41 | * If called without params, retrieves all registered actions
42 | *
43 | * @param string $name
44 | * @return mixed
45 | */
46 | public function get($name = null) {
47 |
48 | if(is_null($name)) {
49 | return static::$actions;
50 | }
51 |
52 | return a::get(static::$actions, $name);
53 |
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/sortable/src/sortable/registry/layout.php:
--------------------------------------------------------------------------------
1 | kirby->option('debug') || (is_dir($root) && is_file($file))) {
27 | return static::$layouts[$name] = new Obj([
28 | 'root' => $root,
29 | 'file' => $file,
30 | 'name' => $name,
31 | 'class' => $name . 'layout',
32 | ]);
33 | }
34 |
35 | throw new Exception('The layout does not exist at the specified path: ' . $root);
36 |
37 | }
38 |
39 | /**
40 | * Retreives a registered layout file
41 | * If called without params, retrieves all registered layouts
42 | *
43 | * @param string $name
44 | * @return mixed
45 | */
46 | public function get($name = null) {
47 |
48 | if(is_null($name)) {
49 | return static::$layouts;
50 | }
51 |
52 | return a::get(static::$layouts, $name);
53 |
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/sortable/src/sortable/registry.php:
--------------------------------------------------------------------------------
1 | kirby = $kirby;
17 |
18 | // start the registry entry autoloader
19 | load([
20 | 'lukaskleinschmidt\\sortable\\registry\\translation' => __DIR__ . DS . 'registry' . DS . 'translation.php',
21 | 'lukaskleinschmidt\\sortable\\registry\\variant' => __DIR__ . DS . 'registry' . DS . 'variant.php',
22 | 'lukaskleinschmidt\\sortable\\registry\\layout' => __DIR__ . DS . 'registry' . DS . 'layout.php',
23 | 'lukaskleinschmidt\\sortable\\registry\\action' => __DIR__ . DS . 'registry' . DS . 'action.php',
24 | ]);
25 |
26 | }
27 |
28 | /**
29 | * Returns a registry entry object by type
30 | *
31 | * @param string $type
32 | * @param string $subtype
33 | * @return Kirby\Registry\Entry
34 | */
35 | public function entry($type, $subtype = null) {
36 |
37 | $class = 'lukaskleinschmidt\\sortable\\registry\\' . $type;
38 |
39 | if(!class_exists('lukaskleinschmidt\\sortable\\registry\\' . $type)) {
40 | throw new Exception('Unsupported registry entry type: ' . $type);
41 | }
42 |
43 | return new $class($this);
44 |
45 | }
46 |
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/sortable/actions/add/controller.php:
--------------------------------------------------------------------------------
1 | field()->origin();
12 |
13 | if($parent->ui()->create() === false) {
14 | throw new Kirby\Panel\Exceptions\PermissionsException();
15 | }
16 |
17 | $form = $this->form('add', array($parent, $this->model(), $this->field()), function($form) use($parent, $self) {
18 |
19 | try {
20 |
21 | $form->validate();
22 |
23 | if(!$form->isValid()) {
24 | throw new Exception($self->field()->l('field.sortable.add.error.template'));
25 | }
26 |
27 | $data = $form->serialize();
28 | $template = $data['template'];
29 |
30 | $page = $parent->children()->create($self->uid($template), $template, array(
31 | 'title' => i18n($parent->blueprint()->pages()->template()->findBy('name', $template)->title())
32 | ));
33 |
34 | $self->update($self->field()->entries()->pluck('uid'));
35 | $self->notify(':)');
36 | $self->redirect($self->model());
37 | // $this->redirect($page, 'edit');
38 |
39 | } catch(Exception $e) {
40 | $form->alert($e->getMessage());
41 | }
42 |
43 | });
44 |
45 | return $this->modal('add', compact('form'));
46 |
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/sortable/src/sortable/registry/variant.php:
--------------------------------------------------------------------------------
1 | kirby->option('debug') || is_dir($root)) {
29 | foreach(dir::read($root) as $file) {
30 | static::$variants[f::name($file)][$name] = $root . DS . $file;
31 | }
32 |
33 | return static::$variants;
34 | // return static::$variants[$name] = new Obj([
35 | // 'root' => $root,
36 | // 'name' => $name,
37 | // 'files' => $files,
38 | // ]);
39 | }
40 |
41 | throw new Exception('The variant does not exist at the specified path: ' . $root);
42 |
43 | }
44 |
45 | /**
46 | * Retreives a registered variant file
47 | * If called without params, retrieves all registered variants
48 | *
49 | * @param string $name
50 | * @return mixed
51 | */
52 | public function get($name = null) {
53 |
54 | if(is_null($name)) {
55 | return static::$variants;
56 | }
57 |
58 | return a::get(static::$variants, $name);
59 |
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/sortable/translations/en.php:
--------------------------------------------------------------------------------
1 | 'No pages yet.',
5 | 'field.sortable.or' => 'or',
6 | 'field.sortable.add.first' => 'Add the first page',
7 | 'field.sortable.paste.first' => 'add from clipboard',
8 |
9 | 'field.sortable.limit' => 'Limit reached',
10 | 'field.sortable.limit.template' => 'Limit reached for this template',
11 |
12 | 'field.sortable.add' => 'Add',
13 | 'field.sortable.add.template.label' => 'Add a new page',
14 | 'field.sortable.add.error.template' => 'The template is missing',
15 |
16 | 'field.sortable.copy' => 'Copy',
17 | 'field.sortable.copy.uri.label' => 'Copy to clipboard',
18 | 'field.sortable.copy.info.label' => 'No pages yet',
19 | 'field.sortable.copy.info.text' => 'There are no pages you can store in the clipboard yet.',
20 | 'field.sortable.copy.error.uri' => 'Select at least one page',
21 |
22 | 'field.sortable.delete' => 'Delete',
23 | 'field.sortable.delete.page.label' => 'Do you really want to delete this page?',
24 |
25 | 'field.sortable.duplicate' => 'Duplicate',
26 | 'field.sortable.edit' => 'Edit',
27 |
28 | 'field.sortable.paste' => 'Paste',
29 | 'field.sortable.paste.uri.label' => 'Paste from clipboard',
30 | 'field.sortable.paste.uri.help' => 'One or more templates are not available',
31 | 'field.sortable.paste.info.label' => 'Clipboard is empty',
32 | 'field.sortable.paste.info.text' => 'There are no pages stored in the clipboard at the moment.',
33 | 'field.sortable.paste.error.uri' => 'Select at least one page',
34 |
35 | 'field.sortable.hide' => 'Hide',
36 | 'field.sortable.show' => 'Show',
37 | );
38 |
--------------------------------------------------------------------------------
/sortable/translations/sv_SE.php:
--------------------------------------------------------------------------------
1 | 'Inga inlägg är skapade ännu.',
5 | 'field.sortable.or' => 'eller',
6 | 'field.sortable.add.first' => 'Lägg till första inlägget',
7 | 'field.sortable.paste.first' => 'lägg till från urklipp',
8 |
9 | 'field.sortable.limit' => 'Gräns nådd',
10 | 'field.sortable.limit.template' => 'Gräns nådd för den här mallen',
11 |
12 | 'field.sortable.add' => 'Lägg till',
13 | 'field.sortable.add.template.label' => 'Lägg till nytt inlägg',
14 | 'field.sortable.add.error.template' => 'Mallen saknas',
15 |
16 | 'field.sortable.copy' => 'Kopiera',
17 | 'field.sortable.copy.uri.label' => 'Kopiera till urklipp',
18 | 'field.sortable.copy.info.label' => 'Inga inlägg ännu',
19 | 'field.sortable.copy.info.text' => 'Det finns inga inlägg som du kan spara i urklipp ännu.',
20 | 'field.sortable.copy.error.uri' => 'Välj minst ett inlägg',
21 |
22 | 'field.sortable.delete' => 'Ta bort',
23 | 'field.sortable.delete.page.label' => 'Är du säker på att du vill ta bort det här inlägget?',
24 |
25 | 'field.sortable.duplicate' => 'Duplicera',
26 | 'field.sortable.edit' => 'Ändra',
27 |
28 | 'field.sortable.paste' => 'Klistra in',
29 | 'field.sortable.paste.uri.label' => 'Klistra in från urklipp',
30 | 'field.sortable.paste.uri.help' => 'En eller fler mallar är inte tillgängliga',
31 | 'field.sortable.paste.info.label' => 'Urklipp är tomt',
32 | 'field.sortable.paste.info.text' => 'Det finns inga inlägg sparade i urklipp.',
33 | 'field.sortable.paste.error.uri' => 'Välj minst ett inlägg',
34 |
35 | 'field.sortable.hide' => 'Göm',
36 | 'field.sortable.show' => 'Visa',
37 | );
38 |
--------------------------------------------------------------------------------
/sortable/translations/pt_BR.php:
--------------------------------------------------------------------------------
1 | 'Nenhuma página até o momento.',
5 | 'field.sortable.or' => 'ou',
6 | 'field.sortable.add.first' => 'Adicione a primeira página',
7 | 'field.sortable.paste.first' => 'adicionar a partir da área de transferência',
8 |
9 | 'field.sortable.limit' => 'Limite alcançado',
10 | 'field.sortable.limit.template' => 'Limite alcançado para este template',
11 |
12 | 'field.sortable.add' => 'Adicionar',
13 | 'field.sortable.add.template.label' => 'Adicionar uma nova página',
14 | 'field.sortable.add.error.template' => 'Template não encontrado',
15 |
16 | 'field.sortable.copy' => 'Copiar',
17 | 'field.sortable.copy.uri.label' => 'Copiar para a área de transferência',
18 | 'field.sortable.copy.info.label' => 'Nenhuma página até o momento',
19 | 'field.sortable.copy.info.text' => 'Ainda não há nenhuma página para você copiar.',
20 | 'field.sortable.copy.error.uri' => 'Selecione pelo menos uma página',
21 |
22 | 'field.sortable.delete' => 'Excluir',
23 | 'field.sortable.delete.page.label' => 'Você realmente deseja excluir esta página?',
24 |
25 | 'field.sortable.duplicate' => 'Duplicar',
26 | 'field.sortable.edit' => 'Editar',
27 |
28 | 'field.sortable.paste' => 'Colar',
29 | 'field.sortable.paste.uri.label' => 'Colar da área de transferência',
30 | 'field.sortable.paste.uri.help' => 'Um ou mais templates não estão disponíveis',
31 | 'field.sortable.paste.info.label' => 'Área de transferência vazia',
32 | 'field.sortable.paste.info.text' => 'Ainda não há nenhuma página para você colar.',
33 | 'field.sortable.paste.error.uri' => 'Selecione pelo menos uma página',
34 |
35 | 'field.sortable.hide' => 'Esconder',
36 | 'field.sortable.show' => 'Mostrar',
37 | );
38 |
--------------------------------------------------------------------------------
/sortable/translations/fr.php:
--------------------------------------------------------------------------------
1 | 'Aucune page pour le moment.',
5 | 'field.sortable.or' => 'ou',
6 | 'field.sortable.add.first' => 'Ajouter la première page',
7 | 'field.sortable.paste.first' => 'ajouter du presse-papiers',
8 |
9 | 'field.sortable.limit' => 'Limite atteinte',
10 | 'field.sortable.limit.template' => 'Limite atteinte pour ce modèle',
11 |
12 | 'field.sortable.add' => 'Ajouter',
13 | 'field.sortable.add.template.label' => 'Ajouter une nouvelle page',
14 | 'field.sortable.add.error.template' => 'Le modèle est manquant',
15 |
16 | 'field.sortable.copy' => 'Copier',
17 | 'field.sortable.copy.uri.label' => 'Copier dans le presse-papiers',
18 | 'field.sortable.copy.info.label' => 'Aucune page pour le moment',
19 | 'field.sortable.copy.info.text' => 'Il n\'y a aucune page que vous pouvez stocker dans le presse-papiers pour le moment.',
20 | 'field.sortable.copy.error.uri' => 'Selectionner au moins une page',
21 |
22 | 'field.sortable.delete' => 'Effacer',
23 | 'field.sortable.delete.page.label' => 'Voulez-vous vraiment effacer cette page ?',
24 |
25 | 'field.sortable.duplicate' => 'Dupliquer',
26 | 'field.sortable.edit' => 'Editer',
27 |
28 | 'field.sortable.paste' => 'Coller',
29 | 'field.sortable.paste.uri.label' => 'Coller du presse-papiers',
30 | 'field.sortable.paste.uri.help' => 'Un ou plusieurs modèle ne sont pas disponibles',
31 | 'field.sortable.paste.info.label' => 'Le presse-papiers et vide',
32 | 'field.sortable.paste.info.text' => 'Il n\y a aucune page stockée dans le presse-papiers pour le moment.',
33 | 'field.sortable.paste.error.uri' => 'Selectionner au moins une page',
34 |
35 | 'field.sortable.hide' => 'Cacher',
36 | 'field.sortable.show' => 'Montrer',
37 | );
38 |
--------------------------------------------------------------------------------
/sortable/translations/de.php:
--------------------------------------------------------------------------------
1 | 'Keine Seiten vorhanden.',
5 | 'field.sortable.or' => 'oder',
6 | 'field.sortable.add.first' => 'Füge die erste Seite hinzu',
7 | 'field.sortable.paste.first' => 'aus der Zwischenablage hinzufügen',
8 |
9 | 'field.sortable.limit' => 'Limit erreicht',
10 | 'field.sortable.limit.template' => 'Limit für diese Vorlage erreicht',
11 |
12 | 'field.sortable.add' => 'Hinzufügen',
13 | 'field.sortable.add.template.label' => 'Eine neue Seite hinzufügen',
14 | 'field.sortable.add.error.template' => 'Die Vorlage fehlt',
15 |
16 | 'field.sortable.copy' => 'Kopieren',
17 | 'field.sortable.copy.uri.label' => 'In die Zwischenablage kopieren',
18 | 'field.sortable.copy.info.label' => 'Keine Seiten vorhanden',
19 | 'field.sortable.copy.info.text' => 'Es gibt noch keine Seiten die du in der Zwischenablage speichern kannst.',
20 | 'field.sortable.copy.error.uri' => 'Wähle mindestens eine Seite aus',
21 |
22 | 'field.sortable.delete' => 'Löschen',
23 | 'field.sortable.delete.page.label' => 'Willst du diese Seite wirklich löschen?',
24 |
25 | 'field.sortable.duplicate' => 'Duplizieren',
26 | 'field.sortable.edit' => 'Bearbeiten',
27 |
28 | 'field.sortable.paste' => 'Einfügen',
29 | 'field.sortable.paste.uri.label' => 'Aus Zwischenablage einfügen',
30 | 'field.sortable.paste.uri.help' => 'Eine oder mehrere Vorlagen sind nicht verfügbar',
31 | 'field.sortable.paste.info.label' => 'Die Zwischenablage ist leer',
32 | 'field.sortable.paste.info.text' => 'In der Zwischenablage befinden sich derzeit keine Seiten.',
33 | 'field.sortable.paste.error.uri' => 'Wähle mindestens eine Seite aus',
34 |
35 | 'field.sortable.hide' => 'Verstecken',
36 | 'field.sortable.show' => 'Anzeigen',
37 | );
38 |
--------------------------------------------------------------------------------
/sortable/actions/toggle/toggle.php:
--------------------------------------------------------------------------------
1 | 'toggle-on',
8 | 'show' => 'toggle-off',
9 | );
10 | public $title = array(
11 | 'hide' => 'field.sortable.hide',
12 | 'show' => 'field.sortable.show',
13 | );
14 |
15 | public function routes() {
16 | return array(
17 | array(
18 | 'pattern' => 'show/(:any)/(:any)',
19 | 'method' => 'POST|GET',
20 | 'action' => 'show',
21 | 'filter' => 'auth',
22 | ),
23 | array(
24 | 'pattern' => 'hide/(:any)',
25 | 'method' => 'POST|GET',
26 | 'action' => 'hide',
27 | 'filter' => 'auth',
28 | ),
29 | );
30 | }
31 |
32 | public function status() {
33 | if($this->status) return $this->status;
34 | return $this->status = $this->page()->isVisible() ? 'hide' : 'show';
35 | }
36 |
37 | public function title() {
38 | $title = a::get($this->title, $this->status());
39 | return $this->i18n($title);
40 | }
41 |
42 | public function icon($position = null) {
43 | if(empty($this->icon)) return null;
44 | $icon = a::get($this->icon, $this->status());
45 | return icon($icon, $position);
46 | }
47 |
48 | public function content() {
49 |
50 | $content = parent::content();
51 | $content->data('action', true);
52 | $content->attr('href', $this->url() . '/' . $this->status() . '/' . $this->page()->uid());
53 |
54 | if($this->status() == 'show') {
55 | $content->attr('href', $content->attr('href') . '/' . ($this->layout()->numVisible() + 1));
56 | }
57 |
58 | return $content;
59 |
60 | }
61 |
62 | public function disabled() {
63 | return $this->disabled || $this->page()->ui()->visibility() === false;
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/sortable/actions/paste/forms/paste.php:
--------------------------------------------------------------------------------
1 | count()) {
6 |
7 | $templates = $page->blueprint()->pages()->template();
8 | $options = [];
9 | $help = false;
10 |
11 | foreach($entries as $entry) {
12 | $template = $entry->intendedTemplate();
13 | $value = $entry->uri();
14 |
15 | $options[$value] = array(
16 | // 'label' => icon($templates->findBy('name', $template)->icon(), 'left') . ' ' . $entry->title(),
17 | 'label' => $entry->title(),
18 | 'checked' => true,
19 | 'readonly' => false,
20 | );
21 |
22 | if(v::notIn($template, $templates->pluck('name'))) {
23 | $options[$value]['checked'] = false;
24 | $options[$value]['readonly'] = true;
25 |
26 | $help = true;
27 | }
28 | }
29 |
30 | $form = new Kirby\Panel\Form(array(
31 | 'uri' => array(
32 | 'label' => $field->l('field.sortable.paste.uri.label'),
33 | 'type' => 'options',
34 | 'columns' => 1,
35 | 'required' => true,
36 | 'options' => $options,
37 | 'help' => $help ? $field->l('field.sortable.paste.uri.help') : '',
38 | )
39 | ));
40 |
41 | } else {
42 |
43 | $form = new Kirby\Panel\Form(array(
44 | 'info' => array(
45 | 'label' => $field->l('field.sortable.paste.info.label'),
46 | 'type' => 'info',
47 | 'text' => $field->l('field.sortable.paste.info.text')
48 | )
49 | ));
50 |
51 | }
52 |
53 | $form->cancel($model);
54 | $form->buttons->submit->val($field->l('field.sortable.paste'));
55 |
56 | if(!$entries->count()) {
57 | $form->buttons->submit = $form->buttons->cancel;
58 | $form->style('centered');
59 | }
60 |
61 | return $form;
62 |
63 | };
64 |
--------------------------------------------------------------------------------
/sortable/actions/paste/controller.php:
--------------------------------------------------------------------------------
1 | field()->origin();
12 | $entries = site()->user()->clipboard();
13 |
14 | if(empty($entries)) {
15 | $entries = array();
16 | }
17 |
18 | $entries = pages($entries);
19 |
20 | if($parent->ui()->create() === false) {
21 | throw new Kirby\Panel\Exceptions\PermissionsException();
22 | }
23 |
24 | $form = $this->form('paste', array($parent, $entries, $this->model(), $this->field()), function($form) use($parent, $self) {
25 |
26 | try {
27 |
28 | $form->validate();
29 |
30 | if(!$form->isValid()) {
31 | throw new Exception($self->field()->l('field.sortable.paste.error.uri'));
32 | }
33 |
34 | $data = $form->serialize();
35 |
36 | $templates = $parent->blueprint()->pages()->template()->pluck('name');
37 | $entries = $self->field()->entries();
38 | $to = $entries->count();
39 |
40 | foreach(pages(str::split($data['uri'], ',')) as $page) {
41 |
42 | if(!in_array($page->intendedTemplate(), $templates)) continue;
43 |
44 | // Reset previously triggered hooks
45 | kirby()::$triggered = array();
46 |
47 | $page = $self->copy($page, $parent);
48 |
49 | $entries->add($page->uid());
50 | $self->sort($page->uid(), ++$to);
51 |
52 | }
53 |
54 | $self->notify(':)');
55 | $self->redirect($self->model());
56 |
57 | } catch(Exception $e) {
58 | $form->alert($e->getMessage());
59 | }
60 |
61 | });
62 |
63 | return $this->modal('paste', compact('form'));
64 |
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/sortable/actions/toggle/controller.php:
--------------------------------------------------------------------------------
1 | field()->entries();
16 | $page = $entries->find($uid);
17 |
18 | if($page->ui()->visibility() === false) {
19 | throw new PermissionsException();
20 | }
21 |
22 | try {
23 |
24 | // Check template specific limit
25 | $count = $entries->filterBy('template', $page->intendedTemplate())->visible()->count();
26 | $limit = $this->field()->options($page)->limit();
27 |
28 | if($limit && $count >= $limit) {
29 | throw new Exception($this->field()->l('field.sortable.limit.template'));
30 | }
31 |
32 | // Check limit
33 | $count = $entries->visible()->count();
34 | $limit = $this->field()->limit();
35 |
36 | if($limit && $count >= $limit) {
37 | throw new Exception($this->field()->l('field.sortable.limit'));
38 | }
39 |
40 | $page->sort($to);
41 | $this->notify(':)');
42 |
43 | } catch(Exception $e) {
44 | $this->alert($e->getMessage());
45 | }
46 |
47 | $this->redirect($this->model());
48 |
49 | }
50 |
51 | /**
52 | * Hide page
53 | *
54 | * @param string $uid
55 | */
56 | public function hide($uid) {
57 |
58 | $page = $this->field()->entries()->find($uid);
59 |
60 | if($page->ui()->visibility() === false) {
61 | throw new PermissionsException();
62 | }
63 |
64 | try {
65 | $page->hide();
66 | $this->notify(':)');
67 | } catch(Exception $e) {
68 | $this->alert($e->getMessage());
69 | }
70 |
71 | $this->redirect($this->model());
72 |
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/sortable/actions/base/base.php:
--------------------------------------------------------------------------------
1 | getFileName());
15 | }
16 |
17 | public function __construct($type) {
18 | $this->type = $type;
19 | }
20 |
21 | public function __call($method, $arguments) {
22 | return isset($this->$method) ? $this->$method : null;
23 | }
24 |
25 | public function type() {
26 | return $this->type;
27 | }
28 |
29 | public function url() {
30 | return $this->field()->url($this->type);
31 | }
32 |
33 | public function i18n($value) {
34 |
35 | if(is_array($value)) {
36 | return i18n($value);
37 | }
38 |
39 | return $this->field()->l($value);
40 |
41 | }
42 |
43 | public function l($key, $variant = null) {
44 | return $this->field()->l($key, $variant);
45 | }
46 |
47 | public function icon($position = null) {
48 | if(!$this->icon) return null;
49 | return icon($this->icon, $position);
50 | }
51 |
52 | public function label() {
53 | if(!$this->label) return null;
54 | return $this->i18n($this->label);
55 | }
56 |
57 | public function title() {
58 | if(!$this->title) return null;
59 | return $this->i18n($this->title);
60 | }
61 |
62 | public function content() {
63 |
64 | $a = new Brick('a');
65 |
66 | $a->addClass($this->class());
67 | $a->attr('title', $this->title());
68 | $a->attr('href', $this->url());
69 |
70 | if($this->disabled()) {
71 | $a->addClass('is-disabled');
72 | }
73 |
74 | if($label = $this->label()) {
75 | $a->append($this->icon('left') . $label);
76 | } else {
77 | $a->append($this->icon());
78 | }
79 |
80 | return $a;
81 |
82 | }
83 |
84 | public function __toString() {
85 | try {
86 | return (string)$this->content();
87 | } catch(Exception $e) {
88 | return (string)$e->getMessage();
89 | }
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/sortable/layouts/base/base.php:
--------------------------------------------------------------------------------
1 | getFileName());
10 | }
11 |
12 | public function __construct($type) {
13 | $this->type = $type;
14 | }
15 |
16 | public function __call($method, $arguments) {
17 | return isset($this->$method) ? $this->$method : null;
18 | }
19 |
20 | public function type() {
21 | return $this->type;
22 | }
23 |
24 | public function input() {
25 | return $this->field()->input($this->page()->uid());
26 | }
27 |
28 | public function counter() {
29 |
30 | $page = $this->page();
31 |
32 | if(!$page->isVisible() || !$this->limit()) {
33 | return null;
34 | }
35 |
36 | $entries = $this->field()->entries()->filterBy('template', $page->intendedTemplate());
37 | $index = $entries->visible()->indexOf($page) + 1;
38 | $limit = $this->limit();
39 |
40 | $counter = new Brick('span');
41 | $counter->addClass('sortable-layout__counter');
42 | $counter->html('( ' . $index . ' / ' . $limit . ' )');
43 |
44 | return $counter;
45 |
46 | }
47 |
48 | public function l($key, $variant = null) {
49 | return $this->field()->l($key, $variant);
50 | }
51 |
52 | public function icon($position = '') {
53 | return $this->page()->icon($position);
54 | }
55 |
56 | public function blueprint() {
57 | return $this->page()->blueprint();
58 | }
59 |
60 | public function title() {
61 | return $this->page()->title();
62 | }
63 |
64 | public function action($type, $data = array()) {
65 |
66 | $data = a::update($data, array(
67 | 'layout' => $this,
68 | 'page' => $this->page(),
69 | ));
70 |
71 | return $this->field()->action($type, $data);
72 |
73 | }
74 |
75 | public function content() {
76 |
77 | $template = $this->root() . DS . 'template.php';
78 |
79 | if(!is_file($template)) {
80 | $template = __DIR__ . DS . 'template.php';
81 | }
82 |
83 | return tpl::load($template, ['layout' => $this], true);
84 |
85 | }
86 |
87 | public function template() {
88 |
89 | $template = new Brick('div');
90 | $template->addClass('sortable__layout');
91 | $template->attr('data-uid', $this->page()->uid());
92 | $template->attr('data-visible', $this->page()->isVisible() ? 'true' : 'false');
93 | $template->append($this->content());
94 | $template->append($this->input());
95 |
96 | return $template;
97 |
98 | }
99 |
100 | public function __toString() {
101 | try {
102 | return (string)$this->template();
103 | } catch(Exception $e) {
104 | return (string)$e->getMessage();
105 | }
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/fields/sortable/assets/css/sortable.css:
--------------------------------------------------------------------------------
1 | /* TEMP: until https://github.com/getkirby/panel/pull/986 is resolved */
2 | .form-blueprint-home > fieldset {
3 | min-width: 0;
4 | }
5 |
6 | @-moz-document url-prefix() {
7 | .form-blueprint-home > fieldset {
8 | display: table-cell;
9 | }
10 | }
11 |
12 | .sortable__counter {
13 | padding-left: 0.25em;
14 | font-size: 0.8em;
15 | font-weight: 400;
16 | color: #bbb;
17 | line-height: 1;
18 | }
19 |
20 | .sortable__empty {
21 | background: #ddd;
22 | padding: 1.5em;
23 | }
24 |
25 | .sortable__empty a {
26 | border-bottom: 2px solid #bbb;
27 | margin: 0 0.25em;
28 | }
29 |
30 | .sortable__empty a:hover {
31 | border-color: #000;
32 | }
33 |
34 | .sortable__empty a:first-child {
35 | margin-left: 0.5em;
36 | }
37 |
38 | .sortable__navigation {
39 | padding-top: 0.5em;
40 | position: relative;
41 | }
42 |
43 | .sortable__action {
44 | color: #777;
45 | font-weight: 400;
46 | line-height: 1.5em;
47 | }
48 |
49 | .sortable__action .icon,
50 | .sortable__action:hover {
51 | color: #000;
52 | }
53 |
54 | .sortable__action--add {
55 | position: absolute;
56 | right: 0;
57 | }
58 |
59 | .sortable__action:nth-child(n+2) {
60 | margin-left: 1em;
61 | }
62 |
63 | .sortable__action.is-disabled {
64 | pointer-events: none;
65 | color: #c9c9c9;
66 | }
67 |
68 | .sortable__layout {
69 | margin-bottom: 0.25em;
70 | }
71 |
72 | .sortable-layout a,
73 | .sortable-layout button,
74 | .sortable__action.is-disabled .icon {
75 | color: inherit;
76 | }
77 |
78 | .sortable-layout {
79 | background: #fff;
80 | border: 2px solid #ddd;
81 | position: relative;
82 | }
83 |
84 | .sortable-layout .icon {
85 | vertical-align: -5%;
86 | }
87 |
88 | .sortable-layout .icon:not(.icon-left) {
89 | width: 1.28571em;
90 | }
91 |
92 | .sortable-layout__icon {
93 | display: inline;
94 | }
95 |
96 | .sortable-layout__counter {
97 | padding-left: 0.25em;
98 | font-size: 0.8em;
99 | color: #bbb;
100 | line-height: 1;
101 | }
102 |
103 | .sortable__container[data-sortable="true"] .sortable-layout [data-handle] {
104 | cursor: move;
105 | }
106 |
107 | .sortable-layout__navigation {
108 | min-height: 44px;
109 |
110 | display: -webkit-flex;
111 | display: -ms-flexbox;
112 | display: flex;
113 | -webkit-flex-flow: row nowrap;
114 | -ms-flex-flow: row nowrap;
115 | flex-flow: row nowrap;
116 | }
117 |
118 | .sortable-layout__icon,
119 | .sortable-layout__action,
120 | .sortable-layout__title {
121 | padding: 0.65em 0.9em 0.55em;
122 | white-space: nowrap;
123 | line-height: 1.5em;
124 | font-size: 1em;
125 | }
126 |
127 | .sortable-layout__icon {
128 | padding-right: 0.35em;
129 | }
130 |
131 | .sortable-layout__title {
132 | -webkit-flex: 1 0 0%;
133 | -ms-flex: 1 0 0%;
134 | flex: 1 0 0%;
135 | overflow: hidden;
136 | text-overflow: ellipsis;
137 | padding-left: 0.35em;
138 | }
139 |
140 | .sortable-layout[data-visible=false] .sortable-layout__icon,
141 | .sortable-layout[data-visible=false] .sortable-layout__title {
142 | color: #c9c9c9;
143 | }
144 |
145 | .sortable-layout__action {
146 | -webkit-flex: 0 0 auto;
147 | -ms-flex: 0 0 auto;
148 | flex: 0 0 auto;
149 | border: none;
150 | border-left: 1px solid #efefef;
151 | background: 0 0;
152 | cursor: pointer;
153 | outline: 0;
154 | }
155 |
156 | .sortable-layout__action.is-disabled {
157 | pointer-events: none;
158 | color: #c9c9c9;
159 | }
160 |
--------------------------------------------------------------------------------
/sortable/src/sortable.php:
--------------------------------------------------------------------------------
1 | kirby = kirby();
25 |
26 | $this->roots = new Roots(dirname(__DIR__));
27 | $this->registry = new Registry($this->kirby());
28 |
29 | }
30 |
31 | public static function instance() {
32 | if(!is_null(static::$instance)) return static::$instance;
33 | return static::$instance = new static();
34 | }
35 |
36 | public static function layout($type, $data = array()) {
37 |
38 | $class = $type . 'layout';
39 |
40 | if(!class_exists($class)) {
41 | throw new Exception('The ' . $type . ' layout is missing.');
42 | }
43 |
44 | $layout = new $class($type);
45 |
46 | foreach($data as $key => $val) {
47 | if(!is_string($key) || str::length($key) === 0) continue;
48 | $layout->{$key} = $val;
49 | }
50 |
51 | return $layout;
52 |
53 | }
54 |
55 | public static function action($type, $data = array()) {
56 |
57 | $class = $type . 'action';
58 |
59 | if(!class_exists($class)) {
60 | throw new Exception('The ' . $type . ' action is missing.');
61 | }
62 |
63 | $action = new $class($type);
64 |
65 | foreach($data as $key => $val) {
66 | if(!is_string($key) || str::length($key) === 0) continue;
67 | $action->{$key} = $val;
68 | }
69 |
70 | return $action;
71 |
72 | }
73 |
74 | public function translation($key, $variant = null) {
75 |
76 | // IDEA: outsource into own class
77 | // $variants = $this->variants();
78 | // return $variants->get($key, $variant, l::get($key));
79 |
80 | if(!is_null($variant) && $variant = a::get($this->variants, $variant)) {
81 | return a::get($variant, $key, l::get($key, $key));
82 | }
83 |
84 | return l::get($key, $key);
85 |
86 | }
87 |
88 | public function register() {
89 |
90 | foreach(dir::read($this->roots()->translations()) as $name) {
91 | $this->set('translation', f::name($name), $this->roots()->translations() . DS . $name);
92 | }
93 |
94 | foreach(dir::read($this->roots()->variants()) as $name) {
95 | $this->set('variant', $name, $this->roots()->variants() . DS . $name);
96 | }
97 |
98 | foreach(dir::read($this->roots()->layouts()) as $name) {
99 | $this->set('layout', $name, $this->roots()->layouts() . DS . $name);
100 | }
101 |
102 | foreach(dir::read($this->roots()->actions()) as $name) {
103 | $this->set('action', $name, $this->roots()->actions() . DS . $name);
104 | }
105 |
106 | }
107 |
108 | public function load() {
109 |
110 | $code = panel()->translation()->code();
111 |
112 | if(!$path = $this->get('translation', $code)) {
113 | $path = $this->get('translation', 'en');
114 | }
115 |
116 | l::set(data::read($path));
117 |
118 | if($variants = $this->get('variant', $code)) {
119 | foreach($variants as $name => $path) {
120 | $this->variants[$name] = data::read($path);
121 | }
122 | }
123 |
124 | $classes = [];
125 |
126 | foreach($this->get('layout') as $name => $layout) {
127 | $classes[$layout->class()] = $layout->file();
128 | }
129 |
130 | foreach($this->get('action') as $name => $action) {
131 | $classes[$action->class()] = $action->file();
132 | }
133 |
134 | load($classes);
135 |
136 | }
137 |
138 | public function kirby() {
139 | return $this->kirby;
140 | }
141 |
142 | public function roots() {
143 | return $this->roots;
144 | }
145 |
146 | public function set() {
147 | return call_user_func_array([$this->registry, 'set'], func_get_args());
148 | }
149 |
150 | public function get() {
151 | return call_user_func_array([$this->registry, 'get'], func_get_args());
152 | }
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/fields/modules/readme.md:
--------------------------------------------------------------------------------
1 | # Kirby Modules Field
2 |
3 | This field extends the `sortable` field and adds some presets and features to help you manage your modules.
4 | To use this field you have to install the [modules-plugin](https://github.com/getkirby-plugins/modules-plugin).
5 |
6 | ### Some of the features
7 | - Preset `parent` and `prefix` to match those set by the modules-plugin
8 | - Adds a [preview](#preview) if provided
9 |
10 | 
11 |
12 | ## Blueprint
13 |
14 | After installing the plugin, you can use the new field type `modules`.
15 | This blueprint shows all available options and their defaults.
16 |
17 | ```yml
18 | fields:
19 | title:
20 | label: Title
21 | type: text
22 |
23 | modules:
24 | label: Modules
25 | type: modules
26 |
27 | add: true
28 | copy: true
29 | paste: true
30 | limit: false
31 | variant: modules
32 |
33 | actions:
34 | - edit
35 | - duplicate
36 | - delete
37 | - toggle
38 |
39 | options:
40 | preview: true
41 | limit: false
42 | edit: true
43 | duplicate: true
44 | delete: true
45 | toggle: true
46 | ...
47 | ```
48 |
49 | ## Examples
50 |
51 | The following examples show and explain some of the possible settings.
52 |
53 | ### Preview
54 |
55 | A preview is a normal PHP file with the HTML and PHP code that defines your preview. The preview has access to the following variables:
56 |
57 | - `$page` is the page on which the module appears
58 | - `$module` is the module subpage, which you can use to access the fields from your module blueprint as well as module files
59 | - `$moduleName` is the name of the module such as text or gallery
60 |
61 | The preview file must be in the same folder as the module itself.
62 | The module directory looks something like this:
63 |
64 | ```
65 | site/modules/
66 | gallery/
67 | gallery.html.php
68 | gallery.yml
69 |
70 | # The preview file
71 | gallery.preview.php
72 | ...
73 | ```
74 |
75 | ### Preview options
76 |
77 | Previews are enabled by default. Set `preview` to `false` to disable the preview.
78 | It is also possible to change the position in the module.
79 |
80 | ```yml
81 | options:
82 | # Render the preview at the bottom
83 | preview: bottom
84 |
85 | # Or at the top
86 | preview: top
87 | preview: true
88 | ```
89 |
90 |
91 | ### Limit the number of visible modules
92 |
93 | ```yml
94 | modules_field:
95 | label: Modules
96 | type: modules
97 |
98 | # Allow 3 visible modules overall
99 | limit: 3
100 |
101 | # Template specific option
102 | options:
103 | module.gallery:
104 | # Allow only 1 visible gallery module
105 | limit: 1
106 | ```
107 |
108 | 
109 |
110 | ### Is it a module or a section?
111 |
112 | Change the naming.
113 |
114 | ```yml
115 | # Modules is fine
116 | variant: modules
117 |
118 | # Nah sections it is
119 | variant: sections
120 | ```
121 |
122 | ### Changing actions
123 |
124 | To change the actions or remove an action completely from the modules, you must specify the `actions` array in the blueprint.
125 |
126 | ```yml
127 | # Default
128 | actions:
129 | - edit
130 | - duplicate
131 | - delete
132 | - toggle
133 | ```
134 |
135 | 
136 |
137 | ```yml
138 | actions:
139 | - edit
140 | - toggle
141 | ```
142 |
143 | 
144 |
145 | ### Disabling an action
146 |
147 | ```yml
148 | options:
149 | edit: false
150 | duplicate: false
151 | delete: true
152 | toggle: true
153 |
154 | # Template specific options
155 | module.gallery:
156 | edit: true
157 | duplicate: true
158 | ```
159 |
160 | 
161 |
162 | ## Requirements
163 |
164 | - [Kirby Modules Plugin](https://github.com/getkirby-plugins/modules-plugin) 1.3+
165 |
--------------------------------------------------------------------------------
/sortable/src/sortable/controllers/field.php:
--------------------------------------------------------------------------------
1 | model();
26 | $field = $this->field();
27 |
28 | $action = sortable::action($type);
29 | $routes = $action->routes();
30 | $router = new Router($routes);
31 |
32 | if($route = $router->run($path)) {
33 | if(is_callable($route->action()) && is_a($route->action(), 'Closure')) {
34 | return call($route->action(), $route->arguments());
35 | } else {
36 |
37 | $controllerFile = $action->root() . DS . 'controller.php';
38 | $controllerName = $type . 'ActionController';
39 |
40 | if(!file_exists($controllerFile)) {
41 | throw new Exception('The action controller file is missing');
42 | }
43 |
44 | require_once($controllerFile);
45 |
46 | if(!class_exists($controllerName)) {
47 | throw new Exception('The action controller class is missing');
48 | }
49 |
50 | $controller = new $controllerName($model, $field, $action);
51 |
52 | return call(array($controller, $route->action()), $route->arguments());
53 |
54 | }
55 |
56 | } else {
57 | throw new Exception('Invalid action route');
58 | }
59 |
60 | }
61 |
62 | /**
63 | * Update field value and sort number
64 | *
65 | * @param string $uid
66 | * @param int $to
67 | */
68 | public function sort($uid, $to) {
69 |
70 | try {
71 | $entries = $this->field()->entries();
72 | $value = $entries->not($uid)->pluck('uid');
73 |
74 | // Order entries value
75 | array_splice($value, $to - 1, 0, $uid);
76 |
77 | if($entries->find($uid)->ui()->visibility() === false) {
78 | throw new PermissionsException();
79 | }
80 |
81 | // Update field value
82 | $this->update($value);
83 | } catch(Exception $e) {
84 | $this->alert($e->getMessage());
85 | }
86 |
87 | // Get current page
88 | $page = $entries->find($uid);
89 |
90 | // Figure out the correct sort num
91 | if($page && $page->isVisible()) {
92 | $collection = new Children($page->parent());
93 |
94 | foreach(array_slice($value, 0, $to - 1) as $id) {
95 | if($entry = $entries->find($id)) {
96 | $collection->data[$entry->id()] = $entry;
97 | }
98 | }
99 |
100 | try {
101 | // Sort the page
102 | $page->sort($collection->visible()->count() + 1);
103 | } catch(Exception $e) {
104 | $this->alert($e->getMessage());
105 | }
106 | }
107 |
108 | }
109 |
110 | /**
111 | * Copy a page to a new location
112 | *
113 | * @param object $page
114 | * @param object $to
115 | * @return object
116 | */
117 | public function copy($page, $to) {
118 |
119 | $template = $page->intendedTempalte();
120 | $blueprint = new Blueprint($template);
121 | $parent = $to;
122 | $data = array();
123 | $uid = $this->uid($page);
124 |
125 | foreach($blueprint->fields(null) as $key => $field) {
126 | $data[$key] = $field->default();
127 | }
128 |
129 | $data = array_merge($data, $page->content()->toArray());
130 | $event = $parent->event('create:action', [
131 | 'parent' => $parent,
132 | 'template' => $template,
133 | 'blueprint' => $blueprint,
134 | 'uid' => $uid,
135 | 'data' => $data
136 | ]);
137 |
138 | $event->check();
139 |
140 | // Actually copy the page
141 | dir::copy($page->root(), $parent->root() . DS . $uid);
142 |
143 | $page = $parent->children()->find($uid);
144 |
145 | if(!$page) {
146 | throw new Exception(l('pages.add.error.create'));
147 | }
148 |
149 | kirby()->trigger($event, $page);
150 |
151 | return $page;
152 |
153 | }
154 |
155 | /**
156 | * Update the field value
157 | *
158 | * @param array $value
159 | */
160 | public function update($value) {
161 |
162 | try {
163 | $this->model()->update(array(
164 | $this->field()->name() => implode(', ', $value)
165 | ));
166 | } catch(Exception $e) {
167 | $this->alert($e->getMessage());
168 | }
169 |
170 | }
171 |
172 | /**
173 | * Create unique uid
174 | *
175 | * @param string $template
176 | * @return string
177 | */
178 | public function uid($template) {
179 |
180 | if(is_a($template, 'Page')) {
181 | $template = $template->intendedTemplate();
182 | }
183 |
184 | $prefix = $this->field()->prefix();
185 |
186 | if($prefix && strpos($template, $prefix) !== false) {
187 | $length = str::length($prefix);
188 | $template = str::substr($template, $length);
189 | }
190 |
191 | // add a unique hash
192 | $checksum = sprintf('%u', crc32($template . microtime()));
193 | return $template . '-' . base_convert($checksum, 10, 36);
194 |
195 | }
196 |
197 |
198 | }
199 |
--------------------------------------------------------------------------------
/fields/sortable/assets/js/sortable.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 |
3 | var Sort = function(element) {
4 | var self = this;
5 |
6 | this.element = $(element);
7 | this.container = $('.sortable__container[data-sortable="true"]', element);
8 | this.api = this.element.data('api');
9 |
10 | this.container._sortable({
11 | handle: '[data-handle]',
12 | start: function(event, ui) {
13 | self.container._sortable('refreshPositions');
14 | self.blur();
15 | },
16 | update: function(event, ui) {
17 | var to = self.container.children().index(ui.item) + 1;
18 | var uid = ui.item.data('uid');
19 | var action = [self.api, uid, to, 'sort'].join('/');
20 |
21 | // disable sorting because a reload is expected
22 | self.disable();
23 |
24 | $.post(action, self.reload.bind(self));
25 | }
26 | });
27 |
28 | this.element.on('click', '[data-action]', function(event) {
29 | var element = $(this);
30 | var action = element.data('action') || element.attr('href');
31 |
32 | $.post(action, self.reload.bind(self));
33 |
34 | return false;
35 | });
36 |
37 | // setup extended field script
38 | var key = this.element.data('field-extended');
39 | if(key != 'sortable' && this.element[key]) this.element[key]();
40 | };
41 |
42 | Sort.prototype.blur = function() {
43 | $('.form input:focus, .form select:focus, .form textarea:focus').blur();
44 | app.content.focus.forget();
45 | };
46 |
47 | Sort.prototype.disable = function() {
48 | this.container._sortable('disable');
49 | };
50 |
51 | Sort.prototype.reload = function() {
52 | this.disable();
53 | app.content.reload();
54 | };
55 |
56 |
57 | // Fixing scrollbar jumping issue
58 | // http://stackoverflow.com/questions/1735372/jquery-sortable-list-scroll-bar-jumps-up-when-sorting
59 | if (typeof($.ui._sortable) === 'undefined') {
60 | $.widget('ui._sortable', $.ui.sortable, {
61 | _mouseStart: function(event, overrideHandle, noActivation) {
62 | var i, body,
63 | o = this.options;
64 |
65 | this.currentContainer = this;
66 |
67 | //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
68 | this.refreshPositions();
69 |
70 | //Create and append the visible helper
71 | this.helper = this._createHelper(event);
72 |
73 | //Cache the helper size
74 | this._cacheHelperProportions();
75 |
76 | /*
77 | * - Position generation -
78 | * This block generates everything position related - it's the core of draggables.
79 | */
80 |
81 | //Cache the margins of the original element
82 | this._cacheMargins();
83 |
84 | //Get the next scrolling parent
85 | this.scrollParent = this.helper.scrollParent();
86 |
87 | //The element's absolute position on the page minus margins
88 | this.offset = this.currentItem.offset();
89 | this.offset = {
90 | top: this.offset.top - this.margins.top,
91 | left: this.offset.left - this.margins.left
92 | };
93 |
94 | $.extend(this.offset, {
95 | click: { //Where the click happened, relative to the element
96 | left: event.pageX - this.offset.left,
97 | top: event.pageY - this.offset.top
98 | },
99 | parent: this._getParentOffset(),
100 | relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
101 | });
102 |
103 | //Create the placeholder
104 | this._createPlaceholder();
105 |
106 | // Only after we got the offset, we can change the helper's position to absolute
107 | this.helper.css("position", "absolute");
108 | this.cssPosition = this.helper.css("position");
109 |
110 | //Generate the original position
111 | this.originalPosition = this._generatePosition(event);
112 | this.originalPageX = event.pageX;
113 | this.originalPageY = event.pageY;
114 |
115 | //Adjust the mouse offset relative to the helper if "cursorAt" is supplied
116 | (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
117 |
118 | //Cache the former DOM position
119 | this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] };
120 |
121 | //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way
122 | if(this.helper[0] !== this.currentItem[0]) {
123 | this.currentItem.hide();
124 | }
125 |
126 | //Set a containment if given in the options
127 | if(o.containment) {
128 | this._setContainment();
129 | }
130 |
131 | if( o.cursor && o.cursor !== "auto" ) { // cursor option
132 | body = this.document.find( "body" );
133 |
134 | // support: IE
135 | this.storedCursor = body.css( "cursor" );
136 | body.css( "cursor", o.cursor );
137 |
138 | this.storedStylesheet = $( "" ).appendTo( body );
139 | }
140 |
141 | if(o.opacity) { // opacity option
142 | if (this.helper.css("opacity")) {
143 | this._storedOpacity = this.helper.css("opacity");
144 | }
145 | this.helper.css("opacity", o.opacity);
146 | }
147 |
148 | if(o.zIndex) { // zIndex option
149 | if (this.helper.css("zIndex")) {
150 | this._storedZIndex = this.helper.css("zIndex");
151 | }
152 | this.helper.css("zIndex", o.zIndex);
153 | }
154 |
155 | //Prepare scrolling
156 | if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") {
157 | this.overflowOffset = this.scrollParent.offset();
158 | }
159 |
160 | //Call callbacks
161 | this._trigger("start", event, this._uiHash());
162 |
163 | //Recache the helper size
164 | if(!this._preserveHelperProportions) {
165 | this._cacheHelperProportions();
166 | }
167 |
168 |
169 | //Post "activate" events to possible containers
170 | if( !noActivation ) {
171 | for ( i = this.containers.length - 1; i >= 0; i-- ) {
172 | this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) );
173 | }
174 | }
175 |
176 | //Prepare possible droppables
177 | if($.ui.ddmanager) {
178 | $.ui.ddmanager.current = this;
179 | }
180 |
181 | if ($.ui.ddmanager && !o.dropBehaviour) {
182 | $.ui.ddmanager.prepareOffsets(this, event);
183 | }
184 |
185 | this.dragging = true;
186 |
187 | this.helper.addClass("ui-sortable-helper");
188 | this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position
189 | return true;
190 | }
191 | });
192 | }
193 |
194 | $.fn.sort = function() {
195 | return this.each(function() {
196 | if ($(this).data('sort')) {
197 | return $(this);
198 | } else {
199 | var sort = new Sort(this);
200 | $(this).data('sort', sort);
201 | return $(this);
202 | }
203 | });
204 | }
205 |
206 | })(jQuery);
207 |
--------------------------------------------------------------------------------
/fields/sortable/sortable.php:
--------------------------------------------------------------------------------
1 | load();
7 |
8 | class SortableField extends InputField {
9 |
10 | public $limit = false;
11 | public $parent = null;
12 | public $prefix = null;
13 | public $layout = 'base';
14 | public $variant = null;
15 | public $options = array();
16 | public $sortable = true;
17 |
18 | // Caches
19 | protected $entries;
20 | protected $defaults;
21 | protected $origin;
22 |
23 | static public $assets = array(
24 | 'js' => array(
25 | 'sortable.js',
26 | ),
27 | 'css' => array(
28 | 'sortable.css',
29 | ),
30 | );
31 |
32 | public function routes() {
33 | return array(
34 | array(
35 | 'pattern' => 'action/(:any)/(:all?)',
36 | 'method' => 'POST|GET',
37 | 'action' => 'forAction',
38 | 'filter' => 'auth',
39 | ),
40 | array(
41 | 'pattern' => '(:all)/(:all)/sort',
42 | 'method' => 'POST|GET',
43 | 'action' => 'sort',
44 | 'filter' => 'auth',
45 | ),
46 | );
47 | }
48 |
49 | public function parent() {
50 | return $this->i18n($this->parent);
51 | }
52 |
53 | public function action($type, $data = array()) {
54 |
55 | $data = a::update($data, array(
56 | 'field' => $this,
57 | 'parent' => $this->origin(),
58 | ));
59 |
60 | return sortable::action($type, $data);
61 |
62 | }
63 |
64 | public function layout($type, $data = array()) {
65 |
66 | $data = a::update($data, array(
67 | 'field' => $this,
68 | 'parent' => $this->origin(),
69 | ));
70 |
71 | return sortable::layout($type, $data);
72 |
73 | }
74 |
75 | public function sortable() {
76 |
77 | if ($this->sortable === false) return false;
78 |
79 | foreach($this->entries() as $page) {
80 | if ($page->event('sort')->isDenied() || $page->event('visibility')->isDenied()) return false;
81 | }
82 |
83 | return true;
84 | }
85 |
86 | public function layouts() {
87 |
88 | $layouts = new Brick('div');
89 | $layouts->addClass('sortable__container');
90 | $layouts->attr('data-sortable', $this->sortable() ? 'true' : 'false');
91 |
92 | $numVisible = 0;
93 | $num = 0;
94 |
95 | foreach($this->entries() as $page) {
96 |
97 | if($page->isVisible()) $numVisible++;
98 | $num++;
99 |
100 | $data = a::update($this->options($page)->toArray(), array(
101 | 'numVisible' => $numVisible,
102 | 'page' => $page,
103 | 'num' => $num,
104 | ));
105 |
106 | $layout = $this->layout($this->layout, $data);
107 | $layouts->append($layout);
108 |
109 | }
110 |
111 | return $layouts;
112 |
113 | }
114 |
115 | /**
116 | * Get translation
117 | *
118 | * @param string $key
119 | * @param string $variant
120 | * @return string
121 | */
122 | public function l($key, $variant = null) {
123 |
124 | if(is_null($variant)) {
125 | $variant = $this->variant();
126 | }
127 |
128 | return sortable()->translation($key, $variant);
129 |
130 | }
131 |
132 | public function input() {
133 |
134 | $value = func_get_arg(0);
135 | $input = parent::input();
136 | $input->attr(array(
137 | 'id' => $value,
138 | 'name' => $this->name() . '[]',
139 | 'type' => 'hidden',
140 | 'value' => $value,
141 | 'required' => false,
142 | 'autocomplete' => false,
143 | ));
144 |
145 | return $input;
146 |
147 | }
148 |
149 | public function defaults() {
150 |
151 | // Return from cache if possible
152 | if(!is_null($this->defaults)) {
153 | return $this->defaults;
154 | }
155 |
156 | if(!$this->options) {
157 | return $this->defaults = array();
158 | }
159 |
160 | // Available templates
161 | $templates = $this->origin()->blueprint()->pages()->template()->pluck('name');
162 |
163 | // Remove template specific options from the defaults
164 | $defaults = array();
165 |
166 | foreach($this->options as $key => $value) {
167 | if(in_array($key, $templates)) continue;
168 | $defaults[$key] = $value;
169 | }
170 |
171 | return $this->defaults = $defaults;
172 |
173 | }
174 |
175 | public function options($template) {
176 |
177 | if(is_a($template, 'Page')) {
178 | $template = $template->intendedTemplate();
179 | }
180 |
181 | // Get entry specific options
182 | $options = a::get($this->options, $template, array());
183 |
184 | return new Obj(a::update($this->defaults(), $options));
185 |
186 | }
187 |
188 | public function content() {
189 |
190 | $template = $this->root() . DS . 'template.php';
191 |
192 | if(!is_file($template)) {
193 | $template = __DIR__ . DS . 'template.php';
194 | }
195 |
196 | $content = new Brick('div');
197 |
198 | // Sort namespace is used because otherwise there
199 | // would be a collision with the jquery sortable plugin
200 | $content->attr('data-field', 'sort');
201 | $content->attr('data-field-extended', $this->type());
202 | $content->attr('data-api', $this->url());
203 | $content->addClass('sortable');
204 | $content->append(tpl::load($template, array('field' => $this)));
205 |
206 | return $content;
207 |
208 | }
209 |
210 | public function origin() {
211 |
212 | // Return from cache if possible
213 | if($this->origin) {
214 | return $this->origin;
215 | }
216 |
217 | $origin = $this->page();
218 |
219 | if($this->parent()) {
220 | $origin = $origin->find($this->parent());
221 | }
222 |
223 | if(!is_a($origin, 'Page')) {
224 | throw new Exception('The parent page could not be found');
225 | }
226 |
227 | return $this->origin = $origin;
228 |
229 | }
230 |
231 | public function entries() {
232 |
233 | // Return from cache if possible
234 | if(!is_null($this->entries)) {
235 | return $this->entries;
236 | }
237 |
238 | $entries = $this->origin()->children();
239 |
240 | // Filter the entries
241 | if($entries->count() && $this->prefix()) {
242 | $entries = $entries->filter(function($page) {
243 | return str::startsWith($page->intendedTemplate(), $this->prefix());
244 | });
245 | }
246 |
247 | // Sort entries
248 | if($entries->count() && $this->value()) {
249 | $i = 0;
250 |
251 | $order = array_flip($this->value()) + array_flip($entries->pluck('uid'));
252 | $order = array_map(function($value) use(&$i) {
253 | return $i++;
254 | }, $order);
255 |
256 | $entries = $entries->find(array_flip($order));
257 | }
258 |
259 | // Always return a collection
260 | if(is_a($entries, 'Page')) {
261 | $page = $entries;
262 | $entries = new Children($this->origin());
263 |
264 | $entries->data[$page->id()] = $page;
265 | }
266 |
267 | return $this->entries = $entries;
268 |
269 | }
270 |
271 | public function counter() {
272 |
273 | if($this->limit()) {
274 |
275 | $counter = new Brick('span');
276 | $counter->addClass('sortable__counter');
277 | $counter->append('( ' . $this->entries()->visible()->count() . ' / ' . $this->limit() . ' )');
278 |
279 | return $counter;
280 | }
281 |
282 | }
283 |
284 | public function label() {
285 | return $this->i18n($this->label);
286 | }
287 |
288 | public function validate() {
289 | return true;
290 | }
291 |
292 | public function value() {
293 |
294 | $value = parent::value();
295 | if(is_array($value)) {
296 | return $value;
297 | } else {
298 | return str::split($value, ',');
299 | }
300 |
301 | }
302 |
303 | public function result() {
304 |
305 | $result = parent::result();
306 | return is_array($result) ? implode(', ', $result) : '';
307 |
308 | }
309 |
310 | public function url($action = null) {
311 |
312 | $url = purl($this->model(), 'field/' . $this->name() . '/' . $this->type());
313 |
314 | if(is_null($action)) {
315 | return $url;
316 | }
317 |
318 | return $url . '/action/' . $action;
319 |
320 | }
321 |
322 | public function template() {
323 | return $this->element()
324 | ->append($this->content())
325 | ->append($this->help());
326 | }
327 |
328 | }
329 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Kirby Sortable
2 | A toolkit for managing subpages in the content area.
3 |
4 | 
5 |
6 |
7 | ## Table of contents
8 | 1. [Features](#1-features)
9 | 2. [Installation](#2-installation)
10 | 3. [Blueprint](#3-blueprint)
11 | 4. [Permissions](#4-permissions)
12 | 5. [Customize](#4-customize)
13 | 6. [Known Bugs](#5-known-bugs)
14 | 7. [Donate](#6-donate)
15 |
16 |
17 | ## 1 Features
18 | This project started as a simple field and has grown into a reliable and extendable plugin.
19 | It includes the [`sortable`](#sortable), [`modules`](#modules), [`redirect`](#redirect) and [`options`](#options) field.
20 | In addition to the four fields the plugin has its own [registry](#registry).
21 |
22 | ### Fields
23 | #### `sortable`
24 | The core field. It is the base for the `modules` field.
25 | Change appereance in the [blueprint](#3-blueprint) or [build your own field](#4-customize) based on this one.
26 |
27 | #### `modules`
28 | The `modules` field is an extended `sortable` field. Bundled with the [modules-plugin](https://github.com/getkirby-plugins/modules-plugin) it is a very powerful tool. You can find further informations [here](fields/modules).
29 | To disable the field add `c::get('sortable.field.modules', false);` to your `config.php`.
30 |
31 | #### `redirect`
32 | Redirect a user to the parent of the currently visited panel page. Useful for pages that act as a container. You can find further informations [here](fields/redirect).
33 | To disable the field add `c::get('sortable.field.redirect', false);` to your `config.php`.
34 |
35 | #### `options`
36 | This field is used internally by the `sortable` field for the copy and paste functionality.
37 |
38 | ### Registry
39 | With the registry you are able to customize the visual appearance and modify or add custom functionality.
40 | The registry makes it possible to register [layouts](#layout-1), [actions](#action), [variants](#variant-1) and [translations](#translation). Learn more about how to register components in the [customize](#4-customize) section.
41 |
42 |
43 | ## 2 Installation
44 | There are several ways to install the plugin.
45 | Please make sure you meet the minimum requirements.
46 |
47 | ### Requirements
48 | - PHP 5.4+
49 | - [Kirby](https://getkirby.com/) 2.3+
50 | - [Kirby Modules Plugin](https://github.com/getkirby-plugins/modules-plugin) 1.3+
51 | when you want to use the `modules` field
52 |
53 | ### Git
54 | To clone or add the plugin as a submodule you need to cd to the root directory of your Kirby installation and run one of the corresponding command:
55 | `$ git clone https://github.com/lukaskleinschmidt/kirby-sortable.git site/plugins/sortable`
56 | `$ git submodule add https://github.com/lukaskleinschmidt/kirby-sortable.git site/plugins/sortable`
57 |
58 | ### Kirby CLI
59 | If you're using the Kirby CLI, you need to cd to the root directory of your Kirby installation and run the following command: `kirby plugin:install lukaskleinschmidt/kirby-sortable`
60 |
61 | ### Download
62 | You can download the latest version of the plugin [here](https://github.com/lukaskleinschmidt/kirby-sortable/releases/latest).
63 | To install the plugin, please put it in the `site/plugins` directory.
64 | The plugin folder must be named `sortable`.
65 |
66 | ```
67 | site/plugins/
68 | sortable/
69 | sortable.php
70 | ...
71 | ```
72 |
73 |
74 | ## 3 Blueprint
75 | After installing the plugin you can use the new field types.
76 | This blueprint shows all available options of the `sortable` field.
77 |
78 | ```yml
79 | fields:
80 | title:
81 | label: Title
82 | type: text
83 |
84 | sortable:
85 | label: Sortable
86 | type: sortable
87 |
88 | sortable: true
89 |
90 | layout: base
91 | variant: null
92 |
93 | limit: false
94 |
95 | parent: null
96 | prefix: null
97 |
98 | options:
99 | limit: false
100 | ```
101 |
102 | ### Options
103 |
104 | #### `sortable`
105 | Disable sorting when necessary.
106 |
107 | #### `layout`
108 | Load a registerd layout. The layout defines how a entry is rendered. Learn how to [register your own layout](#layout-1).
109 |
110 | #### `variant`
111 | Load a registerd variant. A variant is used to change the naming of the field from page to modules for example. Learn how to [register your own variant](#variant-1).
112 |
113 | #### `limit`
114 | Limit he number of visible pages. Example blueprint from the `modules` field.
115 | ```yml
116 | fields:
117 | modules:
118 | label: Modules
119 | type: modules
120 |
121 | # Allow 5 visible modules overall
122 | limit: 5
123 |
124 | # Template specific option
125 | options:
126 |
127 | # Allow only 3 modules per template (applies to all templates)
128 | limit: 3
129 | module.gallery:
130 |
131 | # Allow only 1 visible gallery module (overwrites the current limit of 3)
132 | limit: 1
133 | ```
134 |
135 | #### `parent`
136 | Uid to use when looking for the container page. If left empty the field will look for subpages in the current page.
137 | ```yml
138 | # home.yml
139 |
140 | fields:
141 | events:
142 | label: Events
143 | type: sortable
144 |
145 | parent: events
146 | ```
147 | ```
148 | site/content/
149 | home/
150 | home.txt
151 | events/
152 | event-1/
153 | event.txt
154 | event-2/
155 | event.txt
156 | ...
157 | ```
158 |
159 | #### `prefix`
160 | Template prefix to filter available subpages.
161 | ```yml
162 | # home.yml
163 |
164 | fields:
165 | events:
166 | label: Events
167 | type: sortable
168 |
169 | prefix: event.
170 | ```
171 | ```
172 | site/content/
173 | home/
174 | home.txt
175 | event-1/
176 | event.default.txt
177 | event-2/
178 | event.default.txt
179 | subpage/
180 | default.txt
181 | ...
182 | ```
183 |
184 | ## 4 Permissions
185 | Since `v2.4.0` you can now disable sorting independently from the `panel.page.visibility` permission. The new `panel.page.sort` permission will disable sorting as soon as one module denies sorting.
186 |
187 | Keep in mind that the `panel.page.visibility` permission will additionally to disabling the visibility toggle still disable sorting also.
188 |
189 | ## 5 Customize
190 | With the registry you are able to customize the visual appearance and modify or add functionality.
191 | The registry makes it possible to register layouts, actions, variants and translations.
192 |
193 | ```php
194 | // site/plugins/sortable-variants/sortable-variants.php
195 |
196 | // Make sure that the sortable plugin is loaded
197 | $kirby->plugin('sortable');
198 |
199 | if(!function_exists('sortable')) return;
200 |
201 | $kirby->set('field', 'variants', __DIR__ . DS . 'field');
202 |
203 | $sortable = sortable();
204 | $sortable->set('layout', 'variant', __DIR__ . DS . 'layout');
205 | $sortable->set('variant', 'variants', __DIR__ . DS . 'variant');
206 | $sortable->set('action', '_add', __DIR__ . DS . 'actions' . DS . '_add');
207 | $sortable->set('action', '_paste', __DIR__ . DS . 'actions' . DS . '_paste');
208 | $sortable->set('action', '_duplicate', __DIR__ . DS . 'actions' . DS . '_duplicate');
209 | ```
210 |
211 | A plugin can take care of registering all kinds of extensions, which will then be available in the `sortable` field or any field based on that.
212 |
213 | ### List of registry extensions
214 | These are all possible registry extensions you can register this way:
215 |
216 | #### layout
217 | ```php
218 | // The layout directory must exist and it must have a PHP file with the same name in it
219 | sortable()->set('layout', 'mylayout', __DIR__ . DS . 'mylayout');
220 | ```
221 | Have a look at the [base layout](sortable/layouts/base) or the [module layout](sortable/layouts/module).
222 |
223 | #### action
224 | ```php
225 | // The action directory must exist and it must have a PHP file with the same name in it
226 | sortable()->set('action', 'myaction', __DIR__ . DS . 'myaction');
227 | ```
228 | Have a look at the [actions](sortable/actions).
229 |
230 | #### variant
231 | ```php
232 | // The variant directory must exist and can have multiple tranlation files
233 | sortable()->set('variant', 'myvariant', __DIR__ . DS . 'myvariant');
234 | ```
235 | Have a look at the [modules variant](sortable/variants/modules) or the [sections variant](sortable/variants/sections).
236 |
237 | #### translation
238 | ```php
239 | // The translation file must exist at the given location
240 | sortable()->set('translation', 'en', __DIR__ . DS . 'en.php');
241 | sortable()->set('translation', 'sv_SE', __DIR__ . DS . 'sv_SE.php');
242 | ```
243 | Have a look at the [translations](sortable/translations).
244 |
245 | ### Examples
246 | - [kirby-sortable-variants](https://github.com/lukaskleinschmidt/kirby-sortable-variants)
247 | - [kirby-sortable-events](https://github.com/lukaskleinschmidt/kirby-sortable-events)
248 |
249 |
250 | ## 6 Known Bugs
251 |
252 | Long title can cause the entries to overflow the content area.
253 |
254 | - https://github.com/lukaskleinschmidt/kirby-sortable/issues/37
255 | - https://github.com/getkirby/panel/pull/986
256 |
257 | Put the following code in your custom [panel css](https://getkirby.com/docs/developer-guide/panel/css).
258 |
259 | ```css
260 | .form-blueprint-checklist > fieldset {
261 | min-width: 0;
262 | }
263 |
264 | @-moz-document url-prefix() {
265 | .form-blueprint-checklist > fieldset {
266 | display: table-cell;
267 | }
268 | }
269 | ```
270 |
271 | ---
272 |
273 | Readonly has no effect.
274 |
275 | - https://github.com/lukaskleinschmidt/kirby-sortable/issues/35
276 |
277 | One simple and fast way is to disable functionality with some custom [panel css](https://getkirby.com/docs/developer-guide/panel/css).
278 |
279 | ```css
280 | /* disable global actions */
281 | .field-is-readonly .sortable__action,
282 | .field-is-readonly .sortable__action .icon {
283 | pointer-events: none;
284 | color: #c9c9c9;
285 | }
286 |
287 | /* disable sorting */
288 | .field-is-readonly .sortable [data-handle] {
289 | pointer-events: none;
290 | color: #c9c9c9;
291 | }
292 |
293 | /*
294 | * enable entry actions
295 | * only necessary when you want to disable
296 | * sorting but still want the actions to work
297 | */
298 | .field-is-readonly .sortable-layout__action {
299 | pointer-events: auto;
300 | }
301 |
302 | /* disable entry actions */
303 | .field-is-readonly .sortable-layout__action {
304 | pointer-events: none;
305 | color: #c9c9c9;
306 | }
307 | ```
308 |
309 | ## 7 Donate
310 |
311 | If you enjoy this plugin and want to support me you can [buy me a beer](https://www.paypal.me/lukaskleinschmidt/5eur) :)
312 |
--------------------------------------------------------------------------------