diff -rupN content.module content.module
--- content.module 2009-01-20 08:34:07.000000000 +0100
+++ content.module 2009-01-20 13:26:25.000000000 +0100
@@ -483,8 +483,11 @@ function theme_content_multiple_values($
'data' => t('!title: !required', array('!title' => $element['#title'], '!required' => $required)),
'colspan' => 2
),
- t('Order'),
+ array('data' => t('Order'), 'class' => 'content-multiple-weight-header'),
);
+ if ($field['multiple'] == 1) {
+ $header[] = array('data' => t('Remove'), 'class' => 'content-multiple-remove-header');
+ }
$rows = array();
// Sort items according to '_weight' (needed when the form comes back after
@@ -492,24 +495,31 @@ function theme_content_multiple_values($
$items = array();
foreach (element_children($element) as $key) {
if ($key !== $element['#field_name'] .'_add_more') {
- $items[] = &$element[$key];
+ $items[$element[$key]['#delta']] = &$element[$key];
}
}
- usort($items, '_content_sort_items_value_helper');
+ uasort($items, '_content_sort_items_value_helper');
// Add the items as table rows.
- foreach ($items as $key => $item) {
+ foreach ($items as $delta => $item) {
$item['_weight']['#attributes']['class'] = $order_class;
$delta_element = drupal_render($item['_weight']);
+ if ($field['multiple'] == 1) {
+ $remove_element = drupal_render($item['_remove']);
+ }
$cells = array(
array('data' => '', 'class' => 'content-multiple-drag'),
drupal_render($item),
array('data' => $delta_element, 'class' => 'delta-order'),
);
- $rows[] = array(
- 'data' => $cells,
- 'class' => 'draggable',
- );
+ $row_class = 'draggable';
+ if ($field['multiple'] == 1) {
+ if (!empty($item['_remove']['#default_value'])) {
+ $row_class .= ' content-multiple-removed-row';
+ }
+ $cells[] = array('data' => $remove_element, 'class' => 'content-multiple-remove-cell');
+ }
+ $rows[] = array('data' => $cells, 'class' => $row_class);
}
$output .= theme('table', $header, $rows, array('id' => $table_id, 'class' => 'content-multiple-table'));
@@ -517,6 +527,7 @@ function theme_content_multiple_values($
$output .= drupal_render($element[$element['#field_name'] .'_add_more']);
drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class);
+ drupal_add_js(drupal_get_path('module', 'content') .'/theme/content-edit.js');
}
else {
foreach (element_children($element) as $key) {
@@ -717,8 +728,8 @@ function content_field($op, &$node, $fie
case 'view':
$addition = array();
- // Previewed nodes bypass the 'presave' op, so we need to some massaging.
- if ($node->build_mode == NODE_BUILD_PREVIEW && content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) {
+ // Previewed nodes bypass the 'presave' op, so we need to do some massaging.
+ if ($node->build_mode == NODE_BUILD_PREVIEW) {
if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) {
// Reorder items to account for drag-n-drop reordering.
$items = _content_sort_items($field, $items);
@@ -763,10 +774,10 @@ function content_field($op, &$node, $fie
);
// Fill-in items.
- foreach ($items as $delta => $item) {
+ foreach (array_keys($items) as $weight => $delta) {
$element['items'][$delta] = array(
- '#item' => $item,
- '#weight' => $delta,
+ '#item' => $items[$delta],
+ '#weight' => $weight,
);
}
@@ -886,20 +897,22 @@ function content_field($op, &$node, $fie
* returns filtered and adjusted item array
*/
function content_set_empty($field, $items) {
- // Filter out empty values.
+ // Prepare an empty item.
+ $empty = array();
+ foreach (array_keys($field['columns']) as $column) {
+ $empty[$column] = NULL;
+ }
+
+ // Filter out items flagged for removal.
$filtered = array();
$function = $field['module'] .'_content_is_empty';
foreach ((array) $items as $delta => $item) {
- if (!$function($item, $field)) {
- $filtered[] = $item;
+ if (empty($item['_remove'])) {
+ $filtered[] = ($function($item, $field) ? $empty : $item);
}
}
// Make sure we store the right number of 'empty' values.
- $empty = array();
- foreach (array_keys($field['columns']) as $column) {
- $empty[$column] = NULL;
- }
$pad = $field['multiple'] > 1 ? $field['multiple'] : 1;
$filtered = array_pad($filtered, $pad, $empty);
@@ -914,7 +927,7 @@ function _content_sort_items($field, $it
if ($field['multiple'] >= 1 && isset($items[0]['_weight'])) {
usort($items, '_content_sort_items_helper');
foreach ($items as $delta => $item) {
- if (is_array($items[$delta])) {
+ if (is_array($item) && isset($item['_weight'])) {
unset($items[$delta]['_weight']);
}
}
@@ -995,7 +1008,18 @@ function content_storage($op, $node) {
if (!isset($additions[$field_name])) {
$additions[$field_name] = array();
}
- $additions[$field_name][] = $item;
+
+ // When the node is loaded and saved programatically, we need to
+ // ensure all items are preserved during content_set_empty().
+ $item['_remove'] = 0;
+
+ // Preserve deltas when loading items from database.
+ if (isset($row['delta'])) {
+ $additions[$field_name][$row['delta']] = $item;
+ }
+ else {
+ $additions[$field_name][] = $item;
+ }
}
}
}
Binary files images/button-remove.png and images/button-remove.png differ
Binary files images/button-throbber.gif and images/button-throbber.gif differ
diff -rupN includes/content.node_form.inc includes/content.node_form.inc
--- includes/content.node_form.inc 2008-12-27 23:22:56.000000000 +0100
+++ includes/content.node_form.inc 2009-01-20 13:11:53.000000000 +0100
@@ -151,21 +151,23 @@ function content_multiple_value_form(&$f
switch ($field['multiple']) {
case 0:
+ $deltas = array(0);
$max = 0;
break;
- case 1:
- $filled_items = content_set_empty($field, $items);
- $current_item_count = isset($form_state['item_count'][$field_name])
- ? $form_state['item_count'][$field_name]
- : count($items);
- // We always want at least one empty icon for the user to fill in.
- $max = ($current_item_count > count($filled_items))
- ? $current_item_count - 1
- : $current_item_count;
+ case 1:
+ $deltas = array_keys($items);
+ $current_item_count = isset($form_state['item_count'][$field_name]) ? $form_state['item_count'][$field_name] : max(1, count($deltas));
+ $max = (!empty($deltas) ? max($deltas) : -1);
+ while (count($deltas) < $current_item_count) {
+ $max++;
+ $deltas[] = $max;
+ }
break;
+
default:
$max = $field['multiple'] - 1;
+ $deltas = array_keys(array_fill(0, $field['multiple'], 0));
break;
}
@@ -180,7 +182,7 @@ function content_multiple_value_form(&$f
);
$function = $field['widget']['module'] .'_widget';
- for ($delta = 0; $delta <= $max; $delta++) {
+ foreach ($deltas as $delta) {
if ($element = $function($form, $form_state, $field, $items, $delta)) {
$defaults = array(
'#title' => ($field['multiple'] >= 1) ? '' : $title,
@@ -206,6 +208,21 @@ function content_multiple_value_form(&$f
);
}
+ // Add a checkbox to allow users remove a single delta item.
+ // See content_set_empty() and theme_content_multiple_values().
+ if ($field['multiple'] == 1) {
+ // We name the element '_remove' to avoid clashing with column names
+ // defined by field modules.
+ $element['_remove'] = array(
+ '#type' => 'checkbox',
+ '#attributes' => array(
+ 'class' => 'content-multiple-remove-checkbox',
+ 'alt' => 'type:'. $field['widget']['type'] .',name:'. $field_name .',delta:'. $delta,
+ ),
+ '#default_value' => isset($items[$delta]['_remove']) ? $items[$delta]['_remove'] : 0,
+ );
+ }
+
$form_element[$delta] = array_merge($element, $defaults);
}
}
@@ -313,11 +330,13 @@ function content_add_more_js($type_name_
unset($form_state['values'][$field_name][$field['field_name'] .'_add_more']);
foreach ($_POST[$field_name] as $delta => $item) {
$form_state['values'][$field_name][$delta]['_weight'] = $item['_weight'];
+ $form_state['values'][$field_name][$delta]['_remove'] = isset($item['_remove']) ? $item['_remove'] : 0;
}
$form_state['values'][$field_name] = _content_sort_items($field, $form_state['values'][$field_name]);
$_POST[$field_name] = _content_sort_items($field, $_POST[$field_name]);
// Build our new form element for the whole field, asking for one more element.
+ $delta = max(array_keys($_POST[$field_name])) + 1;
$form_state['item_count'] = array($field_name => count($_POST[$field_name]) + 1);
$form_element = content_field_form($form, $form_state, $field);
// Let other modules alter it.
@@ -337,7 +356,6 @@ function content_add_more_js($type_name_
// Build the new form against the incoming $_POST values so that we can
// render the new element.
- $delta = max(array_keys($_POST[$field_name])) + 1;
$_POST[$field_name][$delta]['_weight'] = $delta;
$form_state = array('submitted' => FALSE);
$form += array(
diff -rupN theme/content-edit.js theme/content-edit.js
--- theme/content-edit.js 1970-01-01 01:00:00.000000000 +0100
+++ theme/content-edit.js 2009-01-20 11:03:00.000000000 +0100
@@ -0,0 +1,191 @@
+// $Id$
+
+/**
+ * Manipulation of content remove buttons.
+ *
+ * TableDrag objects for multiple value fields (and multigroups) are scanned
+ * to find 'remove' checkboxes. These checkboxes are hidden when javascript is
+ * enabled (using the Global CSS Killswitch, html.js, defined in drupal.js).
+ * A new 'remove' button is created here in place of these checkboxes aimed to
+ * provide a more user-friendly method to remove items. The onClick handler of
+ * these buttons fire the onRemove event of the corresponding checkbox that can
+ * be used by field widgets to perform additional processing.
+ *
+ * Custom onRemove handlers are attached to 'remove' checkboxes as follows:
+ *
+ *
+ * $('.content-multiple-remove-checkbox').not('.my-widget-onremove-processed').each(function() {
+ * // Make sure the element is not processed more than once.
+ * $(this).addClass('my-widget-onremove-processed');
+ *
+ * // Attach custom onRemove handler to each checkbox.
+ * $(this).bind('remove', function(event, settings) {
+ * if (settings.type == 'my_widget_type') {
+ * // Custom code to process the remove event of the widget.
+ * }
+ * });
+ * });
+ *
+ *
+ * Note that onRemove handlers should filter by settings.type and/or
+ * settings.name before processing the event.
+ *
+ * onRemove handlers are invoked with the following arguments:
+ * event - The onClick event object.
+ * settings - An object with the following properties:
+ * tableDrag - The tableDrag object.
+ * checkbox_id - The ID of the checkbox element.
+ * checkbox - The DOM object of the remove checkbox element.
+ * button - The DOM object of the remove button element.
+ * type - field or multigroup.
+ * name - The name of the field or multigroup.
+ * delta - The delta of the field or fields in the subgroup.
+ */
+Drupal.behaviors.contentRemoveButtons = function(context) {
+ $('table.content-multiple-table', context).not('.content-multiple-remove-buttons-processed').each(function() {
+ var table = this, tableDrag = Drupal.tableDrag[$(table).attr('id')];
+
+ // Make sure the element is not processed more than once.
+ $(table).addClass('content-multiple-remove-buttons-processed');
+
+ // Create remove buttons, only when there is more than one item.
+ if ($('input.content-multiple-remove-checkbox', table).length > 1) {
+ $('input.content-multiple-remove-checkbox', table).each(function() {
+ Drupal.contentRemoveButtons.createButton(tableDrag, this);
+ });
+ }
+ });
+};
+
+/**
+ * Global contentRemoveButtons object.
+ */
+Drupal.contentRemoveButtons = {
+ _isBusy: false
+};
+
+/**
+ * Create a 'remove' button.
+ */
+Drupal.contentRemoveButtons.createButton = function(tableDrag, checkbox) {
+ // Parse checkbox options passed via alt attribute.
+ var settings = {}, checkbox_alt = $(checkbox).attr('alt');
+ if (typeof checkbox_alt != 'string' || checkbox_alt.length <= 0) {
+ return;
+ }
+ checkbox_alt = checkbox_alt.split(',');
+ for (var i = 0; i < checkbox_alt.length; i++) {
+ var option = checkbox_alt[i].split(':');
+ if (option.length == 2) {
+ settings[option[0]] = option[1];
+ }
+ }
+ // Ignore checkbox if required settings are not present.
+ if (settings.type == undefined || settings.name == undefined || settings.delta == undefined) {
+ return;
+ }
+
+ // Create a new remove button.
+ var button = $(Drupal.theme('contentRemoveButton', settings.type, settings.name, settings.delta));
+
+ // Complete the button settings.
+ $.extend(settings, {
+ tableDrag: tableDrag,
+ checkbox_id: $(checkbox).attr('id'),
+ checkbox: checkbox,
+ button: button.get(0)
+ });
+
+ // Bind the onClick event to the 'remove' button.
+ button.bind('click', function(event) {
+ $(this).blur();
+
+ // Do not allow users click on a 'remove' button while the process of
+ // another one has not been finished yet.
+ if (!Drupal.contentRemoveButtons._isBusy) {
+ Drupal.contentRemoveButtons._isBusy = true;
+ Drupal.contentRemoveButtons.onClick(event, settings);
+ }
+
+ return false;
+ });
+
+ // Attach the new button to the DOM tree.
+ $(checkbox).parent().append(button);
+};
+
+/**
+ * onClick handler for 'remove' buttons.
+ */
+Drupal.contentRemoveButtons.onClick = function(event, settings) {
+ // Display the throbber while remove callbacks are being processed.
+ $(settings.button).addClass('content-multiple-throbber');
+
+ // Check the checkbox related to the 'remove' button.
+ $('input[@id='+ settings.checkbox_id +']').attr('checked', 'checked');
+
+ // Trigger all onRemove event handlers attached to the checkbox.
+ $(settings.checkbox).trigger('remove', [event, settings]);
+
+ // Hide the table row.
+ $(settings.button).parents('tr:first').fadeOut('slow', function() {
+ $(this).addClass('content-multiple-removed-row');
+
+ // When there's only one delta, we don't want to remove any more items, so
+ // we hide the table column where the remove checkboxes and buttons are.
+ if ($('input.content-multiple-remove-checkbox', settings.tableDrag.table).not(':checked').length <= 1) {
+ $('th.content-multiple-remove-header', settings.tableDrag.table).fadeOut();
+ $('td.content-multiple-remove-cell', settings.tableDrag.table).fadeOut();
+ }
+
+ // Restripe table rows and display table changed warning.
+ settings.tableDrag.restripeTable();
+ settings.tableDrag.displayChangedWarning();
+
+ // Disable the busy flag after a while.
+ setTimeout(function() { Drupal.contentRemoveButtons._isBusy = false; }, 500);
+ });
+
+ // Hide the throbber after all remove callbacks have been processed.
+ $(settings.button).removeClass('content-multiple-throbber');
+};
+
+/**
+ * tableDrag override; Re-assign odd/even classes to all visible table rows.
+ */
+Drupal.tableDrag.prototype.restripeTable = function() {
+ var odd = true;
+ $('tr.draggable:visible', this.table).each(function() {
+ if (odd) {
+ $(this).filter('.even').removeClass('even').addClass('odd');
+ }
+ else {
+ $(this).filter('.odd').removeClass('odd').addClass('even');
+ }
+ odd = !odd;
+ });
+};
+
+/**
+ * tableDrag extension; Display table change warning when appropriate.
+ */
+Drupal.tableDrag.prototype.displayChangedWarning = function() {
+ if (this.changed == false) {
+ $(Drupal.theme('tableDragChangedWarning')).insertAfter(this.table).hide().fadeIn('slow');
+ this.changed = true;
+ }
+};
+
+/**
+ * Theme the 'remove' button.
+ *
+ * @param type
+ * 'field' or 'multigroup'.
+ * @param name
+ * The name of the field or multigroup.
+ * @param delta
+ * The delta of the field or fields in the subgroup.
+ */
+Drupal.theme.prototype.contentRemoveButton = function(type, name, delta) {
+ return '';
+};
diff -rupN theme/content-module.css theme/content-module.css
--- theme/content-module.css 2008-10-27 17:58:38.000000000 +0100
+++ theme/content-module.css 2009-01-20 13:41:18.000000000 +0100
@@ -27,6 +27,40 @@
margin:0;
}
+.content-multiple-remove-button {
+ display: block;
+ float: right;
+ height: 12px;
+ width: 16px;
+ margin: 2px 0 1px 0;
+ padding: 0;
+ background:transparent url(../images/button-remove.png) no-repeat 0 0;
+ border-bottom: #C2C9CE 1px solid;
+ border-right: #C2C9CE 1px solid;
+}
+.content-multiple-remove-button:hover {
+ background-position: 0 -12px;
+}
+.content-multiple-throbber,
+.content-multiple-throbber:hover {
+ background-image: url(../images/button-throbber.gif);
+ background-position: 0 0;
+ border: none;
+}
+.content-multiple-weight-header,
+.content-multiple-remove-header,
+.content-multiple-remove-cell,
+.content-multiple-table td.delta-order {
+ text-align: center;
+}
+html.js .content-multiple-weight-header,
+html.js .content-multiple-remove-header,
+html.js .content-multiple-removed-row,
+html.js .content-multiple-table td.delta-order,
+html.js .content-multiple-remove-checkbox {
+ display: none;
+}
+
.node-form .number {
display:inline;
width:auto;