diff --git a/modules/backend/behaviors/ListController.php b/modules/backend/behaviors/ListController.php index e69326e8ef..b1dd201c6a 100644 --- a/modules/backend/behaviors/ListController.php +++ b/modules/backend/behaviors/ListController.php @@ -5,6 +5,7 @@ use Flash; use ApplicationException; use Backend\Classes\ControllerBehavior; +use Winter\Storm\Database\Traits\HasSortableRelations; /** * Adds features for working with backend lists. @@ -150,6 +151,7 @@ public function makeList($definition = null) 'showCheckboxes', 'showTree', 'treeExpanded', + 'sortable', 'customViewPath', ]; @@ -431,6 +433,22 @@ public function listRefresh(string $definition = null) return $this->listWidgets[$definition]->onRefresh(); } + /** + * Returns the sort order value for a specific record. + */ + public function getRecordSortOrder($record, $relation = '') + { + if ($relation) { + /** @var HasSortableRelations $modelInstance */ + $modelInstance = new $this->config->modelClass; + $reorderColumn = $modelInstance->getRelationSortOrderColumn($relation); + + return $this->controller->asExtension('ReorderRelationController')->getRelationRecordSortOrder($record, $reorderColumn); + } + + return $record->{$record->getSortOrderColumn()}; + } + /** * Returns the widget used by this behavior. * @return \Backend\Classes\WidgetBase diff --git a/modules/backend/behaviors/RelationController.php b/modules/backend/behaviors/RelationController.php index 9e0f221ed8..f77c660114 100644 --- a/modules/backend/behaviors/RelationController.php +++ b/modules/backend/behaviors/RelationController.php @@ -667,6 +667,10 @@ protected function makeViewWidget() $config->alias = $this->alias . 'ViewList'; $config->showSorting = $this->getConfig('view[showSorting]', true); $config->defaultSort = $this->getConfig('view[defaultSort]'); + $config->sortable = $this->getConfig('view[sortable]', false); + $config->reorderRelation = $this->relationName; + $config->reorderModel = get_class($this->model); + $config->reorderParentId = $this->model->getKey(); $config->recordsPerPage = $this->getConfig('view[recordsPerPage]'); $config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly); $config->recordUrl = $this->getConfig('view[recordUrl]'); diff --git a/modules/backend/behaviors/ReorderRelationController.php b/modules/backend/behaviors/ReorderRelationController.php new file mode 100644 index 0000000000..9740ffe280 --- /dev/null +++ b/modules/backend/behaviors/ReorderRelationController.php @@ -0,0 +1,245 @@ +config = []; + if ($controller->reorderRelationConfig) { + $this->config = $this->makeConfig($controller->reorderRelationConfig, []); + } + } + + // + // AJAX + // + + /** + * Updates the relation order. + * @throws \Exception + */ + public function onReorderRelation() + { + $this->reorderRelationGetModel(); + $this->validateModel(); + + if ( + (!$ids = post('record_ids')) || + (!$orders = post('sort_orders')) + ) { + return; + } + + /** @var HasSortableRelations $instance */ + $instance = $this->parentModel->newQuery()->find($this->postValue('_reorder_parent_id')); + $instance->setRelationOrder($this->relation, $ids, $orders); + + // refresh the relation view after reorder + $this->controller->initRelation( + $this->parentModel->newQuery()->findOrFail($this->postValue('_reorder_parent_id')), + $this->relation + ); + return $this->controller->relationRefresh($this->relation); + } + + // + // Reordering + // + + /** + * Sets all required model properties. + */ + public function reorderRelationGetModel() + { + $this->parentModel = $this->reorderRelationGetParentModel(); + $this->relation = post('_reorder_relation_name'); + + $relationModelClass = array_get($this->parentModel->getRelationDefinition($this->relation), 0); + if (!$relationModelClass) { + throw new ApplicationException( + sprintf('Could not determine model class for relation "%s"', $this->relation) + ); + } + + return $this->model = new $relationModelClass; + } + + /** + * Returns all the records from the supplied model. + * @return Collection + */ + protected function getRecords() + { + $query = $this->parentModel->newQuery(); + + $this->controller->reorderExtendRelationQuery($query); + + return $query + ->with([$this->relation => function ($q) { + $q->orderBy($this->parentModel->getRelationSortOrderColumn($this->relation), 'ASC'); + }]) + ->findOrFail($this->postValue('_reorder_parent_id')) + ->{$this->relation}; + } + + /** + * Extend the relation query used for finding reorder records. Extra conditions + * can be applied to the query, for example, $query->withTrashed(); + * @param Winter\Storm\Database\Builder $query + * @return void + */ + public function reorderExtendRelationQuery($query) + { + } + + // + // Helpers + // + + /** + * Prepares common partial variables. + */ + protected function prepareVars() + { + $this->vars['reorderRecords'] = $this->getRecords(); + $this->vars['reorderModel'] = $this->model; + } + + /** + * Return a model instance based on the _reorder_model post value. + * @return Model + */ + public function reorderRelationGetParentModel() + { + $model = $this->postValue('_reorder_model'); + + if (!class_exists($model)) { + throw new ApplicationException( + sprintf('Model class "%s" does not exist', $model) + ); + } + + return new $model; + } + + public function getRelationRecordSortOrder($record, $sortColumn) + { + return $record->pivot ? $record->pivot->{$sortColumn} : $record->{$sortColumn}; + } + + /** + * Validate the supplied form model. + * @return void + */ + protected function validateModel() + { + $modelTraits = class_uses($this->parentModel); + + if (!isset($modelTraits[\Winter\Storm\Database\Traits\HasSortableRelations::class])) { + throw new ApplicationException( + sprintf('The "%s" model must implement the HasSortableRelations trait.', get_class($this->parentModel)) + ); + } + } + + /** + * Returns the name attribute for a given $record. The attribute + * defined in the behaviour config is used here. + * + * @param Model $record + * @param $relation + * + * @return string + */ + public function reorderRelationGetRecordName(Model $record, $relation) + { + $attribute = array_get((array)$this->config, "$relation.nameFrom"); + if ($attribute) { + return (string)$record->$attribute; + } + + // Take a guess if no "nameFrom" config is set. + return (string)($record->name ?: $record->title); + } + + /** + * Controller accessor for making partials within this behavior. + * @param string $partial + * @param array $params + * @return string Partial contents + */ + public function reorderRelationMakePartial($partial, $params = []) + { + $contents = $this->controller->makePartial( + 'reorder_' . $partial, + $params + $this->vars, + false + ); + + if (!$contents) { + $contents = $this->makePartial($partial, $params); + } + + return $contents; + } + + /** + * Fetch a post value for the current relation. + * + * @param string $key + * + * @return mixed + */ + private function postValue(string $key) + { + $relation = post('_reorder_relation_name'); + + return post($key. '.' . $relation); + } +} diff --git a/modules/backend/behaviors/relationcontroller/partials/_button_reorder.htm b/modules/backend/behaviors/relationcontroller/partials/_button_reorder.htm new file mode 100644 index 0000000000..c1b7f4f70c --- /dev/null +++ b/modules/backend/behaviors/relationcontroller/partials/_button_reorder.htm @@ -0,0 +1,14 @@ +vars['relationField']; ?> + + + diff --git a/modules/backend/behaviors/reordercontroller/assets/js/winter.reorder.js b/modules/backend/behaviors/reordercontroller/assets/js/winter.reorder.js index e3b5035352..023beefda8 100644 --- a/modules/backend/behaviors/reordercontroller/assets/js/winter.reorder.js +++ b/modules/backend/behaviors/reordercontroller/assets/js/winter.reorder.js @@ -6,15 +6,16 @@ * - Nested sorting: Post back source and target nodes IDs and the move positioning. */ +function ($) { "use strict"; - var ReorderBehavior = function() { this.sortMode = null + this.context = 'default' this.simpleSortOrders = [] - this.initSorting = function (mode) { + this.initSorting = function (mode, context) { this.sortMode = mode + this.context = context if (mode == 'simple') { this.initSortingSimple() @@ -34,7 +35,9 @@ postData = this.getNestedMoveData(sortData) } - $('#reorderTreeList').request('onReorder', { + var handler = this.context === 'relation' ? 'onReorderRelation' : 'onReorder' + + $('#reorderTreeList').request(handler, { data: postData }) } @@ -79,4 +82,4 @@ } $.wn.reorderBehavior = new ReorderBehavior; -}(window.jQuery); \ No newline at end of file +}(window.jQuery); diff --git a/modules/backend/behaviors/reorderrelationcontroller/partials/_container.htm b/modules/backend/behaviors/reorderrelationcontroller/partials/_container.htm new file mode 100644 index 0000000000..bb3e6ec93b --- /dev/null +++ b/modules/backend/behaviors/reorderrelationcontroller/partials/_container.htm @@ -0,0 +1,30 @@ + + + + + + + +
+ +
    + reorderRelationMakePartial('records', [ + 'records' => $reorderRecords, + 'sortColumn' => $reorderSortColumn, + 'relation' => $reorderRelation + ]) ?> +
+ +

+ +
+ + + diff --git a/modules/backend/behaviors/reorderrelationcontroller/partials/_records.htm b/modules/backend/behaviors/reorderrelationcontroller/partials/_records.htm new file mode 100644 index 0000000000..b25d939efe --- /dev/null +++ b/modules/backend/behaviors/reorderrelationcontroller/partials/_records.htm @@ -0,0 +1,13 @@ + + +
  • +
    + + reorderRelationGetRecordName($record, $relation)) ?> + +
    +
  • + + diff --git a/modules/backend/behaviors/reorderrelationcontroller/partials/_relation_modal.htm b/modules/backend/behaviors/reorderrelationcontroller/partials/_relation_modal.htm new file mode 100644 index 0000000000..8df704ad9d --- /dev/null +++ b/modules/backend/behaviors/reorderrelationcontroller/partials/_relation_modal.htm @@ -0,0 +1,28 @@ + + + + + diff --git a/modules/backend/behaviors/reorderrelationcontroller/partials/_request_params.htm b/modules/backend/behaviors/reorderrelationcontroller/partials/_request_params.htm new file mode 100644 index 0000000000..fd580ed9c6 --- /dev/null +++ b/modules/backend/behaviors/reorderrelationcontroller/partials/_request_params.htm @@ -0,0 +1,6 @@ +data-request="onRelationModalClose" +data-request-data=" + _reorder_relation_name: '', + _reorder_model[]: '', + _reorder_parent_id[]: '', +" diff --git a/modules/backend/lang/de/lang.php b/modules/backend/lang/de/lang.php index 9c8148c1a5..8e4e11e77f 100644 --- a/modules/backend/lang/de/lang.php +++ b/modules/backend/lang/de/lang.php @@ -205,6 +205,7 @@ 'refresh' => 'Erneuern', 'updating' => 'Aktualisiere...', 'loading' => 'Laden...', + 'sort_handle' => 'Sortierung', 'setup_title' => 'Listen Setup', 'setup_help' => 'Benutzen Sie Checkboxen, um Spalten auszuwählen, welche Sie in den Listen sehen möchten. Sie können die Position der Spalten ändern, indem Sie diese hinauf oder hinunter ziehen.', 'records_per_page' => 'Aufzeichnungen pro Seite', @@ -334,7 +335,8 @@ ], 'reorder' => [ 'default_title' => 'Einträge sortieren', - 'no_records' => 'Es gibt keine Einträge zum sortieren.' + 'no_records' => 'Es gibt keine Einträge zum sortieren.', + 'relation' => 'Sortiere verwandte Einträge', ], 'model' => [ 'name' => "Model", diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php index 7b29eea022..86e0ac16df 100644 --- a/modules/backend/lang/en/lang.php +++ b/modules/backend/lang/en/lang.php @@ -215,6 +215,7 @@ 'refresh' => 'Refresh', 'updating' => 'Updating...', 'loading' => 'Loading...', + 'sort_handle' => 'Sort handle', 'setup_title' => 'List setup', 'setup_help' => 'Use checkboxes to select columns you want to see in the list. You can change position of columns by dragging them up or down.', 'records_per_page' => 'Records per page', @@ -351,6 +352,7 @@ 'reorder' => [ 'default_title' => 'Reorder records', 'no_records' => 'There are no records available to sort.', + 'relation' => 'Sort related entries', ], 'model' => [ 'name' => 'Model', diff --git a/modules/backend/widgets/Lists.php b/modules/backend/widgets/Lists.php index 0e551f54dd..acb4cc4a79 100644 --- a/modules/backend/widgets/Lists.php +++ b/modules/backend/widgets/Lists.php @@ -9,6 +9,8 @@ use Carbon\Carbon; use Winter\Storm\Html\Helper as HtmlHelper; use Winter\Storm\Router\Helper as RouterHelper; +use Winter\Storm\Database\Traits\Sortable; +use Winter\Storm\Database\Traits\HasSortableRelations; use System\Helpers\DateTime as DateTimeHelper; use System\Classes\PluginManager; use System\Classes\MediaLibrary; @@ -74,6 +76,11 @@ class Lists extends WidgetBase */ public $showSorting = true; + /** + * @var bool Makes this list sortable in-place. + */ + public $sortable = false; + /** * @var mixed A default sort column to look for. */ @@ -187,6 +194,26 @@ class Lists extends WidgetBase */ protected $sortDirection; + /** + * @var string Name of the relation to sort. + */ + protected $reorderRelation; + + /** + * @var string Class path of the model to sort. + */ + protected $reorderModel; + + /** + * @var string ID of the parent model (used when sorting relations). + */ + protected $reorderParentId; + + /** + * @var string Column to sort by. + */ + protected $reorderColumn; + /** * @var array List of CSS classes to apply to the list container element */ @@ -206,6 +233,10 @@ public function init() 'showPageNumbers', 'recordsPerPage', 'perPageOptions', + 'sortable', + 'reorderRelation', + 'reorderModel', + 'reorderParentId', 'showSorting', 'defaultSort', 'showCheckboxes', @@ -233,6 +264,20 @@ public function init() $this->validateModel(); $this->validateTree(); + + if ($this->sortable) { + $this->addJs('/modules/system/assets/ui/js/list.sortable.js', 'core'); + $this->showSorting = false; + $this->showTree = false; + + if ($this->reorderRelation) { + /** @var HasSortableRelations $modelInstance */ + $modelInstance = new $this->reorderModel; + $this->reorderColumn = $modelInstance->getRelationSortOrderColumn($this->reorderRelation); + } else { + $this->reorderColumn = $this->model->getSortOrderColumn(); + } + } } /** @@ -267,6 +312,10 @@ public function prepareVars() $this->vars['showPagination'] = $this->showPagination; $this->vars['showPageNumbers'] = $this->showPageNumbers; $this->vars['showSorting'] = $this->showSorting; + $this->vars['sortable'] = $this->sortable; + $this->vars['reorderModel'] = $this->reorderModel; + $this->vars['reorderRelation'] = $this->reorderRelation; + $this->vars['reorderParentId'] = $this->reorderParentId; $this->vars['sortColumn'] = $this->getSortColumn(); $this->vars['sortDirection'] = $this->sortDirection; $this->vars['showTree'] = $this->showTree; @@ -340,6 +389,18 @@ protected function validateModel() )); } + if ($this->sortable) { + $checkModel = $this->reorderRelation ? $this->reorderModel : $this->model; + $needsTrait = $this->reorderRelation ? HasSortableRelations::class : Sortable::class; + $modelTraits = class_uses($checkModel); + + if (!isset($modelTraits[$needsTrait])) { + throw new ApplicationException( + sprintf('The "%s" model must implement the "%s" trait.', get_class($this->model), $needsTrait) + ); + } + } + return $this->model; } @@ -543,6 +604,12 @@ public function prepareQuery() $sortColumn = Str::snake($column->relation) . '_count'; } + // Fix the sort order if this list has sortable set to true. + if ($this->sortable) { + $sortColumn = $this->reorderColumn; + $this->sortDirection = 'ASC'; + } + $query->orderBy($sortColumn, $this->sortDirection); } @@ -782,6 +849,17 @@ protected function defineListColumns() throw new ApplicationException(Lang::get('backend::lang.list.missing_columns', compact('class'))); } + if ($this->sortable) { + $this->allColumns['sort_handle'] = $this->makeListColumn('sort_handle', [ + 'label' => 'backend::lang.list.sort_handle', + 'path' => '~/modules/backend/widgets/lists/partials/_list_sort_handle.htm', + 'type' => 'partial', + 'width' => '20px', + 'sortable' => false, + 'clickable' => false, + ]); + } + $this->addColumns($this->columns); /** @@ -964,6 +1042,11 @@ protected function getTotalColumns() */ public function getHeaderValue($column) { + // The sort handle should never have a visible column header label. + if ($column->label === 'backend::lang.list.sort_handle') { + return ''; + } + $value = Lang::get($column->label); /** diff --git a/modules/backend/widgets/lists/assets/js/winter.list.js b/modules/backend/widgets/lists/assets/js/winter.list.js index c2bb07baa0..fcd199d4a2 100644 --- a/modules/backend/widgets/lists/assets/js/winter.list.js +++ b/modules/backend/widgets/lists/assets/js/winter.list.js @@ -11,6 +11,7 @@ var $el = this.$el = $(element); this.options = options || {}; + this.sortOrders = [] var scrollClassContainer = options.scrollClassContainer !== undefined ? options.scrollClassContainer @@ -22,6 +23,17 @@ dragSelector: 'thead' }) + if (element.dataset.hasOwnProperty('sortable')) { + this.$el.find('.control-list-tbody').listSortable({ + handle: '.drag-handle' + }) + this.$el.on('dragged.list.sorted', $.proxy(this.processReorder, this)) + + this.$el.find('[data-record-sort-order]').each(function (index, el) { + this.sortOrders.push(el.dataset.recordSortOrder) + }.bind(this)) + } + this.update() } @@ -86,6 +98,21 @@ $checkbox.prop('checked', !$checkbox.is(':checked')).trigger('change') } + ListWidget.prototype.processReorder = function() { + var relation = this.$el.data('sortableRelation') + var handler = relation ? 'onReorderRelation' : 'onReorder' + + var recordIds = [] + this.$el.find('[data-record-id]').each(function (index, el) { + recordIds.push(el.dataset.recordId) + }.bind(this)) + + this.$el.request(handler, { + data: { sort_orders: this.sortOrders, record_ids: recordIds, _reorder_relation_name: relation }, + loading: $.wn.stripeLoadIndicator, + }) + } + // LIST WIDGET PLUGIN DEFINITION // ============================ @@ -143,4 +170,4 @@ $('[data-control="listwidget"]').listWidget(); }) -}(window.jQuery); \ No newline at end of file +}(window.jQuery); diff --git a/modules/backend/widgets/lists/partials/_list.php b/modules/backend/widgets/lists/partials/_list.php index 5b3e50e656..5347b00fa5 100644 --- a/modules/backend/widgets/lists/partials/_list.php +++ b/modules/backend/widgets/lists/partials/_list.php @@ -1,9 +1,18 @@ -
    + + + + + +
    + +> makePartial('list_head_row') ?> - + makePartial('list_body_rows') ?> diff --git a/modules/backend/widgets/lists/partials/_list_body_row.php b/modules/backend/widgets/lists/partials/_list_body_row.php index 8341f1c4ff..4a3f8f6642 100644 --- a/modules/backend/widgets/lists/partials/_list_body_row.php +++ b/modules/backend/widgets/lists/partials/_list_body_row.php @@ -3,7 +3,7 @@ $childRecords = $showTree ? $record->getChildren() : null; $treeLevelClass = $showTree ? 'list-tree-level-'.$treeLevel : ''; ?> - +> makePartial('list_body_checkbox', ['record' => $record]) ?> diff --git a/modules/backend/widgets/lists/partials/_list_sort_handle.htm b/modules/backend/widgets/lists/partials/_list_sort_handle.htm new file mode 100644 index 0000000000..fbd43e5056 --- /dev/null +++ b/modules/backend/widgets/lists/partials/_list_sort_handle.htm @@ -0,0 +1,7 @@ +
    + ☰ +
    diff --git a/modules/system/assets/ui/js/list.sortable.js b/modules/system/assets/ui/js/list.sortable.js index 2c92b92b03..3a746fac65 100644 --- a/modules/system/assets/ui/js/list.sortable.js +++ b/modules/system/assets/ui/js/list.sortable.js @@ -38,6 +38,7 @@ * * Events: * - dragged.list.sortable - triggered on a list element after it was moved + * - dragged.list.sorted - triggered on a list after the drag action finished */ +function ($) { "use strict"; @@ -86,23 +87,23 @@ ListSortable.prototype.registerListHandlers = function(list) { var $list = $(list) - $list.on('dragstart', '> li', this.proxy(this.onDragStart)) - $list.on('dragover', '> li', this.proxy(this.onDragOver)) - $list.on('dragenter', '> li', this.proxy(this.onDragEnter)) - $list.on('dragleave', '> li', this.proxy(this.onDragLeave)) - $list.on('drop', '> li', this.proxy(this.onDragDrop)) - $list.on('dragend', '> li', this.proxy(this.onDragEnd)) + $list.on('dragstart', '> *', this.proxy(this.onDragStart)) + $list.on('dragover', '> *', this.proxy(this.onDragOver)) + $list.on('dragenter', '> *', this.proxy(this.onDragEnter)) + $list.on('dragleave', '> *', this.proxy(this.onDragLeave)) + $list.on('drop', '> *', this.proxy(this.onDragDrop)) + $list.on('dragend', '> *', this.proxy(this.onDragEnd)) } ListSortable.prototype.unregisterListHandlers = function(list) { var $list = $(list) - $list.off('dragstart', '> li', this.proxy(this.onDragStart)) - $list.off('dragover', '> li', this.proxy(this.onDragOver)) - $list.off('dragenter', '> li', this.proxy(this.onDragEnter)) - $list.off('dragleave', '> li', this.proxy(this.onDragLeave)) - $list.off('drop', '> li', this.proxy(this.onDragDrop)) - $list.off('dragend', '> li', this.proxy(this.onDragEnd)) + $list.off('dragstart', '> *', this.proxy(this.onDragStart)) + $list.off('dragover', '> *', this.proxy(this.onDragOver)) + $list.off('dragenter', '> *', this.proxy(this.onDragEnter)) + $list.off('dragleave', '> *', this.proxy(this.onDragLeave)) + $list.off('drop', '> *', this.proxy(this.onDragDrop)) + $list.off('dragend', '> *', this.proxy(this.onDragEnd)) } ListSortable.prototype.unregisterHandlers = function() { @@ -168,7 +169,6 @@ } elementsIdCounter++ - var elementId = elementsIdCounter element.setAttribute('data-list-sortable-element-id', elementsIdCounter) @@ -293,7 +293,7 @@ var current = element while (current) { - if (current.tagName === 'LI' && current.hasAttribute('draggable') ) { + if (current.hasAttribute('draggable')) { return current } @@ -315,6 +315,11 @@ ev.originalEvent.dataTransfer.setData('listsortable/elementid', this.getElementSortableId(ev.target)) ev.originalEvent.dataTransfer.setData(this.listSortableId, this.listSortableId) + // Make sure the sort placeholder is never cut off by any hidden overflow. + var container = $(ev.target).closest('[data-sortable]') + this.originalOverflow = container.css('overflow') + container.css({overflow: 'visible'}) + // The mousemove handler is used to remove the placeholder // when the drag is canceled with Escape button. We can't use // the dragend for removing the placeholders because dragend @@ -395,6 +400,12 @@ ListSortable.prototype.onDragEnd = function(ev) { $(document).off('dragover', this.proxy(this.onDocumentDragOver)) + + var container = $(ev.target).closest('[data-sortable]') + if (container) { + container.trigger('dragged.list.sorted') + container.css({overflow: this.originalOverflow}) + } } ListSortable.prototype.onDocumentDragOver = function(ev) { @@ -461,4 +472,4 @@ $('[data-control=list-sortable]').listSortable() }) -}(window.jQuery); \ No newline at end of file +}(window.jQuery); diff --git a/modules/system/assets/ui/less/list.base.less b/modules/system/assets/ui/less/list.base.less index 54b2f3ef48..813574b760 100644 --- a/modules/system/assets/ui/less/list.base.less +++ b/modules/system/assets/ui/less/list.base.less @@ -56,6 +56,26 @@ th { .table { background-color: @body-bg; } + + .list-sortable-placeholder { + display: block; + position: relative; + height: 0; + &:before { + display: block; + position: absolute; + .icon(@chevron-right); + font-size: 15px; + color: #d35714; + left: 0; + top: -9px; + z-index: 2000; + } + } + + .drag-handle { + cursor: move; + } } diff --git a/modules/system/assets/ui/less/list.less b/modules/system/assets/ui/less/list.less index fa64bcfa48..b1dc6ecb82 100644 --- a/modules/system/assets/ui/less/list.less +++ b/modules/system/assets/ui/less/list.less @@ -88,7 +88,7 @@ table.table.data { } tbody { - tr:nth-child(even) { + tr:nth-of-type(even) { td, th { background-color: @color-list-accent; } } td, th { @@ -127,6 +127,12 @@ table.table.data { } } + // Prevent a border-top on the first row entry if the placeholder + // is placed right above it at the start of a table. + .list-sortable-placeholder:first-child + tr td { + border-top: 0; + } + tr:first-child { th, td { border-top-width: 0; @@ -559,7 +565,7 @@ table.table.data { } tbody { - tr:nth-child(even) { + tr:nth-of-type(even) { td, th { background-color: transparent; } } } diff --git a/modules/system/assets/ui/storm-min.js b/modules/system/assets/ui/storm-min.js index 6b6e09b1e1..a2549fbfb2 100644 --- a/modules/system/assets/ui/storm-min.js +++ b/modules/system/assets/ui/storm-min.js @@ -4684,19 +4684,19 @@ ListSortable.prototype.addList=function(list){this.lists.push(list) this.registerListHandlers(list) if(this.lists.length==1){$(list).one('dispose-control',this.proxy(this.dispose))}} ListSortable.prototype.registerListHandlers=function(list){var $list=$(list) -$list.on('dragstart','> li',this.proxy(this.onDragStart)) -$list.on('dragover','> li',this.proxy(this.onDragOver)) -$list.on('dragenter','> li',this.proxy(this.onDragEnter)) -$list.on('dragleave','> li',this.proxy(this.onDragLeave)) -$list.on('drop','> li',this.proxy(this.onDragDrop)) -$list.on('dragend','> li',this.proxy(this.onDragEnd))} +$list.on('dragstart','> *',this.proxy(this.onDragStart)) +$list.on('dragover','> *',this.proxy(this.onDragOver)) +$list.on('dragenter','> *',this.proxy(this.onDragEnter)) +$list.on('dragleave','> *',this.proxy(this.onDragLeave)) +$list.on('drop','> *',this.proxy(this.onDragDrop)) +$list.on('dragend','> *',this.proxy(this.onDragEnd))} ListSortable.prototype.unregisterListHandlers=function(list){var $list=$(list) -$list.off('dragstart','> li',this.proxy(this.onDragStart)) -$list.off('dragover','> li',this.proxy(this.onDragOver)) -$list.off('dragenter','> li',this.proxy(this.onDragEnter)) -$list.off('dragleave','> li',this.proxy(this.onDragLeave)) -$list.off('drop','> li',this.proxy(this.onDragDrop)) -$list.off('dragend','> li',this.proxy(this.onDragEnd))} +$list.off('dragstart','> *',this.proxy(this.onDragStart)) +$list.off('dragover','> *',this.proxy(this.onDragOver)) +$list.off('dragenter','> *',this.proxy(this.onDragEnter)) +$list.off('dragleave','> *',this.proxy(this.onDragLeave)) +$list.off('drop','> *',this.proxy(this.onDragDrop)) +$list.off('dragend','> *',this.proxy(this.onDragEnd))} ListSortable.prototype.unregisterHandlers=function(){$(document).off('dragover',this.proxy(this.onDocumentDragOver)) $(document).off('mousemove',this.proxy(this.onDocumentMouseMove)) $(this.lists[0]).off('dispose-control',this.proxy(this.dispose))} @@ -4712,7 +4712,6 @@ ListSortable.prototype.elementBelongsToManagedList=function(element){for(var i=t ListSortable.prototype.isDragStartAllowed=function(element){return true} ListSortable.prototype.elementIsPlaceholder=function(element){return element.getAttribute('class')==='list-sortable-placeholder'} ListSortable.prototype.getElementSortableId=function(element){if(element.hasAttribute('data-list-sortable-element-id')){return element.getAttribute('data-list-sortable-element-id')}elementsIdCounter++ -var elementId=elementsIdCounter element.setAttribute('data-list-sortable-element-id',elementsIdCounter) return elementsIdCounter} ListSortable.prototype.dataTransferContains=function(ev,element){if(ev.dataTransfer.types.indexOf!==undefined){return ev.dataTransfer.types.indexOf(element)>=0}return ev.dataTransfer.types.contains(element)} @@ -4741,10 +4740,13 @@ return true}return false} ListSortable.prototype.mouseOutsideLists=function(ev){var mousePosition=$.wn.foundation.event.pageCoordinates(ev) for(var i=this.lists.length-1;i>=0;i--){if($.wn.foundation.element.elementContainsPoint(this.lists[i],mousePosition)){return false}}return true} ListSortable.prototype.getClosestDraggableParent=function(element){var current=element -while(current){if(current.tagName==='LI'&¤t.hasAttribute('draggable')){return current}current=current.parentNode}return null} +while(current){if(current.hasAttribute('draggable')){return current}current=current.parentNode}return null} ListSortable.prototype.onDragStart=function(ev){if(!this.isDragStartAllowed(ev.target)){return}ev.originalEvent.dataTransfer.effectAllowed='move' ev.originalEvent.dataTransfer.setData('listsortable/elementid',this.getElementSortableId(ev.target)) ev.originalEvent.dataTransfer.setData(this.listSortableId,this.listSortableId) +var container=$(ev.target).closest('[data-sortable]') +this.originalOverflow=container.css('overflow') +container.css({overflow:'visible'}) $(document).on('mousemove',this.proxy(this.onDocumentMouseMove)) $(document).on('dragover',this.proxy(this.onDocumentDragOver))} ListSortable.prototype.onDragOver=function(ev){if(!this.isSourceManagedList(ev.originalEvent)){return}var draggable=this.getClosestDraggableParent(ev.target) @@ -4760,7 +4762,10 @@ ev.preventDefault()} ListSortable.prototype.onDragDrop=function(ev){if(!this.isSourceManagedList(ev.originalEvent)){return}var draggable=this.getClosestDraggableParent(ev.target) if(!draggable){return}this.moveElement(draggable,ev.originalEvent) this.removePlaceholders()} -ListSortable.prototype.onDragEnd=function(ev){$(document).off('dragover',this.proxy(this.onDocumentDragOver))} +ListSortable.prototype.onDragEnd=function(ev){$(document).off('dragover',this.proxy(this.onDocumentDragOver)) +var container=$(ev.target).closest('[data-sortable]') +if(container){container.trigger('dragged.list.sorted') +container.css({overflow:this.originalOverflow})}} ListSortable.prototype.onDocumentDragOver=function(ev){if(!this.isSourceManagedList(ev.originalEvent)){return}if(this.mouseOutsideLists(ev.originalEvent)){this.removePlaceholders() return}} ListSortable.prototype.onDocumentMouseMove=function(ev){$(document).off('mousemove',this.proxy(this.onDocumentMouseMove)) diff --git a/modules/system/assets/ui/storm.css b/modules/system/assets/ui/storm.css index b42de87255..3e5f252a71 100644 --- a/modules/system/assets/ui/storm.css +++ b/modules/system/assets/ui/storm.css @@ -3011,6 +3011,9 @@ th{text-align:left} .table>thead:first-child>tr:first-child>td{border-top:0} .table>tbody + tbody{border-top:2px solid #ddd} .table .table{background-color:#f9f9f9} +.table .list-sortable-placeholder{display:block;position:relative;height:0} +.table .list-sortable-placeholder:before{display:block;position:absolute;font-family:"Font Awesome 6 Free";font-weight:900;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-style:normal;font-variant:normal;text-rendering:auto;content:"\f054";font-size:15px;color:#d35714;left:0;top:-9px;z-index:2000} +.table .drag-handle{cursor:move} .table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, @@ -3152,8 +3155,8 @@ table.table.data thead th.active>a:after{color:#c63e26;opacity:1 !important;filt table.table.data thead tr th:first-child{padding-left:10px} table.table.data thead tr th:last-child a{padding-right:25px} table.table.data thead .list-checkbox .custom-checkbox{top:-16px} -table.table.data tbody tr:nth-child(even) td, -table.table.data tbody tr:nth-child(even) th{background-color:#ecf0f1} +table.table.data tbody tr:nth-of-type(even) td, +table.table.data tbody tr:nth-of-type(even) th{background-color:#ecf0f1} table.table.data tbody td, table.table.data tbody th{padding:12px 15px;color:#666;border-top:2px solid white} table.table.data tbody td a:not(.btn), @@ -3166,6 +3169,7 @@ table.table.data tbody td div.progress div.bar, table.table.data tbody th div.progress div.bar{position:absolute;left:-15px;top:-11px;bottom:-11px;background:#0181b9;opacity:0.3;filter:alpha(opacity=30)} table.table.data tbody td div.progress a, table.table.data tbody th div.progress a{position:relative} +table.table.data tbody .list-sortable-placeholder:first-child + tr td{border-top:0} table.table.data tbody tr:first-child th, table.table.data tbody tr:first-child td{border-top-width:0} table.table.data tbody tr:last-child th, @@ -3311,8 +3315,8 @@ table.table.data tr.list-tree-level-10 td.list-cell-index-1{padding-left:115px} .report-widget .table-container{margin:-15px} .report-widget .table-container table.table.data{margin-bottom:0} .report-widget .table-container table.table.data thead tr th{border-top:none !important} -.report-widget .table-container table.table.data tbody tr:nth-child(even) td, -.report-widget .table-container table.table.data tbody tr:nth-child(even) th{background-color:transparent} +.report-widget .table-container table.table.data tbody tr:nth-of-type(even) td, +.report-widget .table-container table.table.data tbody tr:nth-of-type(even) th{background-color:transparent} .list-scrollable-container{touch-action:auto;position:relative} .list-scrollable-container:after, .list-scrollable-container:before{display:none;position:absolute;top:50%;margin-top:-7px;height:9px;font-size:10px;color:#666}