diff --git a/modules/backend/behaviors/ReorderController.php b/modules/backend/behaviors/ReorderController.php new file mode 100644 index 000000000..5b7356d24 --- /dev/null +++ b/modules/backend/behaviors/ReorderController.php @@ -0,0 +1,247 @@ +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; + } + +} \ No newline at end of file diff --git a/modules/backend/behaviors/reordercontroller/assets/js/october.reorder.js b/modules/backend/behaviors/reordercontroller/assets/js/october.reorder.js new file mode 100644 index 000000000..5ce495748 --- /dev/null +++ b/modules/backend/behaviors/reordercontroller/assets/js/october.reorder.js @@ -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); \ No newline at end of file diff --git a/modules/backend/behaviors/reordercontroller/partials/_container.htm b/modules/backend/behaviors/reordercontroller/partials/_container.htm new file mode 100644 index 000000000..6d329f38a --- /dev/null +++ b/modules/backend/behaviors/reordercontroller/partials/_container.htm @@ -0,0 +1,29 @@ + + +
+ render() ?> +
+ + + + +
+ data-handle=" li > .record > a.move' ?>" + data-stripe-load-indicator> + +
    + reorderMakePartial('records', ['records' => $reorderRecords]) ?> +
+ +

+ +
+ + + diff --git a/modules/backend/behaviors/reordercontroller/partials/_records.htm b/modules/backend/behaviors/reordercontroller/partials/_records.htm new file mode 100644 index 000000000..fe29ef9c1 --- /dev/null +++ b/modules/backend/behaviors/reordercontroller/partials/_records.htm @@ -0,0 +1,19 @@ + + +
  • +
    + + title ?> + +
    + + +
      + children): ?> + reorderMakePartial('records', ['records' => $record->children]) ?> + +
    + +
  • + + \ No newline at end of file diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php index e7bfdcb75..89beb9a7f 100644 --- a/modules/backend/lang/en/lang.php +++ b/modules/backend/lang/en/lang.php @@ -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",