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> -->