mirror of
https://github.com/symbiote/silverstripe-gridfieldextensions.git
synced 2024-10-22 17:05:39 +02:00
Implemented a drag-and-drop orderable rows component.
This commit is contained in:
parent
a8c586dfc0
commit
e41f2fdacf
@ -4,6 +4,7 @@ SilverStripe Grid Field Extensions Module
|
|||||||
This module provides a number of useful grid field components:
|
This module provides a number of useful grid field components:
|
||||||
|
|
||||||
* `GridFieldAddExistingSearchButton` - a more advanced search form for adding items.
|
* `GridFieldAddExistingSearchButton` - a more advanced search form for adding items.
|
||||||
|
* `GridFieldOrderableRows` - drag and drop re-ordering of rows.
|
||||||
|
|
||||||
Maintainer Contacts
|
Maintainer Contacts
|
||||||
-------------------
|
-------------------
|
||||||
|
289
code/GridFieldOrderableRows.php
Normal file
289
code/GridFieldOrderableRows.php
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Allows grid field rows to be re-ordered via drag and drop. Both normal data
|
||||||
|
* lists and many many lists can be ordered.
|
||||||
|
*
|
||||||
|
* If the grid field has not been sorted, this component will sort the data by
|
||||||
|
* the sort field.
|
||||||
|
*/
|
||||||
|
class GridFieldOrderableRows extends RequestHandler implements
|
||||||
|
GridField_ColumnProvider,
|
||||||
|
GridField_DataManipulator,
|
||||||
|
GridField_HTMLProvider,
|
||||||
|
GridField_URLHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The database field which specifies the sort, defaults to "Sort".
|
||||||
|
*
|
||||||
|
* @see setSortField()
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $sortField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $sortField
|
||||||
|
*/
|
||||||
|
public function __construct($sortField = 'Sort') {
|
||||||
|
$this->sortField = $sortField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getSortField() {
|
||||||
|
return $this->sortField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the field used to specify the sort.
|
||||||
|
*
|
||||||
|
* @param string $sortField
|
||||||
|
*/
|
||||||
|
public function setSortField($field) {
|
||||||
|
$this->sortField = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the table which contains the sort field.
|
||||||
|
*
|
||||||
|
* @param DataList $list
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getSortTable(DataList $list) {
|
||||||
|
$field = $this->getSortField();
|
||||||
|
|
||||||
|
if($list instanceof ManyManyList) {
|
||||||
|
// @todo These should be publically accesible.
|
||||||
|
$reflector = new ReflectionObject($list);
|
||||||
|
|
||||||
|
$extra = $reflector->getProperty('extraFields');
|
||||||
|
$extra->setAccessible(true);
|
||||||
|
|
||||||
|
$table = $reflector->getProperty('joinTable');
|
||||||
|
$table->setAccessible(true);
|
||||||
|
|
||||||
|
if(array_key_exists($field, $extra->getValue($list))) {
|
||||||
|
return $table->getValue($list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$classes = ClassInfo::dataClassesFor($list->dataClass());
|
||||||
|
|
||||||
|
foreach($classes as $class) {
|
||||||
|
if(singleton($class)->hasOwnTableDatabaseField($field)) {
|
||||||
|
return $class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Couldn't find the sort field '$field'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getURLHandlers($grid) {
|
||||||
|
return array(
|
||||||
|
'POST reorder' => 'handleReorder',
|
||||||
|
'POST movetopage' => 'handleMoveToPage'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHTMLFragments($field) {
|
||||||
|
Requirements::css('gridfieldextensions/css/GridFieldExtensions.css');
|
||||||
|
Requirements::javascript('gridfieldextensions/javascript/GridFieldExtensions.js');
|
||||||
|
|
||||||
|
$field->addExtraClass('ss-gridfield-orderable');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function augmentColumns($grid, &$cols) {
|
||||||
|
if(!in_array('Reorder', $cols) && $grid->getState()->GridFieldOrderableRows->enabled) {
|
||||||
|
array_unshift($cols, 'Reorder');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getColumnsHandled($grid) {
|
||||||
|
return array('Reorder');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getColumnContent($grid, $record, $col) {
|
||||||
|
return ViewableData::create()->renderWith('GridFieldOrderableRowsDragHandle');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getColumnAttributes($grid, $record, $col) {
|
||||||
|
return array('class' => 'col-reorder');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getColumnMetadata($grid, $col) {
|
||||||
|
return array('title' => '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getManipulatedData(GridField $grid, SS_List $list) {
|
||||||
|
$state = $grid->getState();
|
||||||
|
$sorted = (bool) ((string) $state->GridFieldSortableHeader->SortColumn);
|
||||||
|
|
||||||
|
// If the data has not been sorted by the user, then sort it by the
|
||||||
|
// sort column, otherwise disable reordering.
|
||||||
|
$state->GridFieldOrderableRows->enabled = !$sorted;
|
||||||
|
|
||||||
|
if(!$sorted) {
|
||||||
|
return $list->sort($this->getSortField());
|
||||||
|
} else {
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles requests to reorder a set of IDs in a specific order.
|
||||||
|
*/
|
||||||
|
public function handleReorder($grid, $request) {
|
||||||
|
if(!singleton($grid->getModelClass())->canEdit()) {
|
||||||
|
$this->httpError(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = $request->postVar('order');
|
||||||
|
$list = $grid->getList();
|
||||||
|
$field = $this->getSortField();
|
||||||
|
|
||||||
|
if(!is_array($ids)) {
|
||||||
|
$this->httpError(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $list->byIDs($ids)->sort($field);
|
||||||
|
|
||||||
|
// Ensure that each provided ID corresponded to an actual object.
|
||||||
|
if(count($items) != count($ids)) {
|
||||||
|
$this->httpError(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate each object we are sorting with a sort value.
|
||||||
|
$this->populateSortValues($items);
|
||||||
|
|
||||||
|
// Generate the current sort values.
|
||||||
|
$current = $items->map('ID', $field)->toArray();
|
||||||
|
|
||||||
|
// Perform the actual re-ordering.
|
||||||
|
$this->reorderItems($list, $current, $ids);
|
||||||
|
|
||||||
|
return $grid->FieldHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles requests to move an item to the previous or next page.
|
||||||
|
*/
|
||||||
|
public function handleMoveToPage(GridField $grid, $request) {
|
||||||
|
if(!$paginator = $grid->getConfig()->getComponentByType('GridFieldPaginator')) {
|
||||||
|
$this->httpError(404, 'Paginator component not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$move = $request->postVar('move');
|
||||||
|
$field = $this->getSortField();
|
||||||
|
|
||||||
|
$list = $grid->getList();
|
||||||
|
$manip = $grid->getManipulatedList();
|
||||||
|
|
||||||
|
$existing = $manip->map('ID', $field)->toArray();
|
||||||
|
$values = $existing;
|
||||||
|
$order = array();
|
||||||
|
|
||||||
|
$id = isset($move['id']) ? (int) $move['id'] : null;
|
||||||
|
$to = isset($move['page']) ? $move['page'] : null;
|
||||||
|
|
||||||
|
if(!isset($values[$id])) {
|
||||||
|
$this->httpError(400, 'Invalid item ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->populateSortValues($list);
|
||||||
|
|
||||||
|
$page = ((int) $grid->getState()->GridFieldPaginator->currentPage) ?: 1;
|
||||||
|
$per = $paginator->getItemsPerPage();
|
||||||
|
|
||||||
|
if($to == 'prev') {
|
||||||
|
$swap = $list->limit(1, ($page - 1) * $per - 1)->first();
|
||||||
|
$values[$swap->ID] = $swap->$field;
|
||||||
|
|
||||||
|
$order[] = $id;
|
||||||
|
$order[] = $swap->ID;
|
||||||
|
|
||||||
|
foreach($existing as $_id => $sort) {
|
||||||
|
if($id != $_id) $order[] = $_id;
|
||||||
|
}
|
||||||
|
} elseif($to == 'next') {
|
||||||
|
$swap = $list->limit(1, $page * $per)->first();
|
||||||
|
$values[$swap->ID] = $swap->$field;
|
||||||
|
|
||||||
|
foreach($existing as $_id => $sort) {
|
||||||
|
if($id != $_id) $order[] = $_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order[] = $swap->ID;
|
||||||
|
$order[] = $id;
|
||||||
|
} else {
|
||||||
|
$this->httpError(400, 'Invalid page target');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reorderItems($list, $values, $order);
|
||||||
|
|
||||||
|
return $grid->FieldHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function reorderItems($list, array $values, array $order) {
|
||||||
|
// Get a list of sort values that can be used.
|
||||||
|
$pool = array_values($values);
|
||||||
|
sort($pool);
|
||||||
|
|
||||||
|
// Loop through each item, and update the sort values which do not
|
||||||
|
// match to order the objects.
|
||||||
|
foreach(array_values($order) as $pos => $id) {
|
||||||
|
if($values[$id] != $pool[$pos]) {
|
||||||
|
DB::query(sprintf(
|
||||||
|
'UPDATE "%s" SET "%s" = %d WHERE %s',
|
||||||
|
$this->getSortTable($list),
|
||||||
|
$this->getSortField(),
|
||||||
|
$pool[$pos],
|
||||||
|
$this->getSortTableClauseForIds($list, $id)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function populateSortValues(DataList $list) {
|
||||||
|
$field = $this->getSortField();
|
||||||
|
$table = $this->getSortTable($list);
|
||||||
|
$clause = $this->getSortTableClauseForIds($list, 0);
|
||||||
|
|
||||||
|
foreach($list->where($clause)->column('ID') as $id) {
|
||||||
|
$max = DB::query(sprintf('SELECT MAX("%s") + 1 FROM "%s"', $field, $table));
|
||||||
|
$max = $max->value();
|
||||||
|
|
||||||
|
DB::query(sprintf(
|
||||||
|
'UPDATE "%s" SET "%s" = %d WHERE %s',
|
||||||
|
$table,
|
||||||
|
$field,
|
||||||
|
$max,
|
||||||
|
$this->getSortTableClauseForIds($list, $id)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSortTableClauseForIds(DataList $list, $ids) {
|
||||||
|
if(is_array($ids)) {
|
||||||
|
$value = 'IN (' . implode(', ', array_map('intval', $ids)) . ')';
|
||||||
|
} else {
|
||||||
|
$value = '= ' . (int) $ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($list instanceof ManyManyList) {
|
||||||
|
$reflector = new ReflectionObject($list);
|
||||||
|
|
||||||
|
$extra = $reflector->getProperty('extraFields');
|
||||||
|
$extra->setAccessible(true);
|
||||||
|
|
||||||
|
$key = $reflector->getProperty('localKey');
|
||||||
|
$key->setAccessible(true);
|
||||||
|
|
||||||
|
if(array_key_exists($this->getSortField(), $extra->getValue($list))) {
|
||||||
|
return sprintf('"%s" %s', $key->getValue($list), $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\"ID\" $value";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -11,6 +11,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"silverstripe/framework": "3.*"
|
"silverstripe/framework": ">=3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,3 +53,50 @@
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GridFieldOrderableRows
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ss-gridfield-orderable thead tr th.col-Reorder span {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-gridfield-orderable .col-reorder {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-gridfield-orderable .col-reorder .handle {
|
||||||
|
cursor: move;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-gridfield-orderable tbody tr:hover .col-reorder .handle {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-gridfield-orderhelper {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, .1);
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, .4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-gridfield-orderable tfoot .ui-droppable {
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-gridfield-orderable tfoot .ui-droppable-active {
|
||||||
|
background-color: #D4CF90 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-gridfield-orderable tfoot .ss-gridfield-previouspage {
|
||||||
|
background-position: -16px 9px !important;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ss-gridfield-orderable tfoot .ss-gridfield-nextpage {
|
||||||
|
background-position: -40px 9px !important;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
@ -71,5 +71,87 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GridFieldOrderableRows
|
||||||
|
*/
|
||||||
|
|
||||||
|
$(".ss-gridfield-orderable tbody").entwine({
|
||||||
|
onadd: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var helper = function(e, row) {
|
||||||
|
return row.clone()
|
||||||
|
.addClass("ss-gridfield-orderhelper")
|
||||||
|
.width("auto")
|
||||||
|
.find(".col-buttons")
|
||||||
|
.remove()
|
||||||
|
.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
var update = function() {
|
||||||
|
var grid = self.getGridField();
|
||||||
|
|
||||||
|
var data = grid.getItems().map(function() {
|
||||||
|
return { name: "order[]", value: $(this).data("id") };
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.reload({
|
||||||
|
url: grid.data("url") + "/reorder",
|
||||||
|
data: data.get()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sortable({
|
||||||
|
handle: ".handle",
|
||||||
|
helper: helper,
|
||||||
|
opacity: .7,
|
||||||
|
update: update
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onremove: function() {
|
||||||
|
this.sortable("destroy");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".ss-gridfield-orderable .ss-gridfield-previouspage, .ss-gridfield-orderable .ss-gridfield-nextpage").entwine({
|
||||||
|
onadd: function() {
|
||||||
|
var grid = this.getGridField();
|
||||||
|
|
||||||
|
if(this.is(":disabled")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var drop = function(e, ui) {
|
||||||
|
var page;
|
||||||
|
|
||||||
|
if($(this).hasClass("ss-gridfield-previouspage")) {
|
||||||
|
page = "prev";
|
||||||
|
} else {
|
||||||
|
page = "next";
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.find("tbody").sortable("cancel");
|
||||||
|
grid.reload({
|
||||||
|
url: grid.data("url") + "/movetopage",
|
||||||
|
data: [
|
||||||
|
{ name: "move[id]", value: ui.draggable.data("id") },
|
||||||
|
{ name: "move[page]", value: page }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.droppable({
|
||||||
|
accept: ".ss-gridfield-item",
|
||||||
|
activeClass: "ui-droppable-active ui-state-highlight",
|
||||||
|
disabled: this.prop("disabled"),
|
||||||
|
drop: drop,
|
||||||
|
tolerance: "pointer"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onremove: function() {
|
||||||
|
if(this.hasClass("ui-droppable")) this.droppable("destroy");
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
1
templates/GridFieldOrderableRowsDragHandle.ss
Normal file
1
templates/GridFieldOrderableRowsDragHandle.ss
Normal file
@ -0,0 +1 @@
|
|||||||
|
<span class="handle ui-icon ui-icon-grip-dotted-vertical"></span>
|
Loading…
Reference in New Issue
Block a user