Commit 6049f237 authored by Dries's avatar Dries
Browse files

- Patch #181066 by quicksketch et al: drag and drop of table rows on the block adminsitration page.

parent 44373cf0
......@@ -46,6 +46,7 @@ Drupal 6.0, xxxx-xx-xx (development version)
.info file.
* Dynamically check password strength and confirmation.
* Refactored poll administration.
* Drag and drop of table rows in the block administration page.
- Theme system:
* Added .info files to themes and made it easier to specify regions and
features.
......
......@@ -1926,6 +1926,113 @@ function drupal_get_js($scope = 'header', $javascript = NULL) {
return $output;
}
/**
* Assist in adding the tableDrag JavaScript behavior to a themed table.
*
* Draggable tables should be used wherever an outline or list of sortable items
* needs to be arranged by an end-user. Draggable tables are very flexible and
* can manipulate the value of form elements placed within individual columns.
*
* To setup a table to use drag and drop in place of weight select-lists or
* in place of a form that contains parent relationships, the form must be
* themed into a table. The table must have an id attribute set. If using
* theme_table(), the id may be set as such:
* @code
* $output = theme('table', $header, $rows, array('id' => 'my-module-table'));
* return $output;
* @endcode
*
* In the theme function for the form, a special class must be added to each
* form element within the same column, "grouping" them together.
*
* 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";
* @endcode
*
* Calling drupal_add_tabledrag() would then be written as such:
* @code
* drupal_add_tabledrag('my-module-table', 'sort', '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;
* @endcode
*
* $group is still 'my-element-weight', and the additional $subgroup variable
* will be passed in as 'my-elements-weight-'. $region. This also means that
* you'll need to call drupal_add_tabledrag() once for every region added.
*
* @code
* foreach ($regions as $region) {
* drupal_add_tabledrag('my-module-table', 'sort', 'sibling', 'my-elements-weight', 'my-elements-weight-'. $region);
* }
* @endcode
*
* In a situation where tree relationships are present, adding multiple
* subgroups is not necessary, because the table will contain indentations that
* provide enough information about the sibling and parent relationships.
* See theme_menu_overview_form() for an example creating a table containing
* parent relationships.
*
* Please note that this function should be called from the theme layer, such as
* in a .tpl.php file, theme_ function, or in a template_preprocess function,
* not in a form declartion. Though the same JavaScript could be added to the
* page using drupal_add_js() directly, this function helps keep template files
* clean and readable. It also prevents tabledrag.js from being added twice
* accidentally.
*
* @param $table_id
* 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.
* @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.
* @param $group
* A class name applied on all related form elements for this action.
* @param $subgroup
* (optional) If the group has several subgroups within it, this string should
* contain the class name identifying fields in the same subgroup.
* @param $source
* (optional) If the $action is 'match', this string should contain the class
* name identifying what field will be used as the source value when matching
* the value in $subgroup.
* @param $hidden
* (optional) The column containing the field elements may be entirely hidden
* from view dynamically when the JavaScript is loaded. Set to FALSE if the
* column should not be hidden.
* @see block-admin-display-form.tpl.php
* @see theme_menu_overview_form()
*/
function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgroup = NULL, $source = NULL, $hidden = TRUE) {
static $js_added = FALSE;
if (!$js_added) {
drupal_add_js('misc/tabledrag.js', 'core');
$js_added = TRUE;
}
// If a subgroup or source isn't set, assume it is the same as the group.
$target = isset($subgroup) ? $subgroup : $group;
$source = isset($source) ? $source : $target;
$settings['tableDrag'][$table_id][$group][] = array(
'target' => $target,
'source' => $source,
'relationship' => $relationship,
'action' => $action,
'hidden' => $hidden,
);
drupal_add_js($settings, 'setting');
}
/**
* Aggregate JS files, putting them in the files directory.
*
......
This diff is collapsed.
misc/tree.png

979 Bytes

......@@ -6,13 +6,14 @@
* Default theme implementation to configure blocks.
*
* Available variables:
* - $block_listing: An array of block controls within regions.
* - $block_regions: An array of regions. Keyed by name with the title as value.
* - $block_listing: An array of blocks keyed by region and then delta.
* - $form_submit: Form submit button.
* - $throttle: TRUE or FALSE depending on throttle module being enabled.
*
* Each $data in $block_listing contains:
* - $data->is_region_first: TRUE or FALSE depending on the listed blocks
* positioning. Used here to insert a region header.
* Each $block_listing[$region] contains an array of blocks for that region.
*
* Each $data in $block_listing[$region] contains:
* - $data->region_title: Region title for the listed block.
* - $data->block_title: Block title.
* - $data->region_select: Drop-down menu for assigning a region.
......@@ -25,9 +26,15 @@
* @see theme_block_admin_display()
*/
?>
<?php drupal_add_js('misc/tableheader.js'); ?>
<?php print $messages; ?>
<?php
// Add table javascript.
drupal_add_js('misc/tableheader.js');
drupal_add_js(drupal_get_path('module', 'block') .'/block.js');
foreach ($block_regions as $region => $title) {
drupal_add_tabledrag('blocks', 'match', 'sibling', 'block-region-select', 'block-region-'. $region, NULL, FALSE);
drupal_add_tabledrag('blocks', 'order', 'sibling', 'block-weight', 'block-weight-'. $region);
}
?>
<table id="blocks">
<thead>
<tr>
......@@ -42,15 +49,16 @@
</thead>
<tbody>
<?php $row = 0; ?>
<?php foreach ($block_listing as $data): ?>
<?php if ($data->is_region_first): ?>
<tr class="<?php print $row % 2 == 0 ? 'odd' : 'even'; ?>">
<td colspan="<?php print $throttle ? '7' : '6'; ?>" class="region"><?php print $data->region_title; ?></td>
<?php foreach ($block_regions as $region => $title): ?>
<tr class="region region-<?php print $region?>">
<td colspan="<?php print $throttle ? '6' : '5'; ?>" class="region"><?php print $title; ?></td>
</tr>
<?php $row++; ?>
<?php endif; ?>
<tr class="<?php print $row % 2 == 0 ? 'odd' : 'even'; ?><?php print $data->row_class ? ' '. $data->row_class : ''; ?>">
<td class="block"><?php print $data->block_title; ?><?php print $data->block_modified ? '<span class="warning">*</span>' : ''; ?></td>
<tr class="region-message region-<?php print $region?>-message <?php print empty($block_listing[$region]) ? 'region-empty' : 'region-populated'; ?>">
<td colspan="<?php print $throttle ? '6' : '5'; ?>"><em><?php print t('No blocks in this region'); ?></em></td>
</tr>
<?php foreach ($block_listing[$region] as $delta => $data): ?>
<tr class="draggable <?php print $row % 2 == 0 ? 'odd' : 'even'; ?><?php print $data->row_class ? ' '. $data->row_class : ''; ?>">
<td class="block"><?php print $data->block_title; ?></td>
<td><?php print $data->region_select; ?></td>
<td><?php print $data->weight_select; ?></td>
<?php if ($throttle): ?>
......@@ -60,6 +68,7 @@
<td><?php print $data->delete_link; ?></td>
</tr>
<?php $row++; ?>
<?php endforeach; ?>
<?php endforeach; ?>
</tbody>
</table>
......
/* $Id$ */
#blocks td.block {
padding-left: inherit;
padding-right: 1.5em;
}
#blocks select {
margin-left: 24px;
}
#blocks select.progress-disabled {
margin-left: 0px;
}
#blocks .progress .bar {
float: right;
}
......@@ -36,16 +36,14 @@ function block_admin_display_form(&$form_state, $blocks, $theme = NULL) {
init_theme();
$throttle = module_exists('throttle');
$block_regions = array(BLOCK_REGION_NONE => '<'. t('none') .'>') + system_region_list($theme_key);
$block_regions = system_region_list($theme_key) + array(BLOCK_REGION_NONE => '<'. t('none') .'>');
// Build form tree
$form = array(
'#action' => arg(3) ? url('admin/build/block/list/'. $theme_key) : url('admin/build/block'),
'#tree' => TRUE,
'#cache' => TRUE,
'#prefix' => '<div id="block-admin-display-form-wrapper">',
'#suffix' => '</div>',
);
foreach ($blocks as $i => $block) {
$key = $block['module'] .'_'. $block['delta'];
$form[$key]['module'] = array(
......@@ -69,7 +67,7 @@ function block_admin_display_form(&$form_state, $blocks, $theme = NULL) {
);
$form[$key]['region'] = array(
'#type' => 'select',
'#default_value' => $block['status'] ? (isset($block['region']) ? $block['region'] : system_default_region($theme_key)) : BLOCK_REGION_NONE,
'#default_value' => $block['region'],
'#options' => $block_regions,
);
......@@ -82,20 +80,9 @@ function block_admin_display_form(&$form_state, $blocks, $theme = NULL) {
}
}
// Attach the AHAH events to the submit button. Set the AHAH selector to every
// select element in the form. The AHAH event could be attached to every select
// element individually, but using the selector is more efficient, especially
// on a page where hundreds of AHAH enabled elements may be present.
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save blocks'),
'#ahah' => array(
'path' => 'admin/build/block/list/js/'. $theme_key,
'selector' => '#block-admin-display-form-wrapper select',
'wrapper' => 'block-admin-display-form-wrapper',
'event' => 'change',
'effect' => 'fade',
),
);
return $form;
......@@ -114,91 +101,6 @@ function block_admin_display_form_submit($form, &$form_state) {
cache_clear_all();
}
/**
* Javascript callback for AHAH replacement. Re-generate the form with the
* updated values and return necessary html.
*/
function block_admin_display_js($theme = NULL) {
// Load the cached form.
$form_cache = cache_get('form_'. $_POST['form_build_id'], 'cache_form');
// Set the new weights and regions for each block.
$blocks = array();
foreach (element_children($form_cache->data) as $key) {
$field = $form_cache->data[$key];
if (isset($field['info'])) {
$block = array(
'module' => $field['module']['#value'],
'delta' => $field['delta']['#value'],
'info' => html_entity_decode($field['info']['#value'], ENT_QUOTES),
'region' => $_POST[$key]['region'],
'weight' => $_POST[$key]['weight'],
'status' => $_POST[$key]['region'] == BLOCK_REGION_NONE ? 0 : 1,
);
$throttle = module_exists('throttle');
if ($throttle) {
$block['throttle'] = !empty($_POST[$key]['throttle']);
}
if ($block['weight'] != $form_cache->data[$key]['weight']['#default_value'] || $block['region'] != $form_cache->data[$key]['region']['#default_value']) {
$changed_block = $block['module'] .'_'. $block['delta'];
}
$blocks[] = $block;
}
}
// Resort the blocks with the new weights.
usort($blocks, '_block_compare');
// Create a form in the new order.
$form_state = array('submitted' => FALSE);
$form = block_admin_display_form($form_state, $blocks, $theme);
// Maintain classes set on individual blocks.
foreach (element_children($form_cache->data) as $key) {
if (isset($form_cache->data[$key]['#attributes'])) {
$form[$key]['#attributes'] = $form_cache->data[$key]['#attributes'];
}
}
// Preserve the order of the new form while merging the previous data.
$form_order = array_flip(array_keys($form)); // Save the form order.
$form = array_merge($form_cache->data, $form); // Merge the data.
$form = array_merge($form_order, $form); // Put back into the correct order.
// Add a permanent class to the changed block.
$form[$changed_block]['#attributes']['class'] = 'block-modified';
cache_set('form_'. $_POST['form_build_id'], $form, 'cache_form', $form_cache->expire);
// Add a temporary class to mark the new AHAH content.
$form[$changed_block]['#attributes']['class'] = empty($form[$changed_block]['#attributes']['class']) ? 'ahah-new-content' : $form[$changed_block]['#attributes']['class'] .' ahah-new-content';
$form['js_modified'] = array(
'#type' => 'value',
'#value' => TRUE,
);
$form['#post'] = $_POST;
$form['#theme'] = 'block_admin_display_form';
// Add messages to our output.
drupal_set_message(t('Your settings will not be saved until you click the <em>Save blocks</em> button.'), 'warning');
// Render the form.
drupal_alter('form', $form, array(), 'block_admin_display_form');
$form = form_builder('block_admin_display_form', $form, $form_state);
// Remove the wrapper from the form to prevent duplicate div IDs.
unset($form['#prefix'], $form['#suffix']);
$output = drupal_render($form);
// Return the output in JSON format.
drupal_json(array('status' => TRUE, 'data' => $output));
}
/**
* Helper function for sorting blocks on admin/build/block.
*
......@@ -206,13 +108,22 @@ function block_admin_display_js($theme = NULL) {
* Disabled blocks are sorted by name.
*/
function _block_compare($a, $b) {
$status = $b['status'] - $a['status'];
global $theme_key;
static $regions;
// We need the region list to correctly order by region.
if (!isset($regions)) {
$regions = array_flip(array_keys(system_region_list($theme_key)));
$regions[BLOCK_REGION_NONE] = count($regions);
}
// Separate enabled from disabled.
$status = $b['status'] - $a['status'];
if ($status) {
return $status;
}
// Sort by region.
$place = strcmp($a['region'], $b['region']);
// Sort by region (in the order defined by theme .info file).
$place = $regions[$a['region']] - $regions[$b['region']];
if ($place) {
return $place;
}
......@@ -442,14 +353,20 @@ function block_box_delete_submit($form, &$form_state) {
function template_preprocess_block_admin_display_form(&$variables) {
global $theme_key;
$variables['throttle'] = module_exists('throttle');
$block_regions = system_region_list($theme_key);
$variables['throttle'] = module_exists('throttle');
$variables['block_regions'] = $block_regions + array(BLOCK_REGION_NONE => t('Disabled'));
// Highlight regions on page to provide visual reference.
foreach ($block_regions as $key => $value) {
// Highlight regions on page to provide visual reference.
drupal_set_content($key, '<div class="block-region">'. $value .'</div>');
// Initialize an empty array for the region.
$variables['block_listing'][$key] = array();
}
// Initialize disabled blocks array.
$variables['block_listing'][BLOCK_REGION_NONE] = array();
// Setup to track previous region in loop.
$last_region = '';
foreach (element_children($variables['form']) as $i) {
......@@ -460,34 +377,23 @@ function template_preprocess_block_admin_display_form(&$variables) {
// Fetch region for current block.
$region = $block['region']['#default_value'];
// Track first block listing to insert region header inside block_admin_display.tpl.php.
$is_region_first = FALSE;
if ($last_region != $region) {
$is_region_first = TRUE;
// Set region title. Block regions already translated.
if ($region != BLOCK_REGION_NONE) {
$region_title = drupal_ucfirst($block_regions[$region]);
}
else {
$region_title = t('Disabled');
}
}
$variables['block_listing'][$i]->is_region_first = $is_region_first;
$variables['block_listing'][$i]->row_class = isset($block['#attributes']['class']) ? $block['#attributes']['class'] : '';
$variables['block_listing'][$i]->block_modified = isset($block['#attributes']['class']) && strpos($block['#attributes']['class'], 'block-modified') !== FALSE ? TRUE : FALSE;
$variables['block_listing'][$i]->region_title = $region_title;
$variables['block_listing'][$i]->block_title = drupal_render($block['info']);
$variables['block_listing'][$i]->region_select = drupal_render($block['region']) . drupal_render($block['theme']);
$variables['block_listing'][$i]->weight_select = drupal_render($block['weight']);
$variables['block_listing'][$i]->throttle_check = $variables['throttle'] ? drupal_render($block['throttle']) : '';
$variables['block_listing'][$i]->configure_link = drupal_render($block['configure']);
$variables['block_listing'][$i]->delete_link = !empty($block['delete']) ? drupal_render($block['delete']) : '';
// Set special classes needed for table drag and drop.
$variables['form'][$i]['region']['#attributes']['class'] = 'block-region-select block-region-'. $region;
$variables['form'][$i]['weight']['#attributes']['class'] = 'block-weight block-weight-'. $region;
$variables['block_listing'][$region][$i]->row_class = isset($block['#attributes']['class']) ? $block['#attributes']['class'] : '';
$variables['block_listing'][$region][$i]->block_modified = isset($block['#attributes']['class']) && strpos($block['#attributes']['class'], 'block-modified') !== FALSE ? TRUE : FALSE;
$variables['block_listing'][$region][$i]->block_title = drupal_render($block['info']);
$variables['block_listing'][$region][$i]->region_select = drupal_render($block['region']) . drupal_render($block['theme']);
$variables['block_listing'][$region][$i]->weight_select = drupal_render($block['weight']);
$variables['block_listing'][$region][$i]->throttle_check = $variables['throttle'] ? drupal_render($block['throttle']) : '';
$variables['block_listing'][$region][$i]->configure_link = drupal_render($block['configure']);
$variables['block_listing'][$region][$i]->delete_link = !empty($block['delete']) ? drupal_render($block['delete']) : '';
$variables['block_listing'][$region][$i]->printed = FALSE;
$last_region = $region;
}
}
$variables['messages'] = isset($variables['form']['js_modified']) ? theme('status_messages') : '';
$variables['form_submit'] = drupal_render($variables['form']);
}
......@@ -3,8 +3,12 @@
#blocks td.region {
font-weight: bold;
}
#blocks td.block {
padding-left: 1.5em; /* LTR */
#blocks tr.region-message {
font-weight: normal;
color: #999;
}
#blocks tr.region-populated {
display: none;
}
.block-region {
background-color: #ff6;
......@@ -12,12 +16,3 @@
margin-bottom: 4px;
padding: 3px;
}
#blocks select {
margin-right: 24px; /* LTR */
}
#blocks select.progress-disabled {
margin-right: 0px; /* LTR */
}
#blocks tr.ahah-new-content {
background-color: #ffd;
}
// $Id $
/**
* Move a block in the blocks table from one region to another via select list.
*
* This behavior is dependent on the tableDrag behavior, since it uses the
* objects initialized in that behavior to update the row.
*/
Drupal.behaviors.blockDrag = function(context) {
var table = $('table#blocks');
var tableDrag = Drupal.tableDrag.blocks; // Get the blocks tableDrag object.
// Add a handler for when a row is swapped, update empty regions.
tableDrag.row.prototype.onSwap = function(swappedRow) {
checkEmptyRegions(table, this);
};
// A custom message for the blocks page specifically.
Drupal.theme.tableDragChangedWarning = function () {
return '<div class="warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t("The changes to these blocks will not be saved until the <em>Save blocks</em> button is clicked.") + '</div>';
};
// Add a handler so when a row is dropped, update fields dropped into new regions.
tableDrag.onDrop = function() {
dragObject = this;
if ($(dragObject.rowObject.element).prev('tr').is('.region-message')) {
var regionRow = $(dragObject.rowObject.element).prev('tr').get(0);
var regionName = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2');
var regionField = $('select.block-region-select', dragObject.rowObject.element);
var weightField = $('select.block-weight', dragObject.rowObject.element);
var oldRegionName = weightField[0].className.replace(/([^ ]+[ ]+)*block-weight-([^ ]+)([ ]+[^ ]+)*/, '$2');
if (!regionField.is('.block-region-'+ regionName)) {
regionField.removeClass('block-region-' + oldRegionName).addClass('block-region-' + regionName);
weightField.removeClass('block-weight-' + oldRegionName).addClass('block-weight-' + regionName);
regionField.val(regionName);
}
}
};
// Add the behavior to each region select list.
$('select.block-region-select:not(.blockregionselect-processed)', context).each(function() {
$(this).change(function(event) {
// Make our new row and select field.
var row = $(this).parents('tr:first');
var select = $(this);
tableDrag.rowObject = new tableDrag.row(row);
// Find the correct region and insert the row as the first in the region.
$('tr.region-message', table).each(function() {
if ($(this).is('.region-' + select[0].value + '-message')) {
// Add the new row and remove the old one.
$(this).after(row);
// Manually update weights and restripe.
tableDrag.updateFields(row.get(0));
tableDrag.rowObject.changed = true;
if (tableDrag.oldRowElement) {
$(tableDrag.oldRowElement).removeClass('drag-previous');
}
tableDrag.oldRowElement = row.get(0);
tableDrag.restripeTable();
tableDrag.rowObject.markChanged();
tableDrag.oldRowElement = row;
$(row).addClass('drag-previous');
}
});
// Modify empty regions with added or removed fields.
checkEmptyRegions(table, row);
// Remove focus from selectbox.
select.get(0).blur();
});
$(this).addClass('blockregionselect-processed');
});
var checkEmptyRegions = function(table, rowObject) {
$('tr.region-message', table).each(function() {
// If the dragged row is in this region, but above the message row, swap it down one space.
if ($(this).prev('tr').get(0) == rowObject.element) {
// Prevent a recursion problem when using the keyboard to move rows up.
if ((rowObject.method != 'keyboard' || rowObject.direction == 'down')) {
rowObject.swap('after', this);
}
}
// This region has become empty
if ($(this).next('tr').is(':not(.draggable)') || $(this).next('tr').size() == 0) {
$(this).removeClass('region-populated').addClass('region-empty');
}
// This region has become populated.
else if ($(this).is('.region-empty')) {
$(this).removeClass('region-empty').addClass('region-populated');
}
});
};
};
......@@ -248,6 +248,9 @@ function _block_rehash() {
}
// Add defaults and save it into the database.
drupal_write_record('blocks', $block);
// Set region to none if not enabled.
$block['region'] = $block['status'] ? $block['region'] : BLOCK_REGION_NONE;
// Add to the list of blocks we return.
$blocks[] = $block;
}
else {
......@@ -257,7 +260,9 @@ function _block_rehash() {
// do not need to update the database here.
// Add 'info' to this block.
$old_blocks[$module][$delta]['info'] = $block['info'];
// Add this block to the list of blocks we return
// Set region to none if not enabled.
$old_blocks[$module][$delta]['region'] = $old_blocks[$module][$delta]['status'] ? $old_blocks[$module][$delta]['region'] : BLOCK_REGION_NONE;
// Add this block to the list of blocks we return.
$blocks[] = $old_blocks[$module][$delta];
// Remove this block from the list of blocks to be deleted.
unset($old_blocks[$module][$delta]);
......