Initial commit

This commit is contained in:
isUnknown 2026-02-12 15:22:46 +01:00
commit 65e0da7e11
1397 changed files with 596542 additions and 0 deletions

View file

@ -0,0 +1,12 @@
.modules-layout__preview {
border-bottom: 1px solid #ddd;
}
.modules-layout__preview:empty {
display: none;
}
.modules-layout__preview--bottom {
border-bottom: none;
border-top: 1px solid #ddd;
}

View file

@ -0,0 +1,3 @@
<?php
class ModulesFieldController extends LukasKleinschmidt\Sortable\Controllers\Field {}

View file

@ -0,0 +1,31 @@
<?php
class ModulesField extends SortableField {
public $add = true;
public $copy = true;
public $paste = true;
public $layout = 'module';
public $variant = 'modules';
public $actions = array(
'edit',
'duplicate',
'delete',
'toggle',
);
static public $assets = array(
'css' => array(
'modules.css',
),
);
public function parent() {
return Kirby\Modules\Settings::parentUid();
}
public function prefix() {
return Kirby\Modules\Settings::templatePrefix();
}
}

View file

@ -0,0 +1,164 @@
# Kirby Modules Field
This field extends the `sortable` field and adds some presets and features to help you manage your modules.
To use this field you have to install the [modules-plugin](https://github.com/getkirby-plugins/modules-plugin).
### Some of the features
- Preset `parent` and `prefix` to match those set by the modules-plugin
- Adds a [preview](#preview) if provided
![Preview](http://github.kleinschmidt.at/kirby-sortable/modules/preview.gif)
## Blueprint
After installing the plugin, you can use the new field type `modules`.
This blueprint shows all available options and their defaults.
```yml
fields:
title:
label: Title
type: text
modules:
label: Modules
type: modules
add: true
copy: true
paste: true
limit: false
variant: modules
actions:
- edit
- duplicate
- delete
- toggle
options:
preview: true
limit: false
edit: true
duplicate: true
delete: true
toggle: true
...
```
## Examples
The following examples show and explain some of the possible settings.
### Preview
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:
- `$page` is the page on which the module appears
- `$module` is the module subpage, which you can use to access the fields from your module blueprint as well as module files
- `$moduleName` is the name of the module such as text or gallery
The preview file must be in the same folder as the module itself.
The module directory looks something like this:
```
site/modules/
gallery/
gallery.html.php
gallery.yml
# The preview file
gallery.preview.php
...
```
### Preview options
Previews are enabled by default. Set `preview` to `false` to disable the preview.
It is also possible to change the position in the module.
```yml
options:
# Render the preview at the bottom
preview: bottom
# Or at the top
preview: top
preview: true
```
### Limit the number of visible modules
```yml
modules_field:
label: Modules
type: modules
# Allow 3 visible modules overall
limit: 3
# Template specific option
options:
module.gallery:
# Allow only 1 visible gallery module
limit: 1
```
![Limit](http://github.kleinschmidt.at/kirby-sortable/modules/limit.png)
### Is it a module or a section?
Change the naming.
```yml
# Modules is fine
variant: modules
# Nah sections it is
variant: sections
```
### Changing actions
To change the actions or remove an action completely from the modules, you must specify the `actions` array in the blueprint.
```yml
# Default
actions:
- edit
- duplicate
- delete
- toggle
```
![Default actions](http://github.kleinschmidt.at/kirby-sortable/modules/actions.png)
```yml
actions:
- edit
- toggle
```
![Custom actions](http://github.kleinschmidt.at/kirby-sortable/modules/actions-custom.png)
### Disabling an action
```yml
options:
edit: false
duplicate: false
delete: true
toggle: true
# Template specific options
module.gallery:
edit: true
duplicate: true
```
![Disabled actions](http://github.kleinschmidt.at/kirby-sortable/modules/actions-disabled.png)
## Requirements
- [Kirby Modules Plugin](https://github.com/getkirby-plugins/modules-plugin) 1.3+

View file

@ -0,0 +1,32 @@
<label class="label">
<?= $field->label(); ?>
<?= $field->counter(); ?>
<?php if($field->add()) echo $field->action('add'); ?>
</label>
<?php if($field->entries()->count()): ?>
<?= $field->layouts(); ?>
<?php else: ?>
<div class="sortable__empty">
<?php
echo $field->l('field.sortable.empty');
if($field->add()) {
echo $field->action('add', ['label' => $field->l('field.sortable.add.first'), 'icon' => '', 'class' => '']);
if($field->paste()) {
echo $field->l('field.sortable.or');
echo $field->action('paste', ['label' => $field->l('field.sortable.paste.first'), 'icon' => '', 'class' => '']);
}
}
?>
</div>
<?php endif; ?>
<div class="sortable__navigation">
<?php
if($field->copy()) echo $field->action('copy');
if($field->add()) {
if($field->paste()) echo $field->action('paste');
echo $field->action('add');
}
?>
</div>

View file

@ -0,0 +1,44 @@
<?php
class OptionsField extends CheckboxesField {
public function input() {
$value = func_get_arg(0);
$data = func_get_arg(1);
$input = parent::input($value);
if(!$this->error) {
$input->attr('checked', v::accepted($this->value()) || v::accepted($data->checked()));
}
if($data->readonly()) {
$input->attr('disabled', true);
$input->attr('readonly', true);
$input->addClass('input-is-readonly');
}
return $input;
}
public function item($value, $data) {
$data = new Obj($data);
$input = $this->input($value, $data);
$label = new Brick('label', '<span>' . $this->i18n($data->label()) . '</span>');
$label->addClass('input input-with-checkbox');
$label->attr('data-focus', 'true');
$label->prepend($input);
if($data->readonly()) {
$label->addClass('input-is-readonly');
}
return $label;
}
}

View file

@ -0,0 +1,39 @@
# Kirby Redirect Field
This field redirects a user to the parent of the currently visited panel page.
Useful for pages that act like a container.
## Blueprint
```yml
fields:
title:
label: Title
type: text
redirect:
label: Redirect
type: redirect
...
```
## Redirect to a specific page
Redirect to `panel/projects/project-a/edit`.
When the page is not found you will get redirected to the dashboard.
```yml
redirect:
label: Redirect
type: redirect
redirect: projects/project-a
```
## Hide pages
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).
```css
.breadcrumb-link[title="your-page-title"] {
display: none;
}
```

View file

@ -0,0 +1,25 @@
<?php
class RedirectField extends BaseField {
public $redirect;
public function template() {
$redirect = $this->redirect();
if(is_null($redirect)) {
return go(purl($this->page()->parent(), 'edit'));
}
$page = panel()->page($redirect);
if(is_null($page)) {
return go(purl());
}
return go(purl($page));
}
}

View file

@ -0,0 +1,173 @@
/* TEMP: until https://github.com/getkirby/panel/pull/986 is resolved */
.form-blueprint-home > fieldset {
min-width: 0;
}
@-moz-document url-prefix() {
.form-blueprint-home > fieldset {
display: table-cell;
}
}
.sortable__counter {
padding-left: 0.25em;
font-size: 0.8em;
font-weight: 400;
color: #bbb;
line-height: 1;
}
.sortable__empty {
background: #ddd;
padding: 1.5em;
}
.sortable__empty a {
border-bottom: 2px solid #bbb;
margin: 0 0.25em;
}
.sortable__empty a:hover {
border-color: #000;
}
.sortable__empty a:first-child {
margin-left: 0.5em;
}
.sortable__navigation {
padding-top: 0.5em;
position: relative;
}
.sortable__action {
color: #777;
font-weight: 400;
line-height: 1.5em;
}
.sortable__action .icon,
.sortable__action:hover {
color: #000;
}
.sortable__action--add {
position: absolute;
right: 0;
}
.sortable__action:nth-child(n+2) {
margin-left: 1em;
}
.sortable__action.is-disabled {
pointer-events: none;
color: #c9c9c9;
}
.sortable__layout {
margin-bottom: 0.25em;
}
.sortable-layout a,
.sortable-layout button,
.sortable__action.is-disabled .icon {
color: inherit;
}
.sortable-layout {
background: #fff;
border: 2px solid #ddd;
position: relative;
}
.sortable-layout .icon {
vertical-align: -5%;
}
.sortable-layout .icon:not(.icon-left) {
width: 1.28571em;
}
.sortable-layout__icon {
display: inline;
}
.sortable-layout__counter {
padding-left: 0.25em;
font-size: 0.8em;
color: #bbb;
line-height: 1;
}
.sortable-layout [data-handle] {
cursor: move;
}
.sortable-layout__navigation {
min-height: 44px;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-flow: row nowrap;
-ms-flex-flow: row nowrap;
flex-flow: row nowrap;
}
.sortable-layout__icon,
.sortable-layout__action,
.sortable-layout__title {
padding: 0.6em;
white-space: nowrap;
line-height: 1.5em;
font-size: 1em;
}
.sortable-layout__title {
-webkit-flex: 1 0 0%;
-ms-flex: 1 0 0%;
flex: 1 0 0%;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 0.35em;
}
.sortable-layout[data-visible=false] .sortable-layout__icon,
.sortable-layout[data-visible=false] .sortable-layout__title {
color: #c9c9c9;
}
.sortable-layout__action {
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
border: none;
//border-left: 1px solid #efefef;
background: 0 0;
cursor: pointer;
outline: 0;
}
.sortable-layout__action.is-disabled {
pointer-events: none;
color: #c9c9c9;
}
.sortable-placeholder {
width: 25%;
height: 25%;
display: inline-block;
}
.sortable__grid {
width: 33%; display: inline-block; vertical-align: middle;
}
.sortable__grid .sortable-layout__icon {
width: 100%;
}
.sortable__action--copy, .sortable__action--paste {
display: none;
}

View file

@ -0,0 +1,207 @@
(function($) {
var Sort = function(element) {
var self = this;
this.element = $(element);
this.container = $('.sortable__container', element);
this.api = this.element.data('api');
this.container._sortable({
handle: '[data-handle]',
placeholder: "sortable-placeholder",
start: function(event, ui) {
self.container._sortable('refreshPositions');
self.blur();
},
update: function(event, ui) {
var to = self.container.children().index(ui.item) + 1;
var uid = ui.item.data('uid');
var action = [self.api, uid, to, 'sort'].join('/');
// disable sorting because a reload is expected
self.disable();
$.post(action, self.reload.bind(self));
}
});
this.element.on('click', '[data-action]', function(event) {
var element = $(this);
var action = element.data('action') || element.attr('href');
$.post(action, self.reload.bind(self));
return false;
});
// setup extended field script
var key = this.element.data('field-extended');
if(key != 'sortable' && this.element[key]) this.element[key]();
};
Sort.prototype.blur = function() {
$('.form input:focus, .form select:focus, .form textarea:focus').blur();
app.content.focus.forget();
}
Sort.prototype.disable = function() {
this.container._sortable('disable');
}
Sort.prototype.reload = function() {
this.disable();
app.content.reload();
}
// Fixing scrollbar jumping issue
// http://stackoverflow.com/questions/1735372/jquery-sortable-list-scroll-bar-jumps-up-when-sorting
if (typeof($.ui._sortable) === 'undefined') {
$.widget('ui._sortable', $.ui.sortable, {
_mouseStart: function(event, overrideHandle, noActivation) {
var i, body,
o = this.options;
this.currentContainer = this;
//We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
this.refreshPositions();
//Create and append the visible helper
this.helper = this._createHelper(event);
//Cache the helper size
this._cacheHelperProportions();
/*
* - Position generation -
* This block generates everything position related - it's the core of draggables.
*/
//Cache the margins of the original element
this._cacheMargins();
//Get the next scrolling parent
this.scrollParent = this.helper.scrollParent();
//The element's absolute position on the page minus margins
this.offset = this.currentItem.offset();
this.offset = {
top: this.offset.top - this.margins.top,
left: this.offset.left - this.margins.left
};
$.extend(this.offset, {
click: { //Where the click happened, relative to the element
left: event.pageX - this.offset.left,
top: event.pageY - this.offset.top
},
parent: this._getParentOffset(),
relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
});
//Create the placeholder
this._createPlaceholder();
// Only after we got the offset, we can change the helper's position to absolute
this.helper.css("position", "absolute");
this.cssPosition = this.helper.css("position");
//Generate the original position
this.originalPosition = this._generatePosition(event);
this.originalPageX = event.pageX;
this.originalPageY = event.pageY;
//Adjust the mouse offset relative to the helper if "cursorAt" is supplied
(o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
//Cache the former DOM position
this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] };
//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
if(this.helper[0] !== this.currentItem[0]) {
this.currentItem.hide();
}
//Set a containment if given in the options
if(o.containment) {
this._setContainment();
}
if( o.cursor && o.cursor !== "auto" ) { // cursor option
body = this.document.find( "body" );
// support: IE
this.storedCursor = body.css( "cursor" );
body.css( "cursor", o.cursor );
this.storedStylesheet = $( "<style>*{ cursor: "+o.cursor+" !important; }</style>" ).appendTo( body );
}
if(o.opacity) { // opacity option
if (this.helper.css("opacity")) {
this._storedOpacity = this.helper.css("opacity");
}
this.helper.css("opacity", o.opacity);
}
if(o.zIndex) { // zIndex option
if (this.helper.css("zIndex")) {
this._storedZIndex = this.helper.css("zIndex");
}
this.helper.css("zIndex", o.zIndex);
}
//Prepare scrolling
if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") {
this.overflowOffset = this.scrollParent.offset();
}
//Call callbacks
this._trigger("start", event, this._uiHash());
//Recache the helper size
if(!this._preserveHelperProportions) {
this._cacheHelperProportions();
}
//Post "activate" events to possible containers
if( !noActivation ) {
for ( i = this.containers.length - 1; i >= 0; i-- ) {
this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) );
}
}
//Prepare possible droppables
if($.ui.ddmanager) {
$.ui.ddmanager.current = this;
}
if ($.ui.ddmanager && !o.dropBehaviour) {
$.ui.ddmanager.prepareOffsets(this, event);
}
this.dragging = true;
this.helper.addClass("ui-sortable-helper");
this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position
return true;
}
});
}
$.fn.sort = function() {
return this.each(function() {
if ($(this).data('sort')) {
return $(this);
} else {
var sort = new Sort(this);
$(this).data('sort', sort);
return $(this);
}
});
}
})(jQuery);

View file

@ -0,0 +1,3 @@
<?php
class SortableFieldController extends LukasKleinschmidt\Sortable\Controllers\Field {}

View file

@ -0,0 +1,315 @@
<?php
use LukasKleinschmidt\Sortable;
// Load all registerd classes
sortable()->load();
class SortableField extends InputField {
public $limit = false;
public $parent = null;
public $prefix = null;
public $layout = 'base';
public $variant = null;
public $options = array();
// Caches
protected $entries;
protected $defaults;
protected $origin;
static public $assets = array(
'js' => array(
'sortable.js',
),
'css' => array(
'sortable.css',
),
);
public function routes() {
return array(
array(
'pattern' => 'action/(:any)/(:all?)',
'method' => 'POST|GET',
'action' => 'forAction',
'filter' => 'auth',
),
array(
'pattern' => '(:all)/(:all)/sort',
'method' => 'POST|GET',
'action' => 'sort',
'filter' => 'auth',
),
);
}
public function parent() {
return $this->i18n($this->parent);
}
public function action($type, $data = array()) {
$data = a::update($data, array(
'field' => $this,
'parent' => $this->origin(),
));
return sortable::action($type, $data);
}
public function layout($type, $data = array()) {
$data = a::update($data, array(
'field' => $this,
'parent' => $this->origin(),
));
return sortable::layout($type, $data);
}
public function layouts() {
$layouts = new Brick('div');
$layouts->addClass('sortable__container');
$numVisible = 0;
$num = 0;
foreach($this->entries() as $page) {
if($page->isVisible()) $numVisible++;
$num++;
$data = a::update($this->options($page)->toArray(), array(
'numVisible' => $numVisible,
'page' => $page,
'num' => $num,
));
$layout = $this->layout($this->layout, $data);
$layouts->append($layout);
}
return $layouts;
}
/**
* Get translation
*
* @param string $key
* @param string $variant
* @return string
*/
public function l($key, $variant = null) {
if(is_null($variant)) {
$variant = $this->variant();
}
return sortable()->translation($key, $variant);
}
public function input() {
$value = func_get_arg(0);
$input = parent::input();
$input->attr(array(
'id' => $value,
'name' => $this->name() . '[]',
'type' => 'hidden',
'value' => $value,
'required' => false,
'autocomplete' => false,
));
return $input;
}
public function defaults() {
// Return from cache if possible
if(!is_null($this->defaults)) {
return $this->defaults;
}
if(!$this->options) {
return $this->defaults = array();
}
// Available templates
$templates = $this->origin()->blueprint()->pages()->template()->pluck('name');
// Remove template specific options from the defaults
$defaults = array();
foreach($this->options as $key => $value) {
if(in_array($key, $templates)) continue;
$defaults[$key] = $value;
}
return $this->defaults = $defaults;
}
public function options($template) {
if(is_a($template, 'Page')) {
$template = $template->intendedTemplate();
}
// Get entry specific options
$options = a::get($this->options, $template, array());
return new Obj(a::update($this->defaults(), $options));
}
public function content() {
$template = $this->root() . DS . 'template.php';
if(!is_file($template)) {
$template = __DIR__ . DS . 'template.php';
}
$content = new Brick('div');
// Sort namespace is used because otherwise there
// would be a collision with the jquery sortable plugin
$content->attr('data-field', 'sort');
$content->attr('data-field-extended', $this->type());
$content->attr('data-api', $this->url());
$content->addClass('sortable');
$content->append(tpl::load($template, array('field' => $this)));
return $content;
}
public function origin() {
// Return from cache if possible
if($this->origin) {
return $this->origin;
}
$origin = $this->page();
if($this->parent()) {
$origin = $origin->find($this->parent());
}
if(!is_a($origin, 'Page')) {
throw new Exception('The parent page could not be found');
}
return $this->origin = $origin;
}
public function entries() {
// Return from cache if possible
if(!is_null($this->entries)) {
return $this->entries;
}
$entries = $this->origin()->children();
// Filter the entries
if($entries->count() && $this->prefix()) {
$entries = $entries->filter(function($page) {
return str::startsWith($page->intendedTemplate(), $this->prefix());
});
}
// Sort entries
if($entries->count() && $this->value()) {
$i = 0;
$order = a::merge(array_flip($this->value()), array_flip($entries->pluck('uid')));
$order = array_map(function($value) use(&$i) {
return $i++;
}, $order);
$entries = $entries->find(array_flip($order));
}
// Always return a collection
if(is_a($entries, 'Page')) {
$page = $entries;
$entries = new Children($this->origin());
$entries->data[$page->id()] = $page;
}
return $this->entries = $entries;
}
public function counter() {
if($this->limit()) {
$counter = new Brick('span');
$counter->addClass('sortable__counter');
$counter->append('( ' . $this->entries()->published()->count() . ' / ' . $this->limit() . ' )');
return $counter;
}
}
public function label() {
return $this->i18n($this->label);
}
public function validate() {
return true;
}
public function value() {
$value = parent::value();
if(is_array($value)) {
return $value;
} else {
return str::split($value, ',');
}
}
public function result() {
$result = parent::result();
return is_array($result) ? implode(', ', $result) : '';
}
public function url($action = null) {
$url = purl($this->model(), 'field/' . $this->name() . '/' . $this->type());
if(is_null($action)) {
return $url;
}
return $url . '/action/' . $action;
}
public function template() {
return $this->element()
->append($this->content())
->append($this->help());
}
}

View file

@ -0,0 +1,22 @@
<label class="label">
<?= $field->label(); ?>
<?= $field->counter(); ?>
<!-- <?= $field->action('add'); ?> -->
</label>
<?php if($field->entries()->count()): ?>
<?= $field->layouts(); ?>
<?php else: ?>
<!-- <div class="sortable__empty">
<?= $field->l('field.sortable.empty'); ?>
<?= $field->action('add', ['label' => $field->l('field.sortable.add.first'), 'icon' => '', 'class' => '']); ?>
<?= $field->l('field.sortable.or'); ?>
<?= $field->action('paste', ['label' => $field->l('field.sortable.paste.first'), 'icon' => '', 'class' => '']); ?>
</div> -->
<?php endif; ?>
<!-- <div class="sortable__navigation">
<?= $field->action('copy'); ?>
<?= $field->action('paste'); ?>
<?= $field->action('add'); ?>
</div> -->

View file

@ -0,0 +1,7 @@
{
"name": "sortable",
"version": "2.3.2",
"description": "Kirby Sortable",
"license": "MIT",
"type": "kirby-plugin"
}

View file

@ -0,0 +1,241 @@
# Kirby Sortable
A toolkit for managing subpages in the content area.
![Preview](http://github.kleinschmidt.at/kirby-sortable/modules/preview.gif)
## Table of contents
1. [Features](#1-features)
3. [Installation](#2-installation)
4. [Blueprint](#3-blueprint)
5. [Customize](#4-customize)
6. [Donate](#5-donate)
## 1 Features
This project started as a simple field and has grown into a reliable and extendable plugin.
It includes the [`sortable`](#sortable), [`modules`](#modules), [`redirect`](#redirect) and [`options`](#options) field.
In addition to the four fields the plugin has its own [registry](#registry).
### Fields
#### `sortable`
The core field. It is the base for the `modules` field.
Change appereance in the [blueprint](#3-blueprint) or [build your own field](#4-customize) based on this one.
#### `modules`
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).
To disable the field add `c::get('sortable.field.modules', false);` to your `config.php`.
#### `redirect`
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).
To disable the field add `c::get('sortable.field.redirect', false);` to your `config.php`.
#### `options`
This field is used internally by the `sortable` field for the copy and paste functionality.
### Registry
With the registry you are able to customize the visual appearance and modify or add custom functionality.
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.
## 2 Installation
There are several ways to install the plugin.
Please make sure you meet the minimum requirements.
### Requirements
- PHP 5.4+
- [Kirby](https://getkirby.com/) 2.3+
- [Kirby Modules Plugin](https://github.com/getkirby-plugins/modules-plugin) 1.3+
when you want to use the `modules` field
### Git
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:
`$ git clone https://github.com/lukaskleinschmidt/kirby-sortable.git site/plugins/sortable`
`$ git submodule add https://github.com/lukaskleinschmidt/kirby-sortable.git site/plugins/sortable`
### Kirby CLI
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`
### Download
You can download the latest version of the plugin [here](https://github.com/lukaskleinschmidt/kirby-sortable/releases/latest).
To install the plugin, please put it in the `site/plugins` directory.
The plugin folder must be named `sortable`.
```
site/plugins/
sortable/
sortable.php
...
```
## 3 Blueprint
After installing the plugin you can use the new field types.
This blueprint shows all available options of the `sortable` field.
```yml
fields:
title:
label: Title
type: text
sortable:
label: Sortable
type: sortable
layout: base
variant: null
limit: false
parent: null
prefix: null
options:
limit: false
```
### Options
#### `layout`
Load a registerd layout. The layout defines how a entry is rendered. Learn how to [register your own layout](#layout-1).
#### `variant`
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).
#### `limit`
Limit he number of visible pages. Example blueprint from the `modules` field.
```yml
fields:
modules:
label: Modules
type: modules
# Allow 5 visible modules overall
limit: 5
# Template specific option
options:
# Allow only 3 modules per template (applies to all templates)
limit: 3
module.gallery:
# Allow only 1 visible gallery module (overwrites the current limit of 3)
limit: 1
```
#### `parent`
Uid to use when looking for the container page. If left empty the field will look for subpages in the current page.
```yml
# home.yml
fields:
events:
label: Events
type: sortable
parent: events
```
```
site/content/
home/
home.txt
events/
event-1/
event.txt
event-2/
event.txt
...
```
#### `prefix`
Template prefix to filter available subpages.
```yml
# home.yml
fields:
events:
label: Events
type: sortable
prefix: event.
```
```
site/content/
home/
home.txt
event-1/
event.default.txt
event-2/
event.default.txt
subpage/
default.txt
...
```
## 4 Customize
With the registry you are able to customize the visual appearance and modify or add functionality.
The registry makes it possible to register layouts, actions, variants and translations.
```php
// site/plugins/sortable-variants/sortable-variants.php
// Make sure that the sortable plugin is loaded
$kirby->plugin('sortable');
if(!function_exists('sortable')) return;
$kirby->set('field', 'variants', __DIR__ . DS . 'field');
$sortable = sortable();
$sortable->set('layout', 'variant', __DIR__ . DS . 'layout');
$sortable->set('variant', 'variants', __DIR__ . DS . 'variant');
$sortable->set('action', '_add', __DIR__ . DS . 'actions' . DS . '_add');
$sortable->set('action', '_paste', __DIR__ . DS . 'actions' . DS . '_paste');
$sortable->set('action', '_duplicate', __DIR__ . DS . 'actions' . DS . '_duplicate');
```
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.
### List of registry extensions
These are all possible registry extensions you can register this way:
#### layout
```php
// The layout directory must exist and it must have a PHP file with the same name in it
sortable()->set('layout', 'mylayout', __DIR__ . DS . 'mylayout');
```
Have a look at the [base layout](sortable/layouts/base) or the [module layout](sortable/layouts/module).
#### action
```php
// The action directory must exist and it must have a PHP file with the same name in it
sortable()->set('action', 'myaction', __DIR__ . DS . 'myaction');
```
Have a look at the [actions](sortable/actions).
#### variant
```php
// The variant directory must exist and can have multiple tranlation files
sortable()->set('variant', 'myvariant', __DIR__ . DS . 'myvariant');
```
Have a look at the [modules variant](sortable/variants/modules) or the [sections variant](sortable/variants/sections).
#### translation
```php
// The translation file must exist at the given location
sortable()->set('translation', 'en', __DIR__ . DS . 'en.php');
sortable()->set('translation', 'sv_SE', __DIR__ . DS . 'sv_SE.php');
```
Have a look at the [translations](sortable/translations).
### Examples
- [kirby-sortable-variants](https://github.com/lukaskleinschmidt/kirby-sortable-variants)
- [kirby-sortable-events](https://github.com/lukaskleinschmidt/kirby-sortable-events)
## 5 Donate
If you enjoy this plugin and want to support me you can [buy me a beer](https://www.paypal.me/lukaskleinschmidt/5eur) :)

View file

@ -0,0 +1,19 @@
<?php
if(!function_exists('panel')) return;
// load the sortable bootstrapper
require(__DIR__ . DS . 'sortable' . DS . 'bootstrap.php');
sortable()->register();
$kirby->set('field', 'sortable', __DIR__ . DS . 'fields' . DS . 'sortable');
$kirby->set('field', 'options' , __DIR__ . DS . 'fields' . DS . 'options');
if(c::get('sortable.field.redirect', true)) {
$kirby->set('field', 'redirect', __DIR__ . DS . 'fields' . DS . 'redirect');
}
if(c::get('sortable.field.modules', true)) {
$kirby->set('field', 'modules', __DIR__ . DS . 'fields' . DS . 'modules');
}

View file

@ -0,0 +1,33 @@
<?php
class AddAction extends BaseAction {
public $icon = 'plus-circle';
public $class = 'sortable__action sortable__action--add';
public $label = 'field.sortable.add';
public function routes() {
return array(
array(
'pattern' => '/',
'method' => 'POST|GET',
'action' => 'add',
'filter' => 'auth',
),
);
}
public function content() {
$content = parent::content();
$content->data('modal', true);
return $content;
}
public function disabled() {
return $this->disabled || $this->parent()->ui()->create() === false;
}
}

View file

@ -0,0 +1,49 @@
<?php
class AddActionController extends LukasKleinschmidt\Sortable\Controllers\Action {
/**
* Add a entry
*/
public function add() {
$self = $this;
$parent = $this->field()->origin();
if($parent->ui()->create() === false) {
throw new Kirby\Panel\Exceptions\PermissionsException();
}
$form = $this->form('add', array($parent, $this->model(), $this->field()), function($form) use($parent, $self) {
try {
$form->validate();
if(!$form->isValid()) {
throw new Exception($self->field()->l('field.sortable.add.error.template'));
}
$data = $form->serialize();
$template = $data['template'];
$page = $parent->children()->create($self->uid($template), $template, array(
'title' => i18n($parent->blueprint()->pages()->template()->findBy('name', $template)->title())
));
$self->update($self->field()->entries()->pluck('uid'));
$self->notify(':)');
$self->redirect($self->model());
// $this->redirect($page, 'edit');
} catch(Exception $e) {
$form->alert($e->getMessage());
}
});
return $this->modal('add', compact('form'));
}
}

View file

@ -0,0 +1,28 @@
<?php
return function($page, $model, $field) {
$options = [];
foreach($page->blueprint()->pages()->template() as $template) {
$options[$template->name()] = $template->title();
}
$form = new Kirby\Panel\Form(array(
'template' => array(
'label' => $field->l('field.sortable.add.template.label'),
'type' => 'select',
'options' => $options,
'default' => key($options),
'required' => true,
'readonly' => count($options) == 1 ? true : false,
'icon' => count($options) == 1 ? $page->blueprint()->pages()->template()->first()->icon() : 'chevron-down',
)
));
$form->cancel($model);
$form->buttons->submit->val($field->l('field.sortable.add'));
return $form;
};

View file

@ -0,0 +1,3 @@
<div class="modal-content modal-content-small">
<?php echo $form ?>
</div>

View file

@ -0,0 +1,92 @@
<?php
class BaseAction {
public $type = null;
public $icon = null;
public $label = null;
public $title = null;
public $class = 'sortable-layout__action';
public $disabled = false;
public function root() {
$obj = new ReflectionClass($this);
return dirname($obj->getFileName());
}
public function __construct($type) {
$this->type = $type;
}
public function __call($method, $arguments) {
return isset($this->$method) ? $this->$method : null;
}
public function type() {
return $this->type;
}
public function url() {
return $this->field()->url($this->type);
}
public function i18n($value) {
if(is_array($value)) {
return i18n($value);
}
return $this->field()->l($value);
}
public function l($key, $variant = null) {
return $this->field()->l($key, $variant);
}
public function icon($position = null) {
if(!$this->icon) return null;
return icon($this->icon, $position);
}
public function label() {
if(!$this->label) return null;
return $this->i18n($this->label);
}
public function title() {
if(!$this->title) return null;
return $this->i18n($this->title);
}
public function content() {
$a = new Brick('a');
$a->addClass($this->class());
$a->attr('title', $this->title());
$a->attr('href', $this->url());
if($this->disabled()) {
$a->addClass('is-disabled');
}
if($label = $this->label()) {
$a->append($this->icon('left') . $label);
} else {
$a->append($this->icon());
}
return $a;
}
public function __toString() {
try {
return (string)$this->content();
} catch(Exception $e) {
return (string)$e->getMessage();
}
}
}

View file

@ -0,0 +1,43 @@
<?php
class CopyActionController extends LukasKleinschmidt\Sortable\Controllers\Action {
/**
* Save to clipboard
*/
public function save() {
$self = $this;
$page = $this->field()->origin();
$entries = $this->field()->entries();
$form = $this->form('copy', array($page, $entries, $this->model(), $this->field()), function($form) use($page, $self) {
try {
$form->validate();
if(!$form->isValid()) {
throw new Exception($self->field()->l('field.sortable.copy.error.uri'));
}
$data = $form->serialize();
site()->user()->update(array(
'clipboard' => str::split($data['uri']),
));
$self->notify(':)');
$self->redirect($this->model());
} catch(Exception $e) {
$form->alert($e->getMessage());
}
});
return $this->modal('copy', compact('form'));
}
}

View file

@ -0,0 +1,29 @@
<?php
class CopyAction extends BaseAction {
public $icon = 'copy';
public $class = 'sortable__action sortable__action--copy';
public $label = 'field.sortable.copy';
public function routes() {
return array(
array(
'pattern' => '/',
'method' => 'POST|GET',
'action' => 'save',
'filter' => 'auth',
),
);
}
public function content() {
$content = parent::content();
$content->data('modal', true);
return $content;
}
}

View file

@ -0,0 +1,49 @@
<?php
return function($page, $entries, $model, $field) {
if($entries->count()) {
$options = [];
foreach($entries as $entry) {
$options[$entry->uri()] = array(
'label' => $entry->title(),
'checked' => true,
'readonly' => false,
);
}
$form = new Kirby\Panel\Form(array(
'uri' => array(
'label' => $field->l('field.sortable.copy.uri.label'),
'type' => 'options',
'columns' => 1,
'required' => true,
'options' => $options,
)
));
} else {
$form = new Kirby\Panel\Form(array(
'info' => array(
'label' => $field->l('field.sortable.copy.info.label'),
'type' => 'info',
'text' => $field->l('field.sortable.copy.info.text')
)
));
}
$form->cancel($model);
$form->buttons->submit->val($field->l('field.sortable.copy'));
if(!$entries->count()) {
$form->buttons->submit = $form->buttons->cancel;
$form->style('centered');
}
return $form;
};

View file

@ -0,0 +1,3 @@
<div class="modal-content modal-content-small">
<?php echo $form; ?>
</div>

View file

@ -0,0 +1,38 @@
<?php
class DeleteActionController extends LukasKleinschmidt\Sortable\Controllers\Action {
/**
* Delete a entry
*
* @param string $uid
*/
public function delete($uid) {
$self = $this;
$page = $this->field()->entries()->find($uid);
if($page->ui()->delete() === false) {
throw new Kirby\Panel\Exceptions\PermissionsException();
}
$form = $this->form('delete', array($page, $this->model(), $this->field()), function($form) use($page, $self) {
try {
$page->delete();
$self->update($self->field()->entries()->not($page)->pluck('uid'));
$self->notify(':)');
$self->redirect($self->model());
} catch(Exception $e) {
$form->alert($e->getMessage());
}
});
return $this->modal('delete', compact('form'));
}
}

View file

@ -0,0 +1,34 @@
<?php
class DeleteAction extends BaseAction {
public $icon = 'trash-o';
public $title = 'field.sortable.delete';
public function routes() {
return array(
array(
'pattern' => '(:any)',
'method' => 'POST|GET',
'action' => 'delete',
'filter' => 'auth',
),
);
}
public function content() {
$content = parent::content();
$content->attr('href', $this->url() . '/' . $this->page()->uid());
$content->data('modal', true);
return $content;
}
public function disabled() {
$page = $this->page();
return $this->disabled || $page->blueprint()->options()->delete() === false || $page->ui()->delete() === false;
}
}

View file

@ -0,0 +1,20 @@
<?php
return function($page, $model, $field) {
$form = new Kirby\Panel\Form(array(
'page' => array(
'label' => $field->l('field.sortable.delete.page.label'),
'type' => 'text',
'readonly' => true,
'default' => $page->title(),
'help' => $page->id(),
)
));
$form->cancel($model);
$form->style('delete');
return $form;
};

View file

@ -0,0 +1,3 @@
<div class="modal-content">
<?php echo $form ?>
</div>

View file

@ -0,0 +1,30 @@
<?php
class DuplicateActionController extends LukasKleinschmidt\Sortable\Controllers\Action {
/**
* Duplicate a entry
*
* @param string $uid
* @param int $to
*/
public function duplicate($uid, $to) {
$entries = $this->field()->entries();
$parent = $this->field()->origin();
$page = $entries->find($uid);
if($parent->ui()->create() === false) {
throw new Kirby\Panel\Exceptions\PermissionsException();
}
$page = $this->copy($page, $parent);
$entries->add($page->uid());
$this->sort($page->uid(), $to);
$this->notify(':)');
$this->redirect($this->model());
}
}

View file

@ -0,0 +1,33 @@
<?php
class DuplicateAction extends BaseAction {
public $icon = 'clone';
public $title = 'field.sortable.duplicate';
public function routes() {
return array(
array(
'pattern' => '(:any)/(:any)',
'method' => 'POST|GET',
'action' => 'duplicate',
'filter' => 'auth',
),
);
}
public function content() {
$content = parent::content();
$content->attr('href', $this->url() . '/' . $this->page()->uid() . '/' . ($this->layout()->num() + 1));
$content->data('action', true);
return $content;
}
public function disabled() {
return $this->disabled || $this->field()->origin()->ui()->create() === false;
}
}

View file

@ -0,0 +1,21 @@
<?php
class EditAction extends BaseAction {
public $icon = 'pencil';
public $title = 'field.sortable.edit';
public function content() {
$content = parent::content();
$content->attr('href', $this->page()->url('edit'));
return $content;
}
public function disabled() {
return $this->disabled || $this->page()->ui()->read() === false;
}
}

View file

@ -0,0 +1,67 @@
<?php
class PasteActionController extends LukasKleinschmidt\Sortable\Controllers\Action {
/**
* Add from clipboard
*/
public function paste() {
$self = $this;
$parent = $this->field()->origin();
$entries = site()->user()->clipboard();
if(empty($entries)) {
$entries = array();
}
$entries = pages($entries);
if($parent->ui()->create() === false) {
throw new Kirby\Panel\Exceptions\PermissionsException();
}
$form = $this->form('paste', array($parent, $entries, $this->model(), $this->field()), function($form) use($parent, $self) {
try {
$form->validate();
if(!$form->isValid()) {
throw new Exception($self->field()->l('field.sortable.paste.error.uri'));
}
$data = $form->serialize();
$templates = $parent->blueprint()->pages()->template()->pluck('name');
$entries = $self->field()->entries();
$to = $entries->count();
foreach(pages(str::split($data['uri'], ',')) as $page) {
if(!in_array($page->intendedTemplate(), $templates)) continue;
// Reset previously triggered hooks
kirby()::$triggered = array();
$page = $self->copy($page, $parent);
$entries->add($page->uid());
$self->sort($page->uid(), ++$to);
}
$self->notify(':)');
$self->redirect($self->model());
} catch(Exception $e) {
$form->alert($e->getMessage());
}
});
return $this->modal('paste', compact('form'));
}
}

View file

@ -0,0 +1,63 @@
<?php
return function($page, $entries, $model, $field) {
if($entries->count()) {
$templates = $page->blueprint()->pages()->template();
$options = [];
$help = false;
foreach($entries as $entry) {
$template = $entry->intendedTemplate();
$value = $entry->uri();
$options[$value] = array(
// 'label' => icon($templates->findBy('name', $template)->icon(), 'left') . ' ' . $entry->title(),
'label' => $entry->title(),
'checked' => true,
'readonly' => false,
);
if(v::notIn($template, $templates->pluck('name'))) {
$options[$value]['checked'] = false;
$options[$value]['readonly'] = true;
$help = true;
}
}
$form = new Kirby\Panel\Form(array(
'uri' => array(
'label' => $field->l('field.sortable.paste.uri.label'),
'type' => 'options',
'columns' => 1,
'required' => true,
'options' => $options,
'help' => $help ? $field->l('field.sortable.paste.uri.help') : '',
)
));
} else {
$form = new Kirby\Panel\Form(array(
'info' => array(
'label' => $field->l('field.sortable.paste.info.label'),
'type' => 'info',
'text' => $field->l('field.sortable.paste.info.text')
)
));
}
$form->cancel($model);
$form->buttons->submit->val($field->l('field.sortable.paste'));
if(!$entries->count()) {
$form->buttons->submit = $form->buttons->cancel;
$form->style('centered');
}
return $form;
};

View file

@ -0,0 +1,33 @@
<?php
class PasteAction extends BaseAction {
public $icon = 'paste';
public $class = 'sortable__action sortable__action--paste';
public $label = 'field.sortable.paste';
public function routes() {
return array(
array(
'pattern' => '/',
'method' => 'POST|GET',
'action' => 'paste',
'filter' => 'auth',
),
);
}
public function content() {
$content = parent::content();
$content->data('modal', true);
return $content;
}
public function disabled() {
return $this->disabled || $this->parent()->ui()->create() === false;
}
}

View file

@ -0,0 +1,3 @@
<div class="modal-content modal-content-small">
<?php echo $form; ?>
</div>

View file

@ -0,0 +1,75 @@
<?php
use Kirby\Panel\Exceptions\PermissionsException;
class ToggleActionController extends LukasKleinschmidt\Sortable\Controllers\Action {
/**
* Show page
*
* @param string $uid
* @param int $to
*/
public function show($uid, $to) {
$entries = $this->field()->entries();
$page = $entries->find($uid);
if($page->ui()->visibility() === false) {
throw new PermissionsException();
}
try {
// Check template specific limit
$count = $entries->filterBy('template', $page->intendedTemplate())->published()->count();
$limit = $this->field()->options($page)->limit();
if($limit && $count >= $limit) {
throw new Exception($this->field()->l('field.sortable.limit.template'));
}
// Check limit
$count = $entries->published()->count();
$limit = $this->field()->limit();
if($limit && $count >= $limit) {
throw new Exception($this->field()->l('field.sortable.limit'));
}
$page->sort($to);
$this->notify(':)');
} catch(Exception $e) {
$this->alert($e->getMessage());
}
$this->redirect($this->model());
}
/**
* Hide page
*
* @param string $uid
*/
public function hide($uid) {
$page = $this->field()->entries()->find($uid);
if($page->ui()->visibility() === false) {
throw new PermissionsException();
}
try {
$page->hide();
$this->notify(':)');
} catch(Exception $e) {
$this->alert($e->getMessage());
}
$this->redirect($this->model());
}
}

View file

@ -0,0 +1,66 @@
<?php
class ToggleAction extends BaseAction {
public $status;
public $icon = array(
'hide' => 'toggle-on',
'show' => 'toggle-off',
);
public $title = array(
'hide' => 'field.sortable.hide',
'show' => 'field.sortable.show',
);
public function routes() {
return array(
array(
'pattern' => 'show/(:any)/(:any)',
'method' => 'POST|GET',
'action' => 'show',
'filter' => 'auth',
),
array(
'pattern' => 'hide/(:any)',
'method' => 'POST|GET',
'action' => 'hide',
'filter' => 'auth',
),
);
}
public function status() {
if($this->status) return $this->status;
return $this->status = $this->page()->isVisible() ? 'hide' : 'show';
}
public function title() {
$title = a::get($this->title, $this->status());
return $this->i18n($title);
}
public function icon($position = null) {
if(empty($this->icon)) return null;
$icon = a::get($this->icon, $this->status());
return icon($icon, $position);
}
public function content() {
$content = parent::content();
$content->data('action', true);
$content->attr('href', $this->url() . '/' . $this->status() . '/' . $this->page()->uid());
if($this->status() == 'show') {
$content->attr('href', $content->attr('href') . '/' . ($this->layout()->numVisible() + 1));
}
return $content;
}
public function disabled() {
return $this->disabled || $this->page()->ui()->visibility() === false;
}
}

View file

@ -0,0 +1,24 @@
<?php
load([
// main class
'lukaskleinschmidt\\sortable\\sortable' => 'sortable.php',
// global stuff
'lukaskleinschmidt\\sortable\\registry' => 'sortable' . DS . 'registry.php',
'lukaskleinschmidt\\sortable\\roots' => 'sortable' . DS . 'roots.php',
// controllers
'lukaskleinschmidt\\sortable\\controllers\\field' => 'sortable' . DS . 'controllers' . DS . 'field.php',
'lukaskleinschmidt\\sortable\\controllers\\action' => 'sortable' . DS . 'controllers' . DS . 'action.php',
], __DIR__ . DS . 'src' );
class_alias('LukasKleinschmidt\\Sortable\\Sortable', 'LukasKleinschmidt\\Sortable');
// TEMP: Added for convenience because those two classes and namespaces were used in v2.3.1 and below
class_alias('LukasKleinschmidt\\Sortable\\Controllers\\Field', 'Kirby\\Sortable\\Controllers\\Field');
class_alias('LukasKleinschmidt\\Sortable\\Controllers\\Action', 'Kirby\\Sortable\\Controllers\\Action');
include(__DIR__ . DS . 'helpers.php');

View file

@ -0,0 +1,5 @@
<?php
function sortable() {
return LukasKleinschmidt\Sortable::instance();
}

View file

@ -0,0 +1,113 @@
<?php
class BaseLayout {
public $type;
public function root() {
$obj = new ReflectionClass($this);
return dirname($obj->getFileName());
}
public function __construct($type) {
$this->type = $type;
}
public function __call($method, $arguments) {
return isset($this->$method) ? $this->$method : null;
}
public function type() {
return $this->type;
}
public function input() {
return $this->field()->input($this->page()->uid());
}
public function counter() {
$page = $this->page();
if(!$page->isVisible() || !$this->limit()) {
return null;
}
$entries = $this->field()->entries()->filterBy('template', $page->intendedTemplate());
$index = $entries->published()->indexOf($page) + 1;
$limit = $this->limit();
$counter = new Brick('span');
$counter->addClass('sortable-layout__counter');
$counter->html('( ' . $index . ' / ' . $limit . ' )');
return $counter;
}
public function l($key, $variant = null) {
return $this->field()->l($key, $variant);
}
public function icon($position = '') {
return $this->page()->icon($position);
}
public function featured($position = '') {
return $this->page()->featured();
}
public function blueprint() {
return $this->page()->blueprint();
}
public function title() {
return $this->page()->title();
}
public function action($type, $data = array()) {
$data = a::update($data, array(
'layout' => $this,
'page' => $this->page(),
));
return $this->field()->action($type, $data);
}
public function content() {
$template = $this->root() . DS . 'template.php';
if(!is_file($template)) {
$template = __DIR__ . DS . 'template.php';
}
return tpl::load($template, ['layout' => $this], true);
}
public function template() {
$template = new Brick('div');
$template->addClass('sortable__layout');
$template->addClass('sortable__grid');
$template->attr('data-uid', $this->page()->uid());
$template->attr('data-visible', $this->page()->isVisible() ? 'true' : 'false');
$template->append($this->content());
$template->append($this->input());
return $template;
}
public function __toString() {
try {
return (string)$this->template();
} catch(Exception $e) {
return (string)$e->getMessage();
}
}
}

View file

@ -0,0 +1,17 @@
<div class="sortable-layout">
<nav class="sortable-layout__navigation" >
<div class="sortable-layout__icon" title="<?= l('pages.show.template') . ': ' . i18n($layout->blueprint()->title()); ?>" data-handle>
<?php if($layout->featured()->isNotEmpty()): ?>
<img src="<?= $layout->featured()->toFile()->thumb(array('width'=>400))->url(); ?>" alt="" width="100%">
<?php endif ?>
<div class="sortable-layout__title" title="<?= $layout->title(); ?>" data-handle>
<?= $layout->title(); ?>
<?= $layout->counter(); ?>
</div>
<?= $layout->action('edit'); ?>
<?= $layout->action('toggle'); ?>
</div>
</nav>
</div>

View file

@ -0,0 +1,49 @@
<?php
class ModuleLayout extends BaseLayout {
public $edit = true;
public $limit = false;
public $toggle = true;
public $delete = true;
public $preview = true;
public $duplicate = true;
public function preview() {
$page = $this->page();
if(!$preview = $this->preview) {
return;
}
$entry = Kirby\Modules\Modules::instance()->get($page);
$template = $entry->path() . DS . $entry->name() . '.preview.php';
if(!is_file($template)) {
return;
}
$position = $preview === true ? 'top' : $preview;
$preview = new Brick('div');
$preview->addClass('modules-layout__preview modules-layout__preview--' . $position);
$preview->data('module', $entry->name());
$preview->data('handle', true);
$preview->html(tpl::load($template, array('page' => $this->origin(), 'module' => $page, 'moduleName' => $entry->name())));
return $preview;
}
public function action($type, $data = array()) {
$data = a::update($data, array(
'disabled' => $this->$type() === false
));
return parent::action($type, $data);
}
}

View file

@ -0,0 +1,16 @@
<div class="sortable-layout">
<?php if($layout->preview === true || $layout->preview === 'top') echo $layout->preview(); ?>
<nav class="sortable-layout__navigation">
<div class="sortable-layout__icon" title="<?= l('pages.show.template') . ': ' . i18n($layout->blueprint()->title()); ?>" data-handle>
<?= $layout->icon(); ?>
</div>
<div class="sortable-layout__title" title="<?= $layout->title(); ?>" data-handle>
<?= $layout->title(); ?>
<?= $layout->counter(); ?>
</div>
<?php foreach($layout->field()->actions() as $action): ?>
<?= $layout->action($action); ?>
<?php endforeach; ?>
</nav>
<?php if($layout->preview === 'bottom') echo $layout->preview(); ?>
</div>

View file

@ -0,0 +1,154 @@
<?php
namespace LukasKleinschmidt\Sortable;
use A;
use F;
use L;
use Dir;
use Str;
use Data;
use Exception;
class Sortable {
static public $instance;
public $kirby;
public $roots;
public $registry;
public $variants;
public function __construct() {
$this->kirby = kirby();
$this->roots = new Roots(dirname(__DIR__));
$this->registry = new Registry($this->kirby());
}
public static function instance() {
if(!is_null(static::$instance)) return static::$instance;
return static::$instance = new static();
}
public static function layout($type, $data = array()) {
$class = $type . 'layout';
if(!class_exists($class)) {
throw new Exception('The ' . $type . ' layout is missing.');
}
$layout = new $class($type);
foreach($data as $key => $val) {
if(!is_string($key) || str::length($key) === 0) continue;
$layout->{$key} = $val;
}
return $layout;
}
public static function action($type, $data = array()) {
$class = $type . 'action';
if(!class_exists($class)) {
throw new Exception('The ' . $type . ' action is missing.');
}
$action = new $class($type);
foreach($data as $key => $val) {
if(!is_string($key) || str::length($key) === 0) continue;
$action->{$key} = $val;
}
return $action;
}
public function translation($key, $variant = null) {
// IDEA: outsource into own class
// $variants = $this->variants();
// return $variants->get($key, $variant, l::get($key));
if(!is_null($variant) && $variant = a::get($this->variants, $variant)) {
return a::get($variant, $key, l::get($key, $key));
}
return l::get($key, $key);
}
public function register() {
foreach(dir::read($this->roots()->translations()) as $name) {
$this->set('translation', f::name($name), $this->roots()->translations() . DS . $name);
}
foreach(dir::read($this->roots()->variants()) as $name) {
$this->set('variant', $name, $this->roots()->variants() . DS . $name);
}
foreach(dir::read($this->roots()->layouts()) as $name) {
$this->set('layout', $name, $this->roots()->layouts() . DS . $name);
}
foreach(dir::read($this->roots()->actions()) as $name) {
$this->set('action', $name, $this->roots()->actions() . DS . $name);
}
}
public function load() {
$code = panel()->translation()->code();
if(!$path = $this->get('translation', $code)) {
$path = $this->get('translation', 'en');
}
l::set(data::read($path));
if($variants = $this->get('variant', $code)) {
foreach($variants as $name => $path) {
$this->variants[$name] = data::read($path);
}
}
$classes = [];
foreach($this->get('layout') as $name => $layout) {
$classes[$layout->class()] = $layout->file();
}
foreach($this->get('action') as $name => $action) {
$classes[$action->class()] = $action->file();
}
load($classes);
}
public function kirby() {
return $this->kirby;
}
public function roots() {
return $this->roots;
}
public function set() {
return call_user_func_array([$this->registry, 'set'], func_get_args());
}
public function get() {
return call_user_func_array([$this->registry, 'get'], func_get_args());
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace LukasKleinschmidt\Sortable\Controllers;
use Kirby\Panel\View;
use Kirby\Panel\Snippet;
class Action extends Field {
public function __construct($model, $field, $action) {
$this->model = $model;
$this->field = $field;
$this->action = $action;
}
public function form($id, $data = array(), $submit = null) {
$file = $this->action->root() . DS . 'forms' . DS . $id . '.php';
return panel()->form($file, $data, $submit);
}
public function view($file, $data = array()) {
$view = new View($file, $data);
$root = $this->action->root() . DS . 'views';
if(file_exists($root . DS . $file . '.php')) {
$view->_root = $root;
}
return $view;
}
public function snippet($file, $data = array()) {
$snippet = new Snippet($file, $data);
$root = $this->action->root() . DS . 'snippets';
if(file_exists($root . DS . $file . '.php')) {
$snippet->_root = $root;
}
return $snippet;
}
}

View file

@ -0,0 +1,198 @@
<?php
namespace LukasKleinschmidt\Sortable\Controllers;
use Str;
use Dir;
use Router;
use Children;
use Kirby\Panel\Event;
use Kirby\Panel\Models\Page\Blueprint;
use Kirby\Panel\Exceptions\PermissionsException;
use LukasKleinschmidt\Sortable;
class Field extends \Kirby\Panel\Controllers\Field {
/**
* Initiates a action controller
*
* @param string $type
* @param string $path
* @return mixed
*/
public function forAction($type, $path = '') {
$model = $this->model();
$field = $this->field();
$action = sortable::action($type);
$routes = $action->routes();
$router = new Router($routes);
if($route = $router->run($path)) {
if(is_callable($route->action()) && is_a($route->action(), 'Closure')) {
return call($route->action(), $route->arguments());
} else {
$controllerFile = $action->root() . DS . 'controller.php';
$controllerName = $type . 'ActionController';
if(!file_exists($controllerFile)) {
throw new Exception('The action controller file is missing');
}
require_once($controllerFile);
if(!class_exists($controllerName)) {
throw new Exception('The action controller class is missing');
}
$controller = new $controllerName($model, $field, $action);
return call(array($controller, $route->action()), $route->arguments());
}
} else {
throw new Exception('Invalid action route');
}
}
/**
* Update field value and sort number
*
* @param string $uid
* @param int $to
*/
public function sort($uid, $to) {
try {
$entries = $this->field()->entries();
$value = $entries->not($uid)->pluck('uid');
// Order entries value
array_splice($value, $to - 1, 0, $uid);
if($entries->find($uid)->ui()->visibility() === false) {
throw new PermissionsException();
}
// Update field value
$this->update($value);
} catch(Exception $e) {
$this->alert($e->getMessage());
}
// Get current page
$page = $entries->find($uid);
// Figure out the correct sort num
if($page && $page->isVisible()) {
$collection = new Children($page->parent());
foreach(array_slice($value, 0, $to - 1) as $id) {
if($entry = $entries->find($id)) {
$collection->data[$entry->id()] = $entry;
}
}
try {
// Sort the page
$page->sort($collection->published()->count() + 1);
} catch(Exception $e) {
$this->alert($e->getMessage());
}
}
}
/**
* Copy a page to a new location
*
* @param object $page
* @param object $to
* @return object
*/
public function copy($page, $to) {
$template = $page->intendedTempalte();
$blueprint = new Blueprint($template);
$parent = $to;
$data = array();
$uid = $this->uid($page);
foreach($blueprint->fields(null) as $key => $field) {
$data[$key] = $field->default();
}
$data = array_merge($data, $page->content()->toArray());
$event = $parent->event('create:action', [
'parent' => $parent,
'template' => $template,
'blueprint' => $blueprint,
'uid' => $uid,
'data' => $data
]);
$event->check();
// Actually copy the page
dir::copy($page->root(), $parent->root() . DS . $uid);
$page = $parent->children()->find($uid);
if(!$page) {
throw new Exception(l('pages.add.error.create'));
}
kirby()->trigger($event, $page);
return $page;
}
/**
* Update the field value
*
* @param array $value
*/
public function update($value) {
try {
$this->model()->update(array(
$this->field()->name() => implode(', ', $value)
));
} catch(Exception $e) {
$this->alert($e->getMessage());
}
}
/**
* Create unique uid
*
* @param string $template
* @return string
*/
public function uid($template) {
if(is_a($template, 'Page')) {
$template = $template->intendedTemplate();
}
$prefix = $this->field()->prefix();
if(strpos($template, $prefix) !== false) {
$length = str::length($prefix);
$template = str::substr($template, $length);
}
// add a unique hash
$checksum = sprintf('%u', crc32($template . microtime()));
return $template . '-' . base_convert($checksum, 10, 36);
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace LukasKleinschmidt\Sortable;
// Kirby dependencies
use Exception;
use Kirby;
class Registry extends Kirby\Registry {
/**
* @param Kirby $kirby
*/
public function __construct(Kirby $kirby) {
$this->kirby = $kirby;
// start the registry entry autoloader
load([
'lukaskleinschmidt\\sortable\\registry\\translation' => __DIR__ . DS . 'registry' . DS . 'translation.php',
'lukaskleinschmidt\\sortable\\registry\\variant' => __DIR__ . DS . 'registry' . DS . 'variant.php',
'lukaskleinschmidt\\sortable\\registry\\layout' => __DIR__ . DS . 'registry' . DS . 'layout.php',
'lukaskleinschmidt\\sortable\\registry\\action' => __DIR__ . DS . 'registry' . DS . 'action.php',
]);
}
/**
* Returns a registry entry object by type
*
* @param string $type
* @param string $subtype
* @return Kirby\Registry\Entry
*/
public function entry($type, $subtype = null) {
$class = 'lukaskleinschmidt\\sortable\\registry\\' . $type;
if(!class_exists('lukaskleinschmidt\\sortable\\registry\\' . $type)) {
throw new Exception('Unsupported registry entry type: ' . $type);
}
return new $class($this);
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace LukasKleinschmidt\Sortable\Registry;
use Exception;
use Kirby;
use Obj;
use A;
class Action extends Kirby\Registry\Entry {
// Store of registered actions
protected static $actions = [];
/**
* Adds a new action to the registry
*
* @param mixed $name
* @param string $root
*/
public function set($name, $root = null) {
$name = strtolower($name);
$file = $root . DS . $name . '.php';
if(!$this->kirby->option('debug') || (is_dir($root) && is_file($file))) {
return static::$actions[$name] = new Obj([
'root' => $root,
'file' => $file,
'name' => $name,
'class' => $name . 'action',
]);
}
throw new Exception('The action does not exist at the specified path: ' . $root);
}
/**
* Retreives a registered action file
* If called without params, retrieves all registered actions
*
* @param string $name
* @return mixed
*/
public function get($name = null) {
if(is_null($name)) {
return static::$actions;
}
return a::get(static::$actions, $name);
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace LukasKleinschmidt\Sortable\Registry;
use Exception;
use Kirby;
use Obj;
use A;
class Layout extends Kirby\Registry\Entry {
// Store of registered layouts
protected static $layouts = [];
/**
* Adds a new layout to the registry
*
* @param mixed $name
* @param string $root
*/
public function set($name, $root = null) {
$name = strtolower($name);
$file = $root . DS . $name . '.php';
if(!$this->kirby->option('debug') || (is_dir($root) && is_file($file))) {
return static::$layouts[$name] = new Obj([
'root' => $root,
'file' => $file,
'name' => $name,
'class' => $name . 'layout',
]);
}
throw new Exception('The layout does not exist at the specified path: ' . $root);
}
/**
* Retreives a registered layout file
* If called without params, retrieves all registered layouts
*
* @param string $name
* @return mixed
*/
public function get($name = null) {
if(is_null($name)) {
return static::$layouts;
}
return a::get(static::$layouts, $name);
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace LukasKleinschmidt\Sortable\Registry;
use Exception;
use Kirby;
use Obj;
use A;
class Translation extends Kirby\Registry\Entry {
// Store of registered translations
protected static $translations = [];
/**
* Adds a new translation to the registry
*
* @param mixed $name
* @param string $path
*/
public function set($name, $path = null) {
if(!$this->kirby->option('debug') || is_file($path)) {
return static::$translations[$name] = $path;
}
throw new Exception('The translation does not exist at the specified path: ' . $root);
}
/**
* Retreives a registered translation file
* If called without params, retrieves all registered translations
*
* @param string $name
* @return mixed
*/
public function get($name = null) {
if(is_null($name)) {
return static::$translations;
}
return a::get(static::$translations, $name);
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace LukasKleinschmidt\Sortable\Registry;
use Exception;
use Kirby;
use Dir;
use Obj;
use A;
use F;
class Variant extends Kirby\Registry\Entry {
// Store of registered variants
protected static $variants = [];
/**
* Adds a new variant to the registry
*
* @param mixed $name
* @param string $root
*/
public function set($name, $root = null) {
//
// $name = strtolower($name);
// $files = array();
if(!$this->kirby->option('debug') || is_dir($root)) {
foreach(dir::read($root) as $file) {
static::$variants[f::name($file)][$name] = $root . DS . $file;
}
return static::$variants;
// return static::$variants[$name] = new Obj([
// 'root' => $root,
// 'name' => $name,
// 'files' => $files,
// ]);
}
throw new Exception('The variant does not exist at the specified path: ' . $root);
}
/**
* Retreives a registered variant file
* If called without params, retrieves all registered variants
*
* @param string $name
* @return mixed
*/
public function get($name = null) {
if(is_null($name)) {
return static::$variants;
}
return a::get(static::$variants, $name);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace LukasKleinschmidt\Sortable;
use Obj;
class Roots extends Obj {
public $index;
public function __construct($root) {
$this->index = $root;
$this->translations = $this->index . DS . 'translations';
$this->variants = $this->index . DS . 'variants';
$this->layouts = $this->index . DS . 'layouts';
$this->actions = $this->index . DS . 'actions';
}
public function __debuginfo() {
return parent::__debuginfo();
}
}

View file

@ -0,0 +1,37 @@
<?php
return array(
'field.sortable.empty' => 'Keine Seiten vorhanden.',
'field.sortable.or' => 'oder',
'field.sortable.add.first' => 'Füge die erste Seite hinzu',
'field.sortable.paste.first' => 'aus der Zwischenablage hinzufügen',
'field.sortable.limit' => 'Limit erreicht',
'field.sortable.limit.template' => 'Limit für diese Vorlage erreicht',
'field.sortable.add' => 'Hinzufügen',
'field.sortable.add.template.label' => 'Eine neue Seite hinzufügen',
'field.sortable.add.error.template' => 'Die Vorlage fehlt',
'field.sortable.copy' => 'Kopieren',
'field.sortable.copy.uri.label' => 'In die Zwischenablage kopieren',
'field.sortable.copy.info.label' => 'Keine Seiten vorhanden',
'field.sortable.copy.info.text' => 'Es gibt noch keine Seiten die du in der Zwischenablage speichern kannst.',
'field.sortable.copy.error.uri' => 'Wähle mindestens eine Seite aus',
'field.sortable.delete' => 'Löschen',
'field.sortable.delete.page.label' => 'Willst du diese Seite wirklich löschen?',
'field.sortable.duplicate' => 'Duplizieren',
'field.sortable.edit' => 'Bearbeiten',
'field.sortable.paste' => 'Einfügen',
'field.sortable.paste.uri.label' => 'Aus Zwischenablage einfügen',
'field.sortable.paste.uri.help' => 'Eine oder mehrere Vorlagen sind nicht verfügbar',
'field.sortable.paste.info.label' => 'Die Zwischenablage ist leer',
'field.sortable.paste.info.text' => 'In der Zwischenablage befinden sich derzeit keine Seiten.',
'field.sortable.paste.error.uri' => 'Wähle mindestens eine Seite aus',
'field.sortable.hide' => 'Verstecken',
'field.sortable.show' => 'Anzeigen',
);

View file

@ -0,0 +1,37 @@
<?php
return array(
'field.sortable.empty' => 'No pages yet.',
'field.sortable.or' => 'or',
'field.sortable.add.first' => 'Add the first page',
'field.sortable.paste.first' => 'add from clipboard',
'field.sortable.limit' => 'Limit reached',
'field.sortable.limit.template' => 'Limit reached for this template',
'field.sortable.add' => 'Add',
'field.sortable.add.template.label' => 'Add a new page',
'field.sortable.add.error.template' => 'The template is missing',
'field.sortable.copy' => 'Copy',
'field.sortable.copy.uri.label' => 'Copy to clipboard',
'field.sortable.copy.info.label' => 'No pages yet',
'field.sortable.copy.info.text' => 'There are no pages you can store in the clipboard yet.',
'field.sortable.copy.error.uri' => 'Select at least one page',
'field.sortable.delete' => 'Delete',
'field.sortable.delete.page.label' => 'Do you really want to delete this page?',
'field.sortable.duplicate' => 'Duplicate',
'field.sortable.edit' => 'Edit',
'field.sortable.paste' => 'Paste',
'field.sortable.paste.uri.label' => 'Paste from clipboard',
'field.sortable.paste.uri.help' => 'One or more templates are not available',
'field.sortable.paste.info.label' => 'Clipboard is empty',
'field.sortable.paste.info.text' => 'There are no pages stored in the clipboard at the moment.',
'field.sortable.paste.error.uri' => 'Select at least one page',
'field.sortable.hide' => 'Hide',
'field.sortable.show' => 'Show',
);

View file

@ -0,0 +1,37 @@
<?php
return array(
'field.sortable.empty' => 'Aucune page pour le moment.',
'field.sortable.or' => 'ou',
'field.sortable.add.first' => 'Ajouter la première page',
'field.sortable.paste.first' => 'ajouter du presse-papiers',
'field.sortable.limit' => 'Limite atteinte',
'field.sortable.limit.template' => 'Limite atteinte pour ce modèle',
'field.sortable.add' => 'Ajouter',
'field.sortable.add.template.label' => 'Ajouter une nouvelle page',
'field.sortable.add.error.template' => 'Le modèle est manquant',
'field.sortable.copy' => 'Copier',
'field.sortable.copy.uri.label' => 'Copier dans le presse-papiers',
'field.sortable.copy.info.label' => 'Aucune page pour le moment',
'field.sortable.copy.info.text' => 'Il n\'y a aucune page que vous pouvez stocker dans le presse-papiers pour le moment.',
'field.sortable.copy.error.uri' => 'Selectionner au moins une page',
'field.sortable.delete' => 'Effacer',
'field.sortable.delete.page.label' => 'Voulez-vous vraiment effacer cette page ?',
'field.sortable.duplicate' => 'Dupliquer',
'field.sortable.edit' => 'Editer',
'field.sortable.paste' => 'Coller',
'field.sortable.paste.uri.label' => 'Coller du presse-papiers',
'field.sortable.paste.uri.help' => 'Un ou plusieurs modèle ne sont pas disponibles',
'field.sortable.paste.info.label' => 'Le presse-papiers et vide',
'field.sortable.paste.info.text' => 'Il n\y a aucune page stockée dans le presse-papiers pour le moment.',
'field.sortable.paste.error.uri' => 'Selectionner au moins une page',
'field.sortable.hide' => 'Cacher',
'field.sortable.show' => 'Montrer',
);

View file

@ -0,0 +1,37 @@
<?php
return array(
'field.sortable.empty' => 'Nenhuma página até o momento.',
'field.sortable.or' => 'ou',
'field.sortable.add.first' => 'Adicione a primeira página',
'field.sortable.paste.first' => 'adicionar a partir da área de transferência',
'field.sortable.limit' => 'Limite alcançado',
'field.sortable.limit.template' => 'Limite alcançado para este template',
'field.sortable.add' => 'Adicionar',
'field.sortable.add.template.label' => 'Adicionar uma nova página',
'field.sortable.add.error.template' => 'Template não encontrado',
'field.sortable.copy' => 'Copiar',
'field.sortable.copy.uri.label' => 'Copiar para a área de transferência',
'field.sortable.copy.info.label' => 'Nenhuma página até o momento',
'field.sortable.copy.info.text' => 'Ainda não há nenhuma página para você copiar.',
'field.sortable.copy.error.uri' => 'Selecione pelo menos uma página',
'field.sortable.delete' => 'Excluir',
'field.sortable.delete.page.label' => 'Você realmente deseja excluir esta página?',
'field.sortable.duplicate' => 'Duplicar',
'field.sortable.edit' => 'Editar',
'field.sortable.paste' => 'Colar',
'field.sortable.paste.uri.label' => 'Colar da área de transferência',
'field.sortable.paste.uri.help' => 'Um ou mais templates não estão disponíveis',
'field.sortable.paste.info.label' => 'Área de transferência vazia',
'field.sortable.paste.info.text' => 'Ainda não há nenhuma página para você colar.',
'field.sortable.paste.error.uri' => 'Selecione pelo menos uma página',
'field.sortable.hide' => 'Esconder',
'field.sortable.show' => 'Mostrar',
);

View file

@ -0,0 +1,37 @@
<?php
return array(
'field.sortable.empty' => 'Inga inlägg är skapade ännu.',
'field.sortable.or' => 'eller',
'field.sortable.add.first' => 'Lägg till första inlägget',
'field.sortable.paste.first' => 'lägg till från urklipp',
'field.sortable.limit' => 'Gräns nådd',
'field.sortable.limit.template' => 'Gräns nådd för den här mallen',
'field.sortable.add' => 'Lägg till',
'field.sortable.add.template.label' => 'Lägg till nytt inlägg',
'field.sortable.add.error.template' => 'Mallen saknas',
'field.sortable.copy' => 'Kopiera',
'field.sortable.copy.uri.label' => 'Kopiera till urklipp',
'field.sortable.copy.info.label' => 'Inga inlägg ännu',
'field.sortable.copy.info.text' => 'Det finns inga inlägg som du kan spara i urklipp ännu.',
'field.sortable.copy.error.uri' => 'Välj minst ett inlägg',
'field.sortable.delete' => 'Ta bort',
'field.sortable.delete.page.label' => 'Är du säker på att du vill ta bort det här inlägget?',
'field.sortable.duplicate' => 'Duplicera',
'field.sortable.edit' => 'Ändra',
'field.sortable.paste' => 'Klistra in',
'field.sortable.paste.uri.label' => 'Klistra in från urklipp',
'field.sortable.paste.uri.help' => 'En eller fler mallar är inte tillgängliga',
'field.sortable.paste.info.label' => 'Urklipp är tomt',
'field.sortable.paste.info.text' => 'Det finns inga inlägg sparade i urklipp.',
'field.sortable.paste.error.uri' => 'Välj minst ett inlägg',
'field.sortable.hide' => 'Göm',
'field.sortable.show' => 'Visa',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'Keine Module vorhanden.',
'field.sortable.add.first' => 'Füge das erste Modul hinzu',
'field.sortable.add.template.label' => 'Ein neues Module hinzufügen',
'field.sortable.copy.info.label' => 'Keine Module vorhanden',
'field.sortable.copy.info.text' => 'Es gibt noch keine Module die du in der Zwischenablage speichern kannst.',
'field.sortable.copy.error.uri' => 'Wähle mindestens ein Modul aus',
'field.sortable.delete.page.label' => 'Willst du dieses Modul wirklich löschen?',
'field.sortable.paste.info.text' => 'In der Zwischenablage befinden sich derzeit keine Module.',
'field.sortable.paste.error.uri' => 'Wähle mindestens ein Modul aus',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'No modules yet.',
'field.sortable.add.first' => 'Add the first module',
'field.sortable.add.template.label' => 'Add a new module',
'field.sortable.copy.info.label' => 'No modules yet',
'field.sortable.copy.info.text' => 'There are no modules you can store in the clipboard yet.',
'field.sortable.copy.error.uri' => 'Select at least one module',
'field.sortable.delete.page.label' => 'Do you really want to delete this module?',
'field.sortable.paste.info.text' => 'There are no modules stored in the clipboard at the moment.',
'field.sortable.paste.error.uri' => 'Select at least one module',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'Aucun module pour le moment.',
'field.sortable.add.first' => 'Ajouter le premier module',
'field.sortable.add.template.label' => 'Ajouter un nouveau module',
'field.sortable.copy.info.label' => 'Aucun module pour le moment',
'field.sortable.copy.info.text' => 'Il n\'y a pas encore de modules que vous puissiez stocker dans le presse-papiers.',
'field.sortable.copy.error.uri' => 'Selectionner au moins un module',
'field.sortable.delete.page.label' => 'Voulez-vous vraiment effacer ce module ?',
'field.sortable.paste.info.text' => 'Il n\'y a pas de modules stockés dans le presse-papiers pour le moment.',
'field.sortable.paste.error.uri' => 'Selectionner au moins un module',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'Nenhum módulo até o momento.',
'field.sortable.add.first' => 'Adicione o primeiro módulo',
'field.sortable.add.template.label' => 'Adicionar um novo módulo',
'field.sortable.copy.info.label' => 'Nenhum módulo até o momento',
'field.sortable.copy.info.text' => 'Ainda não há nenhum módulo para você copiar.',
'field.sortable.copy.error.uri' => 'Selecione pelo menos um módulo',
'field.sortable.delete.page.label' => 'Você realmente deseja excluir este módulo?',
'field.sortable.paste.info.text' => 'Ainda não há nenhum módulo para você colar.',
'field.sortable.paste.error.uri' => 'Selecione pelo menos um módulo',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'Inga moduler är skapade ännu.',
'field.sortable.add.first' => 'Lägg till första modulen',
'field.sortable.add.template.label' => 'Lägg till en ny modul',
'field.sortable.copy.info.label' => 'Inga moduler ännu',
'field.sortable.copy.info.text' => 'Det finns inga moduler som du kan spara i urklipp ännu.',
'field.sortable.copy.error.uri' => 'Välj minst en modul',
'field.sortable.delete.page.label' => 'Är du säker på att du vill ta bort den här modulen?',
'field.sortable.paste.info.text' => 'Det finns inga moduler sparade i urklipp.',
'field.sortable.paste.error.uri' => 'Välj minst en modul',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'Keine Sektionen vorhanden.',
'field.sortable.add.first' => 'Füge die erste Sektion hinzu',
'field.sortable.add.template.label' => 'Eine neue Sektion hinzufügen',
'field.sortable.copy.info.label' => 'Keine Sektionen vorhanden',
'field.sortable.copy.info.text' => 'Es gibt noch keine Sektionen die du in der Zwischenablage speichern kannst.',
'field.sortable.copy.error.uri' => 'Wähle mindestens eine Sektion aus',
'field.sortable.delete.page.label' => 'Willst du diese Sektion wirklich löschen?',
'field.sortable.paste.info.text' => 'In der Zwischenablage befinden sich derzeit keine Seiten.',
'field.sortable.paste.error.uri' => 'Wähle mindestens eine Seite aus',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'No sections yet.',
'field.sortable.add.first' => 'Add the first section',
'field.sortable.add.template.label' => 'Add a new section',
'field.sortable.copy.info.label' => 'No sections yet',
'field.sortable.copy.info.text' => 'There are no sections you can store in the clipboard yet.',
'field.sortable.copy.error.uri' => 'Select at least one section',
'field.sortable.delete.page.label' => 'Do you really want to delete this section?',
'field.sortable.paste.info.text' => 'There are no sections stored in the clipboard at the moment.',
'field.sortable.paste.error.uri' => 'Select at least one section',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'Aucune section pour le moment.',
'field.sortable.add.first' => 'Ajouter la première section',
'field.sortable.add.template.label' => 'Ajouter une nouvelle section',
'field.sortable.copy.info.label' => 'Aucune section pour le moment',
'field.sortable.copy.info.text' => 'Il n\'y a pas encore de sections que vous puissiez stocker dans le presse-papiers.',
'field.sortable.copy.error.uri' => 'Selectionner au moins une section',
'field.sortable.delete.page.label' => 'Voulez-vous vraiment effacer cette section ?',
'field.sortable.paste.info.text' => 'Il n\'y a pas de sections stockées dans le presse-papiers pour le moment.',
'field.sortable.paste.error.uri' => 'Selectionner au moins une section',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'Nenhuma seção até o momento.',
'field.sortable.add.first' => 'Adicione a primeira seção',
'field.sortable.add.template.label' => 'Adicionar uma nova seção',
'field.sortable.copy.info.label' => 'Nenhuma seção até o momento',
'field.sortable.copy.info.text' => 'Ainda não há nenhuma seção para você copiar.',
'field.sortable.copy.error.uri' => 'Selecione pelo menos uma seção',
'field.sortable.delete.page.label' => 'Você realmente deseja excluir esta seção?',
'field.sortable.paste.info.text' => 'Ainda não há nenhuma seção para você colar.',
'field.sortable.paste.error.uri' => 'Selecione pelo menos uma seção',
);

View file

@ -0,0 +1,17 @@
<?php
return array(
'field.sortable.empty' => 'Inga sektioner är skapade ännu.',
'field.sortable.add.first' => 'Lägg till första sektionen',
'field.sortable.add.template.label' => 'Lägg till en ny sektion',
'field.sortable.copy.info.label' => 'Inga sektioner ännu',
'field.sortable.copy.info.text' => 'Det finns inga sektioner som du kan spara i urklipp ännu.',
'field.sortable.copy.error.uri' => 'Välj minst en sektion',
'field.sortable.delete.page.label' => 'Är du säker på att du vill ta bort den här sektionen?',
'field.sortable.paste.info.text' => 'Det finns inga sektioner sparade i urklipp.',
'field.sortable.paste.error.uri' => 'Välj minst en sektion',
);