├── .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 | 4 | -------------------------------------------------------------------------------- /sortable/actions/add/views/add.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /sortable/helpers.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sortable/actions/paste/views/paste.php: -------------------------------------------------------------------------------- 1 | 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 | layouts(); ?> 9 | 10 |
11 | l('field.sortable.empty'); ?> 12 | action('add', ['label' => $field->l('field.sortable.add.first'), 'icon' => '', 'class' => '']); ?> 13 | l('field.sortable.or'); ?> 14 | action('paste', ['label' => $field->l('field.sortable.paste.first'), 'icon' => '', 'class' => '']); ?> 15 |
16 | 17 | 18 |
19 | action('copy'); ?> 20 | action('paste'); ?> 21 | 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 | 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 | ![Preview](http://github.kleinschmidt.at/kirby-sortable/modules/preview.gif) 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 | ![Limit](http://github.kleinschmidt.at/kirby-sortable/modules/limit.png) 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 | ![Default actions](http://github.kleinschmidt.at/kirby-sortable/modules/actions.png) 136 | 137 | ```yml 138 | actions: 139 | - edit 140 | - toggle 141 | ``` 142 | 143 | ![Custom actions](http://github.kleinschmidt.at/kirby-sortable/modules/actions-custom.png) 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 | ![Disabled actions](http://github.kleinschmidt.at/kirby-sortable/modules/actions-disabled.png) 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 | ![Preview](http://github.kleinschmidt.at/kirby-sortable/modules/preview.gif) 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 | --------------------------------------------------------------------------------