Introduce ReorderController for reordering of model records - Fixes #943

This commit is contained in:
Samuel Georges 2015-08-22 16:33:00 +10:00
parent 60c18d39df
commit 6d1673c4b3
5 changed files with 381 additions and 0 deletions

View File

@ -0,0 +1,247 @@
<?php namespace Backend\Behaviors;
use Lang;
use Backend;
use Backend\Classes\ControllerBehavior;
/**
* Reorder Controller Behavior
* Used for reordering and sorting records.
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*/
class ReorderController extends ControllerBehavior
{
/**
* {@inheritDoc}
*/
protected $requiredProperties = ['reorderConfig'];
/**
* @var array Configuration values that must exist when applying the primary config file.
*/
protected $requiredConfig = ['modelClass'];
/**
* @var Model Import model
*/
public $model;
/**
* @var bool Display parent/child relationships in the list.
*/
protected $showTree = false;
/**
* @var string Reordering mode:
* - simple: October\Rain\Database\Traits\Sortable
* - nested: October\Rain\Database\Traits\NestedTree
*/
protected $sortMode = null;
/**
* @var Backend\Classes\WidgetBase Reference to the widget used for the toolbar.
*/
protected $toolbarWidget;
/**
* Behavior constructor
* @param Backend\Classes\Controller $controller
*/
public function __construct($controller)
{
parent::__construct($controller);
/*
* Build configuration
*/
$this->config = $this->makeConfig($controller->reorderConfig, $this->requiredConfig);
/*
* Form widgets
*/
if ($this->toolbarWidget = $this->makeToolbarWidget()) {
$this->toolbarWidget->bindToController();
}
}
//
// Controller actions
//
public function reorder()
{
$this->addJs('js/october.reorder.js', 'core');
$this->controller->pageTitle = $this->controller->pageTitle
?: Lang::get($this->getConfig('title', 'backend::lang.reorder.default_title'));
$this->validateModel();
$this->prepareVars();
}
//
// AJAX
//
public function onReorder()
{
$model = $this->validateModel();
/*
* Simple
*/
if ($this->sortMode == 'simple') {
if (!$ids = post('record_ids')) return;
if (!$orders = post('sort_orders')) return;
$model->setSortableOrder($ids, $orders);
}
/*
* Nested set
*/
elseif ($this->sortMode == 'nested') {
$sourceNode = $model->find(post('sourceNode'));
$targetNode = post('targetNode') ? $model->find(post('targetNode')) : null;
if ($sourceNode == $targetNode) return;
switch (post('position')) {
case 'before':
$sourceNode->moveBefore($targetNode);
break;
case 'after':
$sourceNode->moveAfter($targetNode);
break;
case 'child':
$sourceNode->makeChildOf($targetNode);
break;
default:
$sourceNode->makeRoot();
break;
}
}
}
//
// Reordering
//
/**
* Prepares common form data
*/
protected function prepareVars()
{
$this->vars['reorderRecords'] = $this->getRecords();
$this->vars['reorderModel'] = $this->model;
$this->vars['reorderSortMode'] = $this->sortMode;
$this->vars['reorderShowTree'] = $this->showTree;
$this->vars['reorderToolbarWidget'] = $this->toolbarWidget;
}
public function reorderRender()
{
return $this->reorderMakePartial('container');
}
public function reorderGetModel()
{
if ($this->model !== null) {
return $this->model;
}
$modelClass = $this->getConfig('modelClass');
if (!$modelClass) {
throw new ApplicationException('Please specify the modelClass property for reordering');
}
return $this->model = new $modelClass;
}
/**
* Validate the supplied form model.
* @return void
*/
protected function validateModel()
{
$model = $this->controller->reorderGetModel();
$modelTraits = class_uses($model);
if (isset($modelTraits['October\Rain\Database\Traits\Sortable'])) {
$this->sortMode = 'simple';
}
elseif (isset($modelTraits['October\Rain\Database\Traits\NestedTree'])) {
$this->sortMode = 'nested';
$this->showTree = true;
}
else {
throw new ApplicationException('The model must implement the NestedTree or Sortable traits.');
}
return $model;
}
/**
* Returns all the records from the supplied model.
* @return Collection
*/
protected function getRecords()
{
$model = $this->controller->reorderGetModel();
$records = null;
if ($this->sortMode == 'simple') {
$records = $model
->orderBy($model->getSortOrderColumn())
->get()
;
}
elseif ($this->sortMode == 'nested') {
$records = $model->getEagerRoot();
}
return $records;
}
//
// Widgets
//
protected function makeToolbarWidget()
{
if ($toolbarConfig = $this->getConfig('toolbar')) {
$toolbarConfig = $this->makeConfig($toolbarConfig);
$toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig);
}
else {
$toolbarWidget = null;
}
return $toolbarWidget;
}
//
// Helpers
//
/**
* Controller accessor for making partials within this behavior.
* @param string $partial
* @param array $params
* @return string Partial contents
*/
public function reorderMakePartial($partial, $params = [])
{
$contents = $this->controller->makePartial('reorder_'.$partial, $params + $this->vars, false);
if (!$contents) {
$contents = $this->makePartial($partial, $params);
}
return $contents;
}
}

