diff -rupN content.module content.module
--- content.module 2009-01-10 01:10:06.000000000 +0100
+++ content.module 2009-01-14 13:46:57.000000000 +0100
@@ -481,10 +481,13 @@ function theme_content_multiple_values($
$header = array(
array(
'data' => t('!title: !required', array('!title' => $element['#title'], '!required' => $required)),
- 'colspan' => 2
+ 'colspan' => 2,
),
- t('Order'),
+ array('data' => t('Order'), 'class' => 'content-tabledrag-nojs-header'),
);
+ if ($field['multiple'] == 1) {
+ $header[] = array('data' => t('Remove'), 'class' => 'content-tabledrag-nojs-header');
+ }
$rows = array();
// Sort items according to '_weight' (needed when the form comes back after
@@ -492,23 +495,34 @@ 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']);
+ $remove_settings = $item['_remove']['#id'] .'|'. $element['#field_name'] .'|'. $delta;
+ if ($field['multiple'] == 1) {
+ $remove_element = drupal_render($item['_remove']) .'' . t('Remove') . '';
+ }
$cells = array(
array('data' => '', 'class' => 'content-multiple-drag'),
drupal_render($item),
array('data' => $delta_element, 'class' => 'delta-order'),
);
+ $row_class = 'draggable';
+ if ($field['multiple'] == 1) {
+ $cells[] = array('data' => $remove_element, 'class' => 'content-remove-button-cell');
+ if (!empty($item['_remove']['#default_value'])) {
+ $row_class .= ' content-removed-row';
+ }
+ }
$rows[] = array(
'data' => $cells,
- 'class' => 'draggable',
+ 'class' => $row_class,
);
}
@@ -517,6 +531,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 +732,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 +778,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,12 +901,25 @@ function content_field($op, &$node, $fie
* returns filtered and adjusted item array
*/
function content_set_empty($field, $items) {
+ // Check for remove requests for unlimited value fields of widgets that
+ // don't manage multiple values themselves.
+ $check_removed = $field['multiple'] == 1 && content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE;
+
// Filter out empty values.
$filtered = array();
$function = $field['module'] .'_content_is_empty';
foreach ((array) $items as $delta => $item) {
- if (!$function($item, $field)) {
- $filtered[] = $item;
+ if ($check_removed) {
+ // Get the item when it isn't flagged for removal.
+ if (!isset($item['_remove']) || empty($item['_remove'])) {
+ $filtered[] = $item;
+ }
+ }
+ else {
+ // Get the item if it isn't empty.
+ if (!$function($item, $field)) {
+ $filtered[] = $item;
+ }
}
}
@@ -914,7 +942,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 +1023,14 @@ function content_storage($op, $node) {
if (!isset($additions[$field_name])) {
$additions[$field_name] = array();
}
- $additions[$field_name][] = $item;
+
+ // 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-14 12:19:24.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(content_set_empty($field, $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,18 @@ 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-remove-checkbox content-remove-checkbox-'. $field_name),
+ '#default_value' => isset($items[$delta]['_remove']) ? $items[$delta]['_remove'] : 0,
+ );
+ }
+
$form_element[$delta] = array_merge($element, $defaults);
}
}
@@ -313,11 +327,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 +353,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-14 11:33:09.000000000 +0100
@@ -0,0 +1,112 @@
+// $Id$
+
+/**
+ * Manipulation of content remove buttons.
+ *
+ * An onClick event handler is bound to each remove button, from where
+ * onRemove events are invoked before the field widget is hidden.
+ * There is a default onRemove handler that is executed first in the chain,
+ * and it is used to enable the checked attribute of the remove checkbox.
+ * Custom onRemove handlers are executed later, and can be used to perform
+ * additional processing, or even disable the checked attribute of the
+ * remove checkbox. The field widget will only be hidden if the checked
+ * attribute of the remove checkbox is still enabled after all onRemove
+ * handlers have been executed.
+ *
+ * Custom onRemove handlers may be attached to remove buttons as follows:
+ *
+ *
+ * $('.content-remove-button').not('.myclass-processed').each(function() {
+ * // Make sure the element is not processed more than once.
+ * $(this).addClass('.myclass-processed');
+ * // Attach custom onRemove handler.
+ * $(this)..bind('remove', myOnRemoveHandler);
+ * });
+ *
+ *
+ * onRemove handlers are invoked with the following arguments:
+ * event - The onClick event object.
+ * settings - An object with the following properties:
+ * 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.
+ * field_name - The name of the field.
+ * delta - The delta of the field item.
+ *
+ * Custom onRemove handlers may uncheck the checkbox element as follows:
+ *
+ *
+ * $('input[@id='+ settings.checkbox_id +']').attr('checked', '');
+ *
+ */
+Drupal.behaviors.contentRemoveButtons = function() {
+ $('.content-remove-button').not('.content-remove-button-processed').each(function() {
+ // Make sure the element is not processed more than once, then
+ // attach our onClick and default onRemove handlers.
+ $(this).addClass('.content-remove-button-processed')
+ .bind('click', Drupal.contentRemoveButtons.onClick)
+ .bind('remove', Drupal.contentRemoveButtons.onRemove);
+ });
+};
+
+Drupal.contentRemoveButtons = {};
+
+/**
+ * onClick handler for content remove buttons.
+ */
+Drupal.contentRemoveButtons.onClick = function(event) {
+ var button = this, settings = $(button).attr('rel').split('|');
+ var checkbox_id = settings[0], field_name = settings[1], delta = settings[2];
+
+ // Display the throbber while onRemove handlers are being processed.
+ $(button).addClass('content-remove-button-throbber');
+
+ // Trigger all onRemove events attached to the button.
+ $(button).trigger('remove', [event, {
+ checkbox_id: checkbox_id,
+ checkbox: $('#'+ checkbox_id).get(0),
+ button: button,
+ field_name: field_name,
+ delta: delta
+ }]);
+
+ // Hide the throbber after all onRemove handlers have been processed.
+ $(button).removeClass('content-remove-button-throbber');
+
+ // Hide the table row of the widget, but only if the checkbox element is
+ // still checked after all onRemove handlers have been executed.
+ if ($('input[@id='+ checkbox_id +']:checked').val()) {
+ $(button).parents('tr:first').fadeOut('slow');
+ }
+
+ // 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-remove-checkbox-'+ field_name).not(':checked').length <= 1) {
+ $(button).parents('table.content-multiple-table:first').each(function() {
+ $(this).find('th.content-remove-header').fadeOut();
+ $(this).find('td.content-remove-cell').fadeOut();
+ });
+ }
+
+ // Display table change warning when appropriate.
+ $(button).parents('table.content-multiple-table:first').each(function() {
+ var base = $(this).attr('id');
+ if (typeof Drupal.tableDrag[base] == 'object') {
+ var tableDrag = Drupal.tableDrag[base];
+ if (tableDrag.changed == false) {
+ $(Drupal.theme('tableDragChangedWarning')).insertAfter(tableDrag.table).hide().fadeIn('slow');
+ tableDrag.changed = true;
+ }
+ }
+ });
+
+ // Actually, do not let the click event proceed.
+ return false;
+};
+
+/**
+ * onRemove handler for content remove buttons.
+ */
+Drupal.contentRemoveButtons.onRemove = function(event, settings) {
+ $('input[@id='+ settings.checkbox_id +']').attr('checked', 'checked');
+};
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-14 12:14:54.000000000 +0100
@@ -23,6 +23,43 @@
padding-right:.5em;
}
+.content-tabledrag-nojs-header,
+.content-remove-button-cell,
+.content-multiple-table td.delta-order {
+ text-align: center;
+}
+html.js .content-removed-row,
+html.js .content-tabledrag-nojs-header,
+html.js .content-multiple-table td.delta-order {
+ display: none;
+}
+.content-remove-button {
+ display: none;
+ 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-remove-button-throbber {
+ background-image: url(../images/button-throbber.gif);
+}
+.content-remove-button:hover {
+ background-position: 0 -12px;
+}
+.content-remove-button span {
+ display: none;
+}
+html.js .content-remove-button {
+ display: block;
+}
+html.js .content-remove-checkbox {
+ display: none;
+}
+
.node-form .content-add-more .form-submit{
margin:0;
}