Implemented a drag-and-drop orderable rows component.

This commit is contained in:
Andrew Short 2012-12-30 16:12:06 +11:00
parent a8c586dfc0
commit e41f2fdacf
6 changed files with 421 additions and 1 deletions

View File

@ -4,6 +4,7 @@ SilverStripe Grid Field Extensions Module
This module provides a number of useful grid field components:
* `GridFieldAddExistingSearchButton` - a more advanced search form for adding items.
* `GridFieldOrderableRows` - drag and drop re-ordering of rows.
Maintainer Contacts
-------------------

View 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";
}
}

View File

@ -11,6 +11,6 @@
}
],
"require": {
"silverstripe/framework": "3.*"
"silverstripe/framework": ">=3.1"
}
}

View File

@ -53,3 +53,50 @@
margin-top: 12px;
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;
}

View File

@ -71,5 +71,87 @@
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);

View File

@ -0,0 +1 @@
<span class="handle ui-icon ui-icon-grip-dotted-vertical"></span>