Commit 6d4862c2 authored by Gábor Hojtsy's avatar Gábor Hojtsy

#181126 by quicksketch et al: drag and drop support for menus

parent 72a59b0b
......@@ -1954,19 +1954,19 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
* In a situation where a single weight column is being sorted in the table, the
* classes could be added like this (in the theme function):
* @code
* $form['my_elements'][$delta]['weight']['attributes']['class'] = "my-elements-weight";
* $form['my_elements'][$delta]['weight']['#attributes']['class'] = "my-elements-weight";
* @endcode
*
* Calling drupal_add_tabledrag() would then be written as such:
* @code
* drupal_add_tabledrag('my-module-table', 'sort', 'sibling', 'my-elements-weight');
* drupal_add_tabledrag('my-module-table', 'order', 'sibling', 'my-elements-weight');
* @endcode
*
* In a more complex case where there are several groups in one column (such as
* the block regions on the admin/build/block page), a separate subgroup class
* must also be added to differentiate the groups.
* @code
* $form['my_elements'][$region][$delta]['weight']['attributes']['class'] = "my-elements-weight my-elements-weight-". $region;
* $form['my_elements'][$region][$delta]['weight']['#attributes']['class'] = "my-elements-weight my-elements-weight-". $region;
* @endcode
*
* $group is still 'my-element-weight', and the additional $subgroup variable
......@@ -1975,7 +1975,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
*
* @code
* foreach ($regions as $region) {
* drupal_add_tabledrag('my-module-table', 'sort', 'sibling', 'my-elements-weight', 'my-elements-weight-'. $region);
* drupal_add_tabledrag('my-module-table', 'order', 'sibling', 'my-elements-weight', 'my-elements-weight-'. $region);
* }
* @endcode
*
......@@ -1996,13 +1996,15 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
* String containing the target table's id attribute. If the table does not
* have an id, one will need to be set, such as <table id="my-module-table">.
* @param $action
* String describing the action to be done on the form item. Either 'match' or
* 'sort'. Match is typically used for parent relationships, sort is typically
* used to set weights on other form elements with the same group.
* String describing the action to be done on the form item. Either 'match'
* 'depth', or 'order'. Match is typically used for parent relationships.
* Order is typically used to set weights on other form elements with the same
* group. Depth updates the target element with the current indentation.
* @param $relationship
* String describing where the $action variable should be performed. Either
* 'parent' or 'sibling'. Parent will only look for fields up the tree.
* Sibling will look for fields in the same group in rows above and below it.
* 'parent', 'sibling', or 'self'. Parent will only look for fields up the
* tree. Sibling will look for fields in the same group in rows above and
* below it. Self affects the dragged row itself.
* @param $group
* A class name applied on all related form elements for this action.
* @param $subgroup
......@@ -2929,6 +2931,9 @@ function drupal_common_themes() {
'progress_bar' => array(
'arguments' => array('percent' => NULL, 'message' => NULL),
),
'indentation' => array(
'arguments' => array('size' => 1),
),
// from pager.inc
'pager' => array(
'arguments' => array('tags' => array(), 'limit' => 10, 'element' => 0, 'parameters' => array()),
......
......@@ -1671,6 +1671,22 @@ function theme_progress_bar($percent, $message) {
return $output;
}
/**
* Create a standard indentation div. Used for drag and drop tables.
*
* @param $size
* Optional. The number of indentations to create.
* @return
* A string containing indentations.
*/
function theme_indentation($size = 1) {
$output = '';
for ($n = 0; $n < $size; $n++) {
$output .= '<div class="indentation">&nbsp;</div>';
}
return $output;
}
/**
* @} End of "defgroup themeable".
*/
......
......@@ -53,15 +53,26 @@ Drupal.tableDrag = function(table, tableSettings) {
this.scrollY = 0;
this.windowHeight = 0;
// Check if this table contains indentations.
var indents = $('div.indentation', table);
this.indentEnabled = indents.size() > 0 ? true : false;
// Check this table's settings to see if there are parent relationships in
// this table. For efficiency, large sections of code can be skipped if we
// don't need to track horizontal movement and indentations.
this.indentEnabled = false;
for (group in tableSettings) {
for (n in tableSettings[group]) {
if (tableSettings[group][n]['relationship'] == 'parent') {
this.indentEnabled = true;
}
}
}
if (this.indentEnabled) {
var indentSize = indents.css('width');
this.oldX = 0;
this.indentCount = 1; // Total width of indents, set in makeDraggable.
this.indentIncrement = indentSize.replace(/[0-9\.]*/, '');
this.indentAmount = parseInt(indentSize);
// Find the width of indentations to measure mouse movements against.
// Because the table doesn't need to start with any indentations, we
// manually create an empty div, check it's width, then remove.
var indent = $(Drupal.theme('tableDragIndentation')).appendTo('body');
this.indentAmount = parseInt(indent.css('width'));
indent.remove();
}
// Make each applicable row draggable.
......@@ -253,7 +264,7 @@ Drupal.tableDrag.prototype.makeDraggable = function(item) {
self.safeBlur = false; // Do not allow the onBlur cleanup.
self.rowObject.direction = 'up';
keyChange = true;
if (self.rowObject.isValidSwap(previousRow, 'up', 0)) {
if (self.rowObject.isValidSwap(previousRow, 0)) {
self.rowObject.swap('before', previousRow);
window.scrollBy(0, -parseInt(item.offsetHeight));
}
......@@ -280,7 +291,7 @@ Drupal.tableDrag.prototype.makeDraggable = function(item) {
self.safeBlur = false; // Do not allow the onBlur cleanup.
self.rowObject.direction = 'down';
keyChange = true;
if (self.rowObject.isValidSwap(nextRow, 'down', 0)) {
if (self.rowObject.isValidSwap(nextRow, 0)) {
self.rowObject.swap('after', nextRow);
}
else {
......@@ -549,8 +560,12 @@ Drupal.tableDrag.prototype.updateFields = function(changedRow) {
// the source rows for each seperately.
var rowSettings = this.rowSettings(group, changedRow);
// Set the row as it's own target.
if (rowSettings.relationship == 'self') {
var sourceRow = changedRow;
}
// Siblings are easy, check previous and next rows.
if (rowSettings.relationship == 'sibling') {
else if (rowSettings.relationship == 'sibling') {
var previousRow = $(changedRow).prev('tr').get(0);
var nextRow = $(changedRow).next('tr').get(0);
var sourceRow = changedRow;
......@@ -586,11 +601,16 @@ Drupal.tableDrag.prototype.updateFields = function(changedRow) {
if (previousRow.length) {
sourceRow = previousRow[0];
}
// Otherwise we went all the way to the top of the table.
// Assume that the first item has no indentions.
// Otherwise we went all the way to the left of the table without finding
// a parent, meaning this item has been placed at the root level.
else {
// Basically copy first item's value.
sourceRow = $('tr.draggable:first')[0];
// Use the first row in the table as source, because it's garanteed to
// be at the root level. Find the first item, then compare this row
// against it as a sibling.
sourceRow = $('tr.draggable:first').get(0);
if (sourceRow == this.rowObject.element) {
sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0);
}
var useSibling = true;
}
}
......@@ -615,6 +635,10 @@ Drupal.tableDrag.prototype.updateFields = function(changedRow) {
var sourceClass = '.' + rowSettings.source;
var sourceElement = $(sourceClass, sourceRow).get(0);
switch (rowSettings.action) {
case 'depth':
// Get the depth of the target row.
targetElement.value = $('.indentation', $(sourceElement).parents('tr:first')).size();
break;
case 'match':
// Update the value.
targetElement.value = sourceElement.value;
......@@ -634,7 +658,7 @@ Drupal.tableDrag.prototype.updateFields = function(changedRow) {
}
else {
// Assume a numeric input field.
var weight = 0;
var weight = parseInt($(targetClass, siblings[0]).val()) || 0;
$(targetClass, siblings).each(function() {
this.value = weight;
weight++;
......@@ -832,7 +856,7 @@ Drupal.tableDrag.prototype.row.prototype.isValidSwap = function(row, indentDiff)
if (this.table.tBodies[0].rows[0] == row) {
// Do not let the first row contain indentations
// or let an un-draggable first row have anything put before it.
if (this.indents > 0 || $(row).is(':not(.draggable)')) {
if ((this.indents + indentDiff) > 0 || $(row).is(':not(.draggable)')) {
return false;
}
}
......
......@@ -28,7 +28,7 @@ function menu_overview_page() {
*/
function menu_overview_form(&$form_state, $menu) {
global $menu_admin;
$sql ="
$sql = "
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, ml.*
FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
WHERE ml.menu_name = '%s'
......@@ -66,7 +66,7 @@ function _menu_overview_tree_form($tree) {
$item = $data['link'];
// Don't show callbacks; these have $item['hidden'] < 0.
if ($item && $item['hidden'] >= 0) {
$mlid = $item['mlid'];
$mlid = 'mlid:'. $item['mlid'];
$form[$mlid]['#item'] = $item;
$form[$mlid]['#attributes'] = $item['hidden'] ? array('class' => 'menu-disabled') : array('class' => 'menu-enabled');
$form[$mlid]['title']['#value'] = l($item['title'], $item['href'], $item['options']) . ($item['hidden'] ? ' ('. t('disabled') .')' : '');
......@@ -78,6 +78,19 @@ function _menu_overview_tree_form($tree) {
'#type' => 'checkbox',
'#default_value' => $item['has_children'] && $item['expanded'],
);
$form[$mlid]['weight'] = array(
'#type' => 'weight',
'#default_value' => isset($form_state[$mlid]['weight']) ? $form_state[$mlid]['weight'] : $item['weight'],
);
$form[$mlid]['mlid'] = array(
'#type' => 'hidden',
'#value' => $item['mlid'],
);
$form[$mlid]['plid'] = array(
'#type' => 'textfield',
'#default_value' => isset($form_state[$mlid]['plid']) ? $form_state[$mlid]['plid'] : $item['plid'],
'#size' => 6,
);
// Build a list of operations.
$operations = array();
$operations['edit'] = l(t('edit'), 'admin/build/menu/item/'. $item['mlid'] .'/edit');
......@@ -103,27 +116,65 @@ function _menu_overview_tree_form($tree) {
return $form;
}
function menu_overview_form_submit($form) {
/**
* Submit handler for the menu overview form.
*
* This function takes great care in saving parent items first, then items
* underneath them. Saving items in the incorrect order can break the menu tree.
*
* @see menu_overview_form()
*/
function menu_overview_form_submit($form, &$form_state) {
// When dealing with saving menu items, the order in which these items are
// saved is critical. If a changed child item is saved before its parent,
// the child item could be saved with an invalid path past its immediate
// parent. To prevent this, save items in the form in the same order they
// are sent by $_POST, ensuring parents are saved first, then their children.
// See http://drupal.org/node/181126#comment-632270
$order = array_flip(array_keys($form['#post'])); // Get the $_POST order.
$form = array_merge($order, $form); // Update our original form with the new order.
$updated_items = array();
$fields = array('expanded', 'weight', 'plid');
foreach (element_children($form) as $mlid) {
if (isset($form[$mlid]['hidden'])) {
if (isset($form[$mlid]['#item'])) {
$element = $form[$mlid];
// Update any fields that have changed in this menu item.
foreach ($fields as $field) {
if ($element[$field]['#value'] != $element[$field]['#default_value']) {
$element['#item'][$field] = $element[$field]['#value'];
$updated_items[$mlid] = $element['#item'];
}
}
// Hidden is a special case, the value needs to be reversed.
if ($element['hidden']['#value'] != $element['hidden']['#default_value']) {
$element['#item']['hidden'] = !$element['hidden']['#value'];
menu_link_save($element['#item']);
}
if ($element['expanded']['#value'] != $element['expanded']['#default_value']) {
$element['#item']['expanded'] = $element['expanded']['#value'];
menu_link_save($element['#item']);
$updated_items[$mlid] = $element['#item'];
}
}
}
// Save all our changed items to the database.
foreach ($updated_items as $item) {
menu_link_save($item);
}
}
/**
* Theme the menu overview form into a table.
*/
function theme_menu_overview_form($form) {
$header = array(t('Enabled'), t('Expanded'), t('Menu item'), array('data' => t('Operations'), 'colspan' => '3'));
drupal_add_tabledrag('menu-overview', 'match', 'parent', 'menu-plid', 'menu-plid', 'menu-mlid');
drupal_add_tabledrag('menu-overview', 'order', 'sibling', 'menu-weight');
$header = array(
t('Menu item'),
array('data' => t('Expanded'), 'class' => 'checkbox'),
array('data' => t('Enabled'), 'class' => 'checkbox'),
t('Weight'),
array('data' => t('Operations'), 'colspan' => '3'),
);
$rows = array();
foreach (element_children($form) as $mlid) {
if (isset($form[$mlid]['hidden'])) {
......@@ -137,15 +188,23 @@ function theme_menu_overview_form($form) {
$operations[] = '';
}
// Add special classes to be used for tabledrag.js.
$element['plid']['#attributes']['class'] = 'menu-plid';
$element['mlid']['#attributes']['class'] = 'menu-mlid';
$element['weight']['#attributes']['class'] = 'menu-weight';
// Change the parent field to a hidden. This allows any value but hides the field.
$element['plid']['#type'] = 'hidden';
$row = array();
$row[] = array('data' => drupal_render($element['hidden']), 'align' => 'center');
$row[] = array('data' => drupal_render($element['expanded']), 'align' => 'center');
$depth = $element['#item']['depth'];
$indentation = str_repeat('&nbsp;&nbsp;', $depth - 1) . ($depth > 1 ? '-&nbsp;' : '');
$row[] = $indentation . drupal_render($element['title']);
$row[] = theme('indentation', $element['#item']['depth'] - 1) . drupal_render($element['title']);
$row[] = array('data' => drupal_render($element['expanded']), 'class' => 'checkbox');
$row[] = array('data' => drupal_render($element['hidden']), 'class' => 'checkbox');
$row[] = drupal_render($element['weight']) . drupal_render($element['plid']) . drupal_render($element['mlid']);
$row = array_merge($row, $operations);
$row = array_merge(array('data' => $row), $element['#attributes']);
$row['class'] = !empty($row['class']) ? $row['class'] .' draggable' : 'draggable';
$rows[] = $row;
}
}
......
......@@ -46,8 +46,9 @@ thead th {
}
div.indentation {
width: 20px;
height: 1.7em;
margin: -0.4em 0.2em -0.4em -0.4em;
padding: 0.4em 0 0.4em 0.6em;
padding: 0.42em 0 0.42em 0.6em;
float: left;
}
div.tree-child {
......@@ -367,7 +368,7 @@ a.tabledrag-handle {
cursor: move;
float: left;
height: 1.7em;
margin: -0.42em 0 -0.42em -0.5em;
margin: -0.4em 0 -0.4em -0.5em;
padding: 0.42em 1.5em 0.42em 0.5em;
text-decoration: none;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment