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; }