diff options
author | Gábor Hojtsy <gabor@hojtsy.hu> | 2007-11-20 10:18:43 +0000 |
---|---|---|
committer | Gábor Hojtsy <gabor@hojtsy.hu> | 2007-11-20 10:18:43 +0000 |
commit | 6d4862c25899023089832c6e350e66720b04d81f (patch) | |
tree | aa3fc0699d37a3534846c990a4774abc1d0e0eab | |
parent | 72a59b0b825437daf458e659a6303e67ea84dfbf (diff) | |
download | brdo-6d4862c25899023089832c6e350e66720b04d81f.tar.gz brdo-6d4862c25899023089832c6e350e66720b04d81f.tar.bz2 |
#181126 by quicksketch et al: drag and drop support for menus
-rw-r--r-- | includes/common.inc | 23 | ||||
-rw-r--r-- | includes/theme.inc | 16 | ||||
-rw-r--r-- | misc/tabledrag.js | 54 | ||||
-rw-r--r-- | modules/menu/menu.admin.inc | 89 | ||||
-rw-r--r-- | modules/system/system.css | 5 |
5 files changed, 146 insertions, 41 deletions
diff --git a/includes/common.inc b/includes/common.inc index 4c7ff96d1..94a5cc537 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -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()), diff --git a/includes/theme.inc b/includes/theme.inc index c986c05e5..27cdf822b 100644 --- a/includes/theme.inc +++ b/includes/theme.inc @@ -1672,6 +1672,22 @@ function theme_progress_bar($percent, $message) { } /** + * 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"> </div>'; + } + return $output; +} + +/** * @} End of "defgroup themeable". */ diff --git a/misc/tabledrag.js b/misc/tabledrag.js index 03f5a4bb8..c379efc97 100644 --- a/misc/tabledrag.js +++ b/misc/tabledrag.js @@ -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; } } diff --git a/modules/menu/menu.admin.inc b/modules/menu/menu.admin.inc index 3ac0fcb23..ad6bccae4 100644 --- a/modules/menu/menu.admin.inc +++ b/modules/menu/menu.admin.inc @@ -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(' ', $depth - 1) . ($depth > 1 ? '- ' : ''); - $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; } } diff --git a/modules/system/system.css b/modules/system/system.css index 7333c470f..9422847c5 100644 --- a/modules/system/system.css +++ b/modules/system/system.css @@ -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; } |