View File

@ -0,0 +1,82 @@
/*
* Scripts for the Reorder controller behavior.
*
* The following functions are observed:
* - Simple sorting: Post back the original sort orders and the new ordered identifiers.
* - Nested sorting: Post back source and target nodes IDs and the move positioning.
*/
+function ($) { "use strict";
var ReorderBehavior = function() {
this.sortMode = null
this.simpleSortOrders = []
this.initSorting = function (mode) {
this.sortMode = mode
if (mode == 'simple') {
this.initSortingSimple()
}
$('#reorderTreeList').on('move.oc.treelist', $.proxy(this.processReorder, this))
}
this.processReorder = function(ev, sortData){
var postData
if (this.sortMode == 'simple') {
postData = { sort_orders: this.simpleSortOrders }
}
else if (this.sortMode == 'nested') {
postData = this.getNestedMoveData(sortData)
}
$('#reorderTreeList').request('onReorder', {
data: postData
})
}
this.getNestedMoveData = function (sortData) {
var
$el,
$item = sortData.item,
moveData = {
targetNode: 0,
sourceNode: $item.data('recordId'),
position: 'root'
}
if (($el = $item.next()) && $el.length) {
moveData.position = 'before'
}
else if (($el = $item.prev()) && $el.length) {
moveData.position = 'after'
}
else if (($el = $item.parents('li:first')) && $el.length) {
moveData.position = 'child'
}
if ($el.length) {
moveData.targetNode = $el.data('recordId')
}
return moveData
}
this.initSortingSimple = function () {
var sortOrders = []
$('#reorderTreeList li').each(function(){
sortOrders.push($(this).data('recordSortOrder'))
})
this.simpleSortOrders = sortOrders
}
}
$.oc.reorderBehavior = new ReorderBehavior;
}(window.jQuery);

View File

@ -0,0 +1,29 @@
<?php if ($reorderToolbarWidget): ?>
<!-- Reorder Toolbar -->
<div id="<?= $this->getId('reorderToolbar') ?>" class="reorder-toolbar">
<?= $reorderToolbarWidget->render() ?>
</div>
<?php endif ?>
<!-- Reorder List -->
<?= Form::open() ?>
<div
id="reorderTreeList"
class="control-treelist"
data-control="treelist"
<?= $reorderShowTree ? '' : 'data-nested="0"' ?>
data-handle="<?= $reorderShowTree ? 'a.move' : '> li > .record > a.move' ?>"
data-stripe-load-indicator>
<?php if ($reorderRecords): ?>
<ol id="reorderRecords">
<?= $this->reorderMakePartial('records', ['records' => $reorderRecords]) ?>
</ol>
<?php else: ?>
<p><?= Lang::get('backend::lang.reorder.no_records') ?></p>
<?php endif ?>
</div>
<?= Form::close() ?>
<script>
$.oc.reorderBehavior.initSorting('<?= $reorderSortMode ?>')
</script>

View File

@ -0,0 +1,19 @@
<?php foreach ($records as $record): ?>
<li data-record-id="<?= $record->id ?>" data-record-sort-order="<?= $record->sort_order ?>">
<div class="record">
<a href="javascript:;" class="move"></a>
<span><?= $record->title ?></span>
<input name="record_ids[]" type="hidden" value="<?= $record->id ?>" />
</div>
<?php if ($reorderShowTree): ?>
<ol>
<?php if ($record->children): ?>
<?= $this->reorderMakePartial('records', ['records' => $record->children]) ?>
<?php endif ?>
</ol>
<?php endif ?>
</li>
<?php endforeach ?>

View File

@ -236,6 +236,10 @@ return [
'unlink_name' => 'Unlink :name',
'unlink_confirm' => 'Are you sure?'
],
'reorder' => [
'default_title' => 'Reorder records',
'no_records' => 'There are no records available to sort.',
],
'model' => [
'name' => 'Model',
'not_found' => "Model ':class' with an ID of :id could not be found",