Index: content.node_form.js =================================================================== --- content.node_form.js (revision 0) +++ content.node_form.js (revision 0) @@ -0,0 +1,255 @@ +// $Id: content.node_form.js,v 1.1.2.1 2009/06/06 23:44:56 markuspetrux Exp $ + +/** + * Private namespace for local methods. + */ +Drupal.contentRemoveButtons = Drupal.contentRemoveButtons || {}; + +/** + * 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. + */ +Drupal.behaviors.contentRemoveButtons = function(context) { + var self = Drupal.contentRemoveButtons; + + $('table.content-multiple-table', context).not('.content-remove-buttons-processed').addClass('content-remove-buttons-processed').each(function() { + var table = this, tableDrag = Drupal.tableDrag[$(table).attr('id')]; + + // Replace remove checkboxes with buttons. + $('input.content-multiple-remove-checkbox', table).each(function() { + var $checkbox = $(this), $row = $checkbox.parents('tr:first'); + var isRemoved = $checkbox.attr('checked'); + var $button = $(Drupal.theme('contentRemoveButton', tableDrag.getRemoveButtonTitle(isRemoved))); + + // Bind the onClick event to the remove button. + $button.bind('click', function(event) { + self.onClick($button, $checkbox, $row, tableDrag); + return false; + }); + + // Attach the new button to the DOM tree. + $checkbox.parent().append($button); + + // If the row is removed, then hide the contents of the cells and show + // the removed warning on the cell next to the drag'n'drop cell. + if (isRemoved) { + self.getCellWrappers($row).hide(); + self.showRemovedWarning($row, tableDrag); + + // FAPI not rendering the form on errors - case #1: + // If the form has been submitted and any error was found, FAPI will + // send back the same exact form that was submitted to show the error + // messages, but it will not invoke the rendering engine which is where + // we actually assign the removed class to the row, so we need to check + // this situation here and add the class if it is not present. + if (!$row.hasClass('content-multiple-removed-row')) { + $row.addClass('content-multiple-removed-row'); + } + } + else { + // FAPI not rendering the form on errors - case #2: + // Similar issue than #1, but this time caused when user removes an + // item, previews, FAPI renders the new form with the removed class, + // then user changes anything in the form that causes an error, and + // also restores the previously removed item. This time, FAPI will + // send the form validation error with the item not flagged for removal + // but having the removed class that was present when the form was + // rendered in the previous step. So we need to remove this class here, + // if present, because the item is not really flagged for removal. + if ($row.hasClass('content-multiple-removed-row')) { + $row.removeClass('content-multiple-removed-row'); + } + } + }); + }); +}; + +/** + * onClick handler for remove buttons. + * + * @param $button + * The jQuery object of the remove button. + * @param $checkbox + * The jQuery object of the remove checkbox. + * @param $row + * The jQuery object of the table row. + * @param tableDrag + * The tableDrag object where the row is. + */ +Drupal.contentRemoveButtons.onClick = function($button, $checkbox, $row, tableDrag) { + var self = Drupal.contentRemoveButtons; + + // Prevent the user from firing this event while another one is still being + // processed. This flag is (should be) restored at end of animations. + // Note that this technique is required because the browser may experience + // delays while performing the animation, for whatever reason, and if this + // process it fired more than once at the same time for the same row, then + // it may cause unexpected behavior because the state of the elements being + // manipulated would be unknown. + if ($row.animating) { + return; + } + $row.animating = true; + + // Toggle the state of the checkbox. + var isRemoved = !$checkbox.attr('checked'); + $checkbox.attr('checked', isRemoved); + + // Toggle the row class. + if (isRemoved) { + $row.addClass('content-multiple-removed-row'); + } + else { + $row.removeClass('content-multiple-removed-row'); + } + + // Toggle the button title. + $button.attr('title', tableDrag.getRemoveButtonTitle(isRemoved)); + + // Get the list of cell wrappers in this row. + var $cellWrappers = self.getCellWrappers($row); + + // If for whatever reason this row doesn't have cells with elements, + // then we are done, but we still need to reset the global busy flag + // and display the tableDrag changed warning. + if (!$cellWrappers.size()) { + tableDrag.displayChangedWarning(); + $row.animating = false; + return; + } + + // Toggle the visible state of the row cells. + $cellWrappers.each(function() { + var $cellWrapper = $(this); + + // Drop the removed warning during restore operation. + if (!isRemoved) { + self.hideRemovedWarning($row); + } + + // Toggle the visibility state of the contents of cells. + $cellWrapper.animate({opacity: (isRemoved ? 'hide' : 'show')}, 'fast', function() { + var $cell = $cellWrapper.parent(); + + // Show the removed warning during remove operation. + if (isRemoved && $cell.prev(':first').hasClass('content-multiple-drag')) { + self.showRemovedWarning($row, tableDrag); + } + + // Disable the busy flag when animation of last cell has finished. + if ($cell.next(':first').hasClass('delta-order')) { + tableDrag.displayChangedWarning(); + $row.animating = false; + } + }); + }); +}; + +/** + * Show the removed warning on the given row. + * + * @param $row + * The jQuery object of the table row. + * @param tableDrag + * The tableDrag object where the row is. + */ +Drupal.contentRemoveButtons.showRemovedWarning = function($row, tableDrag) { + $('.content-multiple-drag', $row).next(':first').append(Drupal.theme('contentRemovedWarning', tableDrag.getRemovedWarning())); +}; + +/** + * Hide the removed warning from the given row. + * + * @param $row + * The jQuery object of the table row. + */ +Drupal.contentRemoveButtons.hideRemovedWarning = function($row) { + if ($('.content-multiple-removed-warning', $row).size()) { + $('.content-multiple-removed-warning', $row).remove(); + } +}; + +/** + * Get cell wrappers for the given row. + * + * @param $row + * The jQuery object of the table row. + */ +Drupal.contentRemoveButtons.getCellWrappers = function($row) { + // Create cell wrappers if this row has not already been processed. + if (!$('.content-multiple-cell-content-wrapper', $row).size()) { + // Wrap the contents of all cells (except the drag'n'drop, weight and + // remove button cells) with a dummy block element. This operation makes + // animations faster because we just need to show/hide a single element + // per cell, and it also prevents from creating more than one warning + // element per row. + $row.children('td:not(.content-multiple-drag):not(.delta-order):not(.content-multiple-remove-cell)').each(function() { + var $cell = $(this); + $cell.wrapInner('
'); + }); + } + return $('.content-multiple-cell-content-wrapper', $row); +}; + +/** + * 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; + } +}; + +/** + * Get the title of the remove button. + * + * This method is an extension of the tableDrag class. This means a separate + * module can override this method for a particular tableDrag instance. For + * example, the multigroup module can change the text to read 'Remove this + * group of items', another module could change it to 'Remove this image', + * and so on... + * To override this function: + * + * @code + * var tableId = $(table).attr('id'); + * Drupal.tableDrag[tableId].getRemoveButtonTitle = function(isRemoved) { + * return (isRemoved ? Drupal.t('Restore this foo') : Drupal.t('Remove this foo')); + * }; + * @endcode + * + * @param isRemoved + * A flag that indicates the state of the button. + */ +Drupal.tableDrag.prototype.getRemoveButtonTitle = function(isRemoved) { + return (isRemoved ? Drupal.t('Restore this item') : Drupal.t('Remove this item')); +}; + +/** + * Get the item removed warning. + * + * This method is an extension of the tableDrag class. It can be overridden by + * a separate module. See getRemoveButtonTitle() for further information. + */ +Drupal.tableDrag.prototype.getRemovedWarning = function() { + return Drupal.t('Removed'); +}; + +/** + * Theme the remove button. + */ +Drupal.theme.prototype.contentRemoveButton = function(title) { + return ''; +}; + +/** + * Theme the item removed warning. + */ +Drupal.theme.prototype.contentRemovedWarning = function(warning) { + return '
'+ warning +'
'; +}; Index: includes/content.admin.inc =================================================================== --- includes/content.admin.inc (revision 1263) +++ includes/content.admin.inc (working copy) @@ -147,6 +147,7 @@ if (module_exists('fieldgroup')) { $groups = fieldgroup_groups($type['type']); $group_types = fieldgroup_types(); + $plain_tree = _fieldgroup_plain_tree($groups); $group_options = _fieldgroup_groups_label($type['type']); // Add the ability to group under the newly created row. $group_options['_add_new_group'] = '_add_new_group'; @@ -194,6 +195,8 @@ // Groups. foreach ($groups as $name => $group) { + $current_group_options = $plain_tree; + unset($current_group_options[$name]); $weight = $group['weight']; $form[$name] = array( 'label' => array('#value' => check_plain($group['label'])), @@ -202,17 +205,24 @@ 'configure' => array('#value' => l(t('Configure'), 'admin/content/node-type/'. $type['url_str'] .'/groups/'. $group['group_name'])), 'remove' => array('#value' => l(t('Remove'), 'admin/content/node-type/'. $type['url_str'] .'/groups/'. $group['group_name'] .'/remove')), 'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3), - 'parent' => array('#type' => 'hidden', '#default_value' => ''), + 'parent' => array('#type' => 'select', '#options' => $current_group_options, '#default_value' => ''), + 'prev_parent' => array('#type' => 'hidden', '#value' => ''), 'hidden_name' => array('#type' => 'hidden', '#default_value' => $group['group_name']), - '#root' => TRUE, '#row_type' => 'group', 'group' => array('#type' => 'value', '#value' => $group), ); // Adjust child fields rows. - foreach ($group['fields'] as $field_name => $field) { - $form[$field_name]['parent']['#default_value'] = $name; - $form[$field_name]['prev_parent']['#value'] = $name; + if (isset($group['fields'])) { + foreach ($group['fields'] as $field_name => $field) { + $form[$field_name]['parent']['#default_value'] = $name; + $form[$field_name]['prev_parent']['#value'] = $name; + } } + + // Adjust child group rows + $form[$name]['parent']['#default_value'] = $group['parent_group_name']; + $form[$name]['prev_parent']['#value'] = $group['parent_group_name']; + $form['#group_rows'][] = $name; $weights[] = $weight; } @@ -317,6 +327,9 @@ // Additional row : add new group. if (!empty($group_types)) { + $current_group_options = $group_options; + $options = fieldgroup_types(); + unset($current_group_options[_add_new_group]); $weight++; $name = '_add_new_group'; $form[$name] = array( @@ -346,9 +359,9 @@ '#value' => 'standard', ), 'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3), - 'parent' => array('#type' => 'hidden', '#default_value' => ''), + 'parent' => array('#type' => 'select', '#options' => $current_group_options, '#default_value' => ''), + 'prev_parent' => array('#type' => 'hidden', '#value' => ''), 'hidden_name' => array('#type' => 'hidden', '#default_value' => $name), - '#root' => TRUE, '#add_new' => TRUE, '#row_type' => 'add_new_group', ); @@ -662,6 +675,7 @@ $form[$name] = array( 'human_name' => array('#value' => check_plain($group['label'])), 'weight' => array('#type' => 'value', '#value' => $weight), + 'parent' => array('#type' => 'value', '#value' => ''), ); if ($contexts_selector == 'basic') { $form[$name]['label'] = array( @@ -686,6 +700,8 @@ foreach ($group['fields'] as $field_name => $field) { $form[$field_name]['parent']['#value'] = $name; } + $form[$name]['parent']['#value'] = $group['parent_group_name']; + $form[$name]['group']['#value']['depth'] = $group['depth']; } $form['submit'] = array('#type' => 'submit', '#value' => t('Save')); @@ -1667,6 +1683,25 @@ } } } + + // Nested fieldgroup + if (module_exists('fieldgroup')) { + // readjust the depth and parenting of fieldgroup + foreach ($group_rows as $name) { + if ($parent = $form[$name]['parent']['#value']) { + $form[$name]['#depth'] = $form[$parent]['#depth'] + 1; + $dummy[$parent][$name] = $dummy[$name]; + unset($dummy[$name]); + } + } + // readjust the depth + foreach ($field_rows as $name) { + if ($parent = $form[$name]['parent']['#value']) { + $form[$name]['#depth'] = $form[$parent]['#depth'] + 1; + } + } + } + return $dummy ? explode(' ', trim(drupal_render($dummy))) : array(); } Index: includes/content.node_form.inc =================================================================== --- includes/content.node_form.inc (revision 1263) +++ includes/content.node_form.inc (working copy) @@ -325,12 +325,7 @@ drupal_alter('form', $form_element, array(), 'content_add_more_js'); // Add the new element at the right place in the (original, unbuilt) form. - if (module_exists('fieldgroup') && ($group_name = _fieldgroup_field_get_group($type['type'], $field_name))) { - $form[$group_name][$field_name] = $form_element[$field_name]; - } - else { - $form[$field_name] = $form_element[$field_name]; - } + content_set_form_element($field_name, $type['type'], $form, $form_element[$field_name]); // Save the new definition of the form. $form_state['values'] = array(); @@ -348,7 +343,7 @@ $form = form_builder($_POST['form_id'], $form, $form_state); // Render the new output. - $field_form = (!empty($group_name)) ? $form[$group_name][$field_name] : $form[$field_name]; + $field_form = content_get_form_element($field_name, $type['type'], $form); // We add a div around the new content to receive the ahah effect. $field_form[$delta]['#prefix'] = '
'. (isset($field_form[$delta]['#prefix']) ? $field_form[$delta]['#prefix'] : ''); $field_form[$delta]['#suffix'] = (isset($field_form[$delta]['#suffix']) ? $field_form[$delta]['#suffix'] : '') .'
'; @@ -372,3 +367,74 @@ print drupal_to_js(array('status' => TRUE, 'data' => $output)); exit; } + +/** + * Store an element into a form. + * + * @param $name + * The field name. + * @param $type + * The content type where the field instance belongs to. + * @param $form + * The form to store this field element into. + * @param $element + * The form element to store. + */ +function content_set_form_element($name, $type, &$form, $element, $is_group = FALSE) { + if (module_exists('fieldgroup') && ($parents = _fieldgroup_field_get_parents($type, $name, $is_group))) { + foreach (module_implements('fieldgroup_parents_alter') as $module) { + $parents = call_user_func($module .'_fieldgroup_parents_alter', $form, $parents, $type, $name); + } + $reference = &$form; + if ($is_group) { + array_shift($parents); + $parents = array_reverse($parents); + } + else { + $parents = array_reverse($parents); + } + + foreach (array_values($parents) as $group_name) { + $reference = &$reference[$group_name]; + } + + $reference[$name] = $element; + + } + else { + $form[$name] = $element; + } +} + +/** + * Retrieve an element from a form. + * + * @param $name + * The field name. + * @param $type + * The content type where the field instance belongs to. + * @param $form + * The form to retrieve this field element from. + */ +function content_get_form_element($name, $type, $form, $is_group = FALSE) { + if (module_exists('fieldgroup') && ($parents = _fieldgroup_field_get_parents($type, $name, $is_group))) { + foreach (module_implements('fieldgroup_parents_alter') as $module) { + $parents = call_user_func($module .'_fieldgroup_parents_alter', $form, $parents, $type, $name); + } + $reference = &$form; + if ($is_group) { + array_shift($parents); + $parents = array_reverse($parents); + } + else { + $parents = array_reverse($parents); + } + + foreach (array_values($parents) as $group_name) { + $reference = &$reference[$group_name]; + } + + return $reference[$name]; + } + return $form[$name]; +} Index: modules/content_multigroup/README.txt =================================================================== --- modules/content_multigroup/README.txt (revision 1263) +++ modules/content_multigroup/README.txt (working copy) @@ -1,4 +1,100 @@ -; $Id: README.txt,v 1.1.2.4 2009/06/04 18:57:59 yched Exp $ +; $Id: README.txt,v 1.1.2.4.2.3 2009/08/10 03:53:24 markuspetrux Exp $ -Ongoing work on the multigroup module has moved to the experimental -CCK 3.0 branch. +CONTENTS OF THIS FILE +===================== +- USING MULTIGROUPS +- FIELDS AND WIDGETS THAT WORK IN MULTIGROUPS +- VIEWS INTEGRATION +- TROUBLESHOOTING + + +USING MULTIGROUPS +================= + +The Multigroup group treats all included fields like a single field, keeping +the related delta values of all included fields synchronized. + +To use a Multigroup, create a new group, make it the 'Multigroup' type, set +the number of multiple values for all the fields in the Multigroup, and drag +into it the fields that should be included. + +All fields in the Multigroup will automatically get the group setting for +multiple values. On the node form, the group is rearranged to keep the delta +values for each field in a single drag 'n drop group, by transposing the +normal array(group_name => field_name => delta => value) into +array(group_name => delta => field_name => value). + +During validation and submission, the field values are restored to their +normal positions. + + +FIELDS AND WIDGETS THAT WORK IN MULTIGROUPS +=========================================== + +All fields that allow the Content module to handle their multiple values should +work here. Fields that handle their own multiple values will not be allowed +into Multigroups unless they implement hook_content_multigroup_allowed_widgets() +to add their widgets to the allowed widget list. Example: + + @code + function MODULE_content_multigroup_allowed_widgets() { + return array('WIDGET_NAME_1', 'WIDGET_NAME_2', ...); + } + @endcode + +All fields that allow the Content module to handle their multiple values +should work correctly when a field placed on a Multigroup is moved off, to a +normal field group, or to the top level of the form. Fields that handle their +own multiple values which may store different results in Multigroup and +standard groups should implement hook_content_multigroup_no_remove_widgets() +to add their widgets to the list of widgets that cannot be removed from +Multigroups. Example: + + @code + function MODULE_content_multigroup_no_remove_widgets() { + return array('WIDGET_NAME_1', 'WIDGET_NAME_2', ...); + } + @endcode + +The Content Taxonomy module [1] is an example where it implements the previous +hooks for a few widgets. + +[1] http://drupal.org/project/content_taxonomy + +If a simple array of widgets is not sufficient to test whether this action +will work, modules can implement hook_content_multigroup_allowed_in() +and hook_content_multigroup_allowed_out() to intervene. Both hooks should +return an array as in the following example: + + @code + function MODULE_content_multigroup_allowed_in() { + return array( + 'allowed' => FALSE, + 'message' => t('This change is not allowed. Reason here...'), + ); + } + @endcode + +Custom code and modules that add fields to groups outside of the UI should +use content_multigroup_allowed_in() and content_multigroup_allowed_out() to +test whether fields are allowed in or out of a Multigroup. These functions +can be located in content_multigroup.admin.inc. + + +VIEWS INTEGRATION +================= + +For each multigroup, there is a new filter under "Content multigroup" category +in Views that provides a method to synchronize fields by delta. + + +TROUBLESHOOTING +=============== + +The most likely cause of problems with field modules not working in multigroup +is if they wipe out #element_validate with their own validation functions, or +they hard-code assumptions into submit or validation processes that the form +is structured in the usual field => delta => value order instead of allowing +for the possibility of a different structure. See Nodereference for an example +of a field that handles validation without making assumptions about the form +structure. Index: modules/content_multigroup/content_multigroup.admin.inc =================================================================== --- modules/content_multigroup/content_multigroup.admin.inc (revision 0) +++ modules/content_multigroup/content_multigroup.admin.inc (revision 0) @@ -0,0 +1,525 @@ + t('N/A'), + 1 => t('Unlimited'), + 0 => 1) + drupal_map_assoc(range(2, 10)); +} + +/** + * Validation for creating/moving fields and groups on the + * Manage Fields screen. + */ +function content_multigroup_field_overview_form_validate($form, &$form_state) { + $form_values = $form_state['values']; + + $type_name = $form['#type_name']; + $fields = array(); + $groups = array(); + + $group = $form_values['_add_new_group']; + if (array_filter(array($group['label'], $group['group_name']))) { + $group['settings'] = field_group_default_settings($group['group_type']); + $validation = fieldgroup_validate_name($group, $form['#type_name']); + + // If there's something wrong with the new group, + // don't bother doing any more validation, further + // processing will be stopped by the fieldgroup module. + if (!empty($validation['errors'])) { + return; + } + $group['group_name'] = $validation['group_name']; + $new_group_name = $group['group_name']; + $groups['_add_new_group'] = $group; + } + + // See if we have fields moving into or out of a Multigroup. + // Set any fields to use the new name here so they will get processed + // correctly by the fieldgroup module when saved. + $group_rows = array(); + foreach ($form_values as $key => $values) { + if ($values['parent'] == '_add_new_group') { + $values['parent'] = $new_group_name; + $form_values[$key] = $values; + } + + if (!empty($form[$key]['#row_type']) && $form[$key]['#row_type'] == 'group') { + // Gather up info about all groups. + $group_name = $form_values[$key]['group']['group_name']; + $groups[$group_name] = $form_values[$key]['group']; + $group_rows[$group_name] = $group_name; + } + if (!empty($form[$key]['#row_type']) && $form[$key]['#row_type'] == 'field') { + if ($values['prev_parent'] != $values['parent']) { + // Gather up fields that have moved in or out of a group. + $fields[$key] = $form_values[$key]['field']; + } + } + } + + $rebuild = FALSE; + + foreach ($groups as $key => $values) { + if (in_array($key, $group_rows)) { + if (!empty($groups[$key]['parent']) && $groups[$groups[$key]['parent']]['group_type'] == 'multigroup') { + $error_message = t('You cannot place any kind of group inside a multigroup. ' . $key . ' was moved back to where it started.'); + form_set_value($form[$key]['weight'], $form[$key]['weight']['#default_value'], $form_state); + form_set_value($form[$key]['parent'], $form[$key]['parent']['#default_value'], $form_state); + drupal_set_message($error_message, 'error'); + } + } + } + + foreach ($fields as $field_name => $field) { + $new_group = $form_values[$field_name]['parent']; + $old_group = $form_values[$field_name]['prev_parent']; + if (!empty($new_group) && isset($groups[$new_group]) && $groups[$new_group]['group_type'] == 'multigroup') { + $allowed_in = content_multigroup_allowed_in($field, $groups[$new_group]); + if (!$allowed_in['allowed']) { + form_set_error($field_name, $allowed_in['message']); + } + else { + if (!empty($allowed_in['message'])) { + drupal_set_message($allowed_in['message']); + } + module_load_include('inc', 'content', 'includes/content.crud'); + $content_type = content_types($type_name); + $group_multiple = $groups[$new_group]['settings']['multigroup']['multiple']; + $multiple_values = content_multigroup_multiple_values(); + $field = $content_type['fields'][$field_name]; + $field['multiple'] = $group_multiple; + $field = content_field_instance_collapse($field); + content_field_instance_update($field, FALSE); + $rebuild = TRUE; + drupal_set_message(t('The field %field has been updated to use %multiple values, to match the multiple value setting of the Multigroup %group.', array( + '%field' => $field['label'], '%multiple' => $multiple_values[$group_multiple], '%group' => $groups[$new_group]['label']))); + } + } + elseif (!empty($old_group) && isset($groups[$old_group]) && $groups[$old_group]['group_type'] == 'multigroup') { + $allowed_out = content_multigroup_allowed_out($field, $groups[$old_group]); + if (!$allowed_out['allowed']) { + form_set_error($field_name, $allowed_out['message']); + } + elseif (!empty($allowed_out['message'])) { + drupal_set_message($allowed_out['message']); + } + } + } + + // Clear caches and rebuild menu only if any field has been updated. + if ($rebuild) { + content_clear_type_cache(TRUE); + menu_rebuild(); + } +} + +/** + * Helper function for deciding if a field is + * allowed into a Multigroup. + */ +function content_multigroup_allowed_in($field, $group) { + if ($group['group_type'] != 'multigroup') { + return array('allowed' => TRUE, 'message' => ''); + } + + // We can't allow fields with more multiple values than the group has + // to be moved into it. + $max_existing = content_max_delta($field['field_name']); + $group_multiple = $group['settings']['multigroup']['multiple']; + $multiple_values = content_multigroup_multiple_values(); + if ($group_multiple != 1 && $max_existing > $group_multiple) { + return array( + 'allowed' => FALSE, + 'message' => t('This change is not allowed. The field %field already has %multiple values in the database but the group %group only allows %group_max. Making this change would result in the loss of data.', array('%field' => $field['widget']['label'], '%multiple' => $max_existing, '%group' => $group['label'], '%group_max' => $multiple_values[$group_multiple])) + ); + } + + // Fields that handle their own multiple values may not have the same values + // in Multigroup fields and normal fields. We don't know if they will work or not. + + // Adding a hook here where widgets that handle their own multiple values + // that will work correctly in Multigroups can allow their fields in. + + if (content_handle('widget', 'multiple values', $field) != CONTENT_HANDLE_CORE) { + $allowed_widgets = array( + 'optionwidgets_select', + 'optionwidgets_buttons', + 'optionwidgets_onoff', + 'nodereference_buttons', + 'nodereference_select', + 'userreference_buttons', + 'userreference_select', + ); + $allowed_widgets = array_merge($allowed_widgets, module_invoke_all('content_multigroup_allowed_widgets')); + if (!in_array($field['widget']['type'], $allowed_widgets)) { + return array( + 'allowed' => FALSE, + 'message' => t('This change is not allowed. The field %field handles multiple values differently than the Content module. Making this change could result in the loss of data.', array('%field' => $field['widget']['label'])) + ); + } + } + + // Allow other modules to intervene. + // Any failure will prevent this action. + foreach (module_implements('content_multigroup_allowed_in') as $module) { + $function = $module .'_content_multigroup_allowed_in'; + $result = $function($field, $group); + if ($result['allowed'] === FALSE) { + return array('allowed' => FALSE, 'message' => $result['message']); + } + } + + $message = t('You are moving the field %field into a Multigroup.', array('%field' => $field['widget']['label'])); + return array('allowed' => TRUE, 'message' => $message); +} + +/** + * Helper function for deciding if a field is + * allowed out of a Multigroup. + */ +function content_multigroup_allowed_out($field, $group) { + if ($group['group_type'] != 'multigroup') { + return array('allowed' => TRUE, 'message' => ''); + } + // Optionwidgets do not behave the same in a Multigroup field as out of it. + // In a Multigroup the same option can be selected multiple times, + // but that is not possible in a normal group. + + // Adding a hook here where widgets that handle their own multiple values + // can indicate their fields should not be removed from Multigroups. + + $max_existing = content_max_delta($field['field_name']); + $no_remove_widgets = array( + 'optionwidgets_select', + 'optionwidgets_buttons', + 'optionwidgets_onoff', + 'nodereference_buttons', + 'nodereference_select', + 'userreference_buttons', + 'userreference_select', + ); + $no_remove_widgets = array_merge($no_remove_widgets, module_invoke_all('content_multigroup_no_remove_widgets')); + if (in_array($field['widget']['type'], $no_remove_widgets) && $max_existing > 0) { + return array( + 'allowed' => FALSE, + 'message' => t('This change is not allowed. The field %field already has data created and uses a widget that stores data differently in a Standard group than in a Multigroup. Making this change could result in the loss of data.', array('%field' => $field['widget']['label'])) + ); + } + + // Allow other modules to intervene. + // Any failure will prevent this action. + foreach (module_implements('content_multigroup_allowed_out') as $module) { + $function = $module .'_content_multigroup_allowed_out'; + $result = $function($field, $group); + if ($result['allowed'] === FALSE) { + return array('allowed' => FALSE, 'message' => $result['message']); + } + } + + $message = t('You are moving the field %field out of a Multigroup.', array('%field' => $field['widget']['label'])); + return array('allowed' => TRUE, 'message' => $message); +} + +/** + * Alter the basic field settings form. + * + * It should not be possible to choose a widget type that is not compatible + * with multigroups. + */ +function content_multigroup_field_basic_form(&$form, &$form_state) { + $field_name = $form['basic']['field_name']['#value']; + $type_name = $form['type_name']['#value']; + + // Ignore this field if it is not part of a field group. + if (!($group_name = fieldgroup_get_group($type_name, $field_name))) { + return; + } + + // Retrieve information about the group the field is in. + $groups = fieldgroup_groups($type_name); + $group = $groups[$group_name]; + + // Ignore this field if it is not part of a multigroup. + if ($group['group_type'] != 'multigroup') { + return; + } + + // Retrieve information about the field itself. + $field = content_fields($field_name, $type_name); + + // Check if the widget can be moved out of the multigroup. + $allowed_out = content_multigroup_allowed_out($field, $group); + if (!$allowed_out['allowed']) { + $form['basic']['widget_type']['#disabled'] = TRUE; + $form['basic']['widget_type']['#suffix'] = '
'. t('The widget type cannot be changed because the field %field already has data created and this widget stores data differently in a Standard group than in a Multigroup. Allowing this change could result in the loss of data.', array('%field' => $field['widget']['label'])) .'
'; + return; + } + + // Remove from the list of available widgets those that are not + // compatible with multigroups. + $widget_types = _content_widget_types(); + foreach (array_keys($form['basic']['widget_type']['#options']) as $widget_type) { + if ($field['widget']['type'] != $widget_type) { + $field_copy = $field; + $field_copy['widget']['type'] = $widget_type; + $field_copy['widget']['module'] = $widget_types[$widget_type]['module']; + $allowed_in = content_multigroup_allowed_in($field_copy, $group); + if (!$allowed_in['allowed']) { + unset($form['basic']['widget_type']['#options'][$widget_type]); + } + } + } +} + +/** + * Alter the "Display fields" form. + * + * Add an additional selector for setting multigroup field display format. + */ +function content_multigroup_display_overview_form(&$form, &$form_state) { + + $type_name = $form['#type_name']; + $contexts_selector = $form['#contexts']; + + // Gather type information. + $content_type = content_types($type_name); + + // The content module stops building the form if the type has no fields. + if (empty($content_type['fields'])) { + return; + } + + $groups = array(); + if (module_exists('fieldgroup')) { + $groups = fieldgroup_groups($type_name); + } + $contexts = content_build_modes($contexts_selector); + + // Multigroups, extra values. + $label_options = array( + 'above' => t('Above'), + 'hidden' => t(''), + ); + $options = array( + 'simple' => t('Simple'), + 'fieldset' => t('Fieldset'), + 'fieldset_collapsible' => t('Fieldset - collapsible'), + 'fieldset_collapsed' => t('Fieldset - collapsed'), + 'hr' => t('Horizontal line'), + 'table-single' => t('Table - Single column'), + 'table-multiple' => t('Table - Multiple columns'), + ); + foreach ($groups as $group_name => $group) { + if ($group['group_type'] != 'multigroup') { + continue; + } + $subgroup_settings = isset($group['settings']['multigroup']['subgroup']) ? $group['settings']['multigroup']['subgroup'] : array(); + + $subgroup_name = $group_name .'_subgroup'; + $form['#fields'] = array_merge(array($subgroup_name), $form['#fields']); + $form[$subgroup_name] = array( + 'human_name' => array('#value' => t('[Subgroup format]')), + 'weight' => array('#type' => 'value', '#value' => -20), + 'parent' => array('#type' => 'value', '#value' => $group_name), + 'subgroup' => array('#type' => 'value', '#value' => 1), + ); + if ($contexts_selector == 'basic') { + $form[$subgroup_name]['label'] = array( + '#type' => 'select', + '#options' => $label_options, + '#default_value' => isset($subgroup_settings['label']) ? $subgroup_settings['label'] : 'above', + ); + } + foreach ($contexts as $key => $title) { + $form[$subgroup_name][$key]['format'] = array( + '#type' => 'select', + '#options' => $options, + '#default_value' => isset($subgroup_settings[$key]['format']) ? $subgroup_settings[$key]['format'] : 'fieldset', + ); + $form[$subgroup_name][$key]['exclude'] = array('#type' => 'value', '#value' => 0); + } + } + + $form['#submit'] = array_merge(array('content_multigroup_display_overview_form_submit'), $form['#submit']); +} + +/** + * Submit handler for the display overview form. + * + * Do this in pre_save so we catch it before the content module + * tries to use our 'field'. + */ +function content_multigroup_display_overview_form_submit($form, &$form_state) { + $groups = fieldgroup_groups($form['#type_name']); + $reset_cache = FALSE; + + // Find any subgroups we inserted into the display fields form, + // save our settings, and remove them from $form_state. + foreach ($form_state['values'] as $key => $values) { + if (in_array($key, $form['#fields']) && !empty($values['parent']) && !empty($values['subgroup'])) { + $group_name = $values['parent']; + $group = $groups[$group_name]; + unset($values['subgroup'], $values['parent']); + + // We have some numeric keys here, so we can't use array_merge. + foreach ($values as $k => $v) { + $group['settings']['multigroup']['subgroup'][$k] = $v; + } + + // Update the group information in the database. Note that + // 'fieldgroup_data' in cache tables are also cleared here, + // but we need to reset static caches of fieldgroup_groups(). + fieldgroup_save_group($form['#type_name'], $group); + $reset_cache = TRUE; + + // Remove the subgroup from $form_state. + unset($form_state['values'][$key]); + } + } + if ($reset_cache) { + fieldgroup_groups('', FALSE, TRUE); + } +} + +/** + * Alter the Fieldgroup edit form to add Multigroup settings. + */ +function content_multigroup_group_edit_form(&$form, &$form_state) { + $type_name = $form['#content_type']['type']; + $group_name = $form['group_name']['#default_value']; + + $content_type = content_types($type_name); + $groups = fieldgroup_groups($type_name); + $group = $groups[$group_name]; + + if ($group['group_type'] != 'multigroup') { + return; + } + + module_load_include('inc', 'content', 'includes/content.admin'); + module_load_include('inc', 'content', 'includes/content.crud'); + $form['group_type'] = array( + '#type' => 'hidden', + '#value' => $group['group_type'], + ); + $form['settings']['multigroup'] = array( + '#type' => 'fieldset', + '#title' => t('Multigroup settings'), + '#collapsed' => FALSE, + '#collapsible' => TRUE, + ); + + if (isset($group['settings']['multigroup']['subgroup'])) { + // Preserve subgroup display settings. + $form['settings']['multigroup']['subgroup'] = array( + '#type' => 'value', + '#value' => $group['settings']['multigroup']['subgroup'], + ); + } + + $form['settings']['multigroup']['multiple-columns'] = array( + '#type' => 'checkbox', + '#title' => t('Multiple columns'), + '#default_value' => isset($group['settings']['multigroup']['multiple-columns']) ? $group['settings']['multigroup']['multiple-columns'] : 0, + '#description' => t('Enable this option to render each field on a separate column on the node edit form.'), + ); + + $form['settings']['multigroup']['required'] = array( + '#type' => 'checkbox', + '#title' => t('Required'), + '#default_value' => isset($group['settings']['multigroup']['required']) ? $group['settings']['multigroup']['required'] : 1, + '#description' => t('Enable this option to require a minimum of one collection of fields in this Multigroup.'), + ); + + $description = t('Number of times to repeat the collection of Multigroup fields.') .' '; + $description .= t("'Unlimited' will provide an 'Add more' button so the users can add items as many times as they like.") .' '; + $description .= t('All fields in this group will automatically be set to allow this number of values.'); + + $group_multiple = isset($group['settings']['multigroup']['multiple']) ? $group['settings']['multigroup']['multiple'] : 1; + $form['settings']['multigroup']['multiple'] = array( + '#type' => 'select', + '#title' => t('Number of repeats'), + '#options' => content_multigroup_multiple_values(), + '#default_value' => $group_multiple, + '#description' => $description, + ); + + $form['settings']['multigroup']['labels'] = array( + '#type' => 'fieldset', + '#title' => t('Labels'), + '#description' => t("Labels for each subgroup of fields. Labels can be hidden or shown in various contexts using the 'Display fields' screen."), + ); + if ($group_multiple < 2) { + $group_multiple = 0; + } + for ($i = 0; $i < 10; $i++) { + $form['settings']['multigroup']['labels'][$i] = array( + '#type' => 'textfield', + '#title' => t('Subgroup %number label', array('%number' => $i + 1)), + '#default_value' => isset($group['settings']['multigroup']['labels'][$i]) ? $group['settings']['multigroup']['labels'][$i] : '', + ); + } + + $form['#validate'][] = 'content_multigroup_group_edit_form_validate'; + $form['#submit'][] = 'content_multigroup_group_edit_form_submit'; +} + +/** + * Validate the Fieldgroup edit form. + */ +function content_multigroup_group_edit_form_validate($form, &$form_state) { + $form_values = $form_state['values']; + $group_type = $form_values['group_type']; + if ($group_type != 'multigroup') { + return; + } + $content_type = $form['#content_type']; + $groups = fieldgroup_groups($content_type['type']); + $group = $groups[$form_values['group_name']]; + foreach ($group['fields'] as $field_name => $data) { + // Make sure we don't set the multiple values to a number that + // would result in lost data. + $max_existing = content_max_delta($field_name); + if ($form_values['settings']['multigroup']['multiple'] != 1 + && $max_existing > $form_values['settings']['multigroup']['multiple']) { + form_set_error('settings][multigroup][multiple', t('The field %field in this group already has %multiple values in the database. To prevent the loss of data you cannot set the number of Multigroup values to less than this.', array('%field' => $data['label'], '%multiple' => $max_existing))); + } + } +} + +/** + * Submit the Fieldgroup edit form. + * + * Update multiple values of fields contained in Multigroups. + */ +function content_multigroup_group_edit_form_submit($form, &$form_state) { + $form_values = $form_state['values']; + $group_type = $form_values['group_type']; + if ($group_type != 'multigroup') { + return; + } + module_load_include('inc', 'content', 'includes/content.crud'); + $content_type = $form['#content_type']; + $groups = fieldgroup_groups($content_type['type']); + $group = $groups[$form_values['group_name']]; + $group_fields = array_intersect_key($content_type['fields'], $group['fields']); + if (!empty($group_fields)) { + foreach ($group_fields as $field_name => $field) { + $field['multiple'] = $form_values['settings']['multigroup']['multiple']; + $field = content_field_instance_collapse($field); + content_field_instance_update($field, FALSE); + } + content_clear_type_cache(TRUE); + menu_rebuild(); + } +} Index: modules/content_multigroup/content_multigroup.css =================================================================== --- modules/content_multigroup/content_multigroup.css (revision 1263) +++ modules/content_multigroup/content_multigroup.css (working copy) @@ -1,9 +1,42 @@ -/* $Id: content_multigroup.css,v 1.1.2.1 2008/10/14 15:16:50 karens Exp $ */ +/* $Id: content_multigroup.css,v 1.1.4.2 2009/06/07 00:06:21 markuspetrux Exp $ */ label.content-multigroup { - font-weight:bold; -} + font-weight: bold; +} -/* Not styled by default, but available to style */ +/* Not styled by default, but available to style. */ hr.content-multigroup { -} \ No newline at end of file +} + +/* Inline field labels visible within the context of multigroups. */ +.content-multigroup-wrapper .field .field-label-inline { + visibility: visible; +} + +/** + * Hide field labels and description on the node edit form when the multiple + * columns option is enabled. + */ +.content-multigroup-edit-table-multiple-columns label, +.content-multigroup-edit-table-multiple-columns .description { + display: none; +} + +/* Hide field labels when using 'table-multiple' display mode. */ +.content-multigroup-display-table-multiple-columns .field .field-label, +.content-multigroup-display-table-multiple-columns .field .field-label-inline, +.content-multigroup-display-table-multiple-columns .field .field-label-inline-first { + display: none; +} + +/* Display table with a row for each subgroup and all fields in a single column. */ +.content-multigroup-display-table-single-column .content-multigroup-wrapper { + clear: both; +} +.content-multigroup-display-table-single-column .content-multigroup-wrapper label.content-multigroup { + display: block; +} +.content-multigroup-display-table-single-column .content-multigroup-wrapper .field { + float: left; + margin-right: 1em; +} Index: modules/content_multigroup/content_multigroup.info =================================================================== --- modules/content_multigroup/content_multigroup.info (revision 0) +++ modules/content_multigroup/content_multigroup.info (revision 0) @@ -0,0 +1,13 @@ +; $Id: content_multigroup.info,v 1.1.2.2 2009/06/07 01:16:40 markuspetrux Exp $ +name = Content Multigroup +description = Combine multiple CCK fields into repeating field collections that work in unison. +dependencies[] = content +dependencies[] = fieldgroup +package = CCK +core = 6.x +; Information added by drupal.org packaging script on 2010-01-26 +version = "6.x-3.x-dev" +core = "6.x" +project = "cck" +datestamp = "1264464167" + Index: modules/content_multigroup/content_multigroup.install =================================================================== --- modules/content_multigroup/content_multigroup.install (revision 0) +++ modules/content_multigroup/content_multigroup.install (revision 0) @@ -0,0 +1,47 @@ + 2, + 'path' => drupal_get_path('module', 'content_multigroup') . '/views', + ); } /** + * Implementation of hook_ctools_plugin_directory(). + */ +function content_multigroup_ctools_plugin_directory($module, $plugin) { + if ($module == 'ctools' && $plugin == 'content_types') { + return 'panels/' . $plugin; + } +} + +/** + * Implementation of hook_menu(). + */ +function content_multigroup_menu() { + $items = array(); + // Callback for AHAH add more buttons. + $items['content_multigroup/js_add_more'] = array( + 'page callback' => 'content_multigroup_add_more_js', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + 'file' => 'content_multigroup.node_form.inc', + ); + return $items; +} + +/** * Implementation of hook_theme(). */ function content_multigroup_theme() { return array( 'content_multigroup_node_form' => array( 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_form.inc', ), 'content_multigroup_node_label' => array( 'arguments' => array('text' => NULL), + 'file' => 'content_multigroup.node_form.inc', ), 'content_multigroup_display_simple' => array( 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', ), + 'content_multigroup_display_fieldset' => array( + 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', + ), 'content_multigroup_display_hr' => array( 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', ), - 'content_multigroup_display_table' => array( + 'content_multigroup_display_table_single' => array( 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', ), + 'content_multigroup_display_table_multiple' => array( + 'arguments' => array('element' => NULL), + 'file' => 'content_multigroup.node_view.inc', + ), ); } /** + * Implementation of hook_elements(). + */ +function content_multigroup_elements() { + return array( + 'content_multigroup_display_fieldset' => array('#value' => NULL), + ); +} + +/** * Implementation of hook_fieldgroup_types(). */ function content_multigroup_fieldgroup_types() { @@ -61,40 +107,19 @@ module_load_include('inc', 'content', 'includes/content.admin'); $settings = array('multigroup' => array('multiple' => 1)); foreach (array_keys(content_build_modes()) as $key) { - $settings['multigroup']['display_settings'][$key]['format'] = 'fieldset'; + $settings['display'][$key]['format'] = 'fieldset'; } return $settings; } } -function content_multigroup_multiple_values() { - return array( - //'' => t('N/A'), - 1 => t('Unlimited'), - 0 => 1) + drupal_map_assoc(range(2, 10)); -} - /** - * Implementation of hook_menu(). - */ -function content_multigroup_menu() { - $items = array(); - // Callback for AHAH add more buttons. - $items['content_multigroup/js_add_more'] = array( - 'page callback' => 'content_multigroup_add_more_js', - 'access arguments' => array('access content'), - 'type' => MENU_CALLBACK, - ); - return $items; -} - -/** * Implementation of hook_form_alter(). */ function content_multigroup_form_alter(&$form, $form_state, $form_id) { - // If this is a field edit form and the field is in a Multigroup, - // override the multiple value settings. if ($form_id == 'content_field_edit_form' && isset($form['widget'])) { + // If this is a field edit form and the field is in a Multigroup, + // override the multiple value settings. $content_type = content_types($form['type_name']['#value']); $groups = fieldgroup_groups($content_type['type']); $group_name = _fieldgroup_field_get_group($content_type['type'], $form['field_name']['#value']); @@ -104,1047 +129,60 @@ $form['field']['multiple']['#access'] = FALSE; } } + elseif ($form_id == 'content_field_edit_form' && isset($form_state['change_basic'])) { + // This is the basic field settings form. It should not be possible to + // choose a widget type that is not compatible with multigroups. + module_load_include('inc', 'content_multigroup', 'content_multigroup.admin'); + content_multigroup_field_basic_form($form, $form_state); + } elseif ($form_id == 'content_field_overview_form') { - content_multigroup_field_overview_form($form, $form_state); + // Validation for creating/moving fields and groups on the + // Manage Fields screen. + module_load_include('inc', 'content_multigroup', 'content_multigroup.admin'); $form['#validate'][] = 'content_multigroup_field_overview_form_validate'; } - elseif ($form_id == 'content_display_overview_form') { - content_multigroup_display_overview_form($form, $form_state, $form_id); - $form['#submit'] = array_merge(array('content_multigroup_display_overview_form_submit'), $form['#submit']); + elseif ($form_id == 'content_display_overview_form' && !empty($form['#groups'])) { + // Add an additional selector for setting multigroup field display + // format to the Display Fields screen. + module_load_include('inc', 'content_multigroup', 'content_multigroup.admin'); + content_multigroup_display_overview_form($form, $form_state); } elseif ($form_id == 'fieldgroup_group_edit_form') { - return content_multigroup_group_edit_form($form, $form_state, $form_id); + // Alter the Fieldgroup edit form to add Multigroup settings. + module_load_include('inc', 'content_multigroup', 'content_multigroup.admin'); + content_multigroup_group_edit_form($form, $form_state); } } -function content_multigroup_field_overview_form(&$form, &$form_state) { - $options = fieldgroup_types(); - $options['standard'] = t('Standard'); - $options['multigroup'] = t('Multigroup'); - $form['_add_new_group']['group_type'] = array( - '#type' => 'select', - '#description' => t('Type of group.'), - '#options' => $options, - '#default_value' => 'standard', - ); -} - /** - * Validation for creating/moving fields and groups on the - * Manage Fields screen. - */ -function content_multigroup_field_overview_form_validate($form, &$form_state) { - $form_values = $form_state['values']; - - $type_name = $form['#type_name']; - $fields = array(); - $groups = array(); - - $group = $form_values['_add_new_group']; - if (array_filter(array($group['label'], $group['group_name']))) { - - $group['settings'] = field_group_default_settings($form_values['_add_new_group']['group_type']); - $group = $form_values['_add_new_group']; - $validation = fieldgroup_validate_name($group, $form['#type_name']); - - // If there's something wrong with the new group, - // don't bother doing any more validation, further - // processing will be stopped by the fieldgroup module. - if (!empty($validation['errors'])) { - return; - } - $group['group_name'] = $validation['group_name']; - $new_group_name = $group['group_name']; - $groups['_add_new_group'] = $group; - } - - // See if we have fields moving into or out of a Multigroup. - // Set any fields to use the new name here so they will get processed - // correctly by the fieldgroup module when saved. - foreach ($form_values as $key => $values) { - if ($values['parent'] == '_add_new_group') { - $values['parent'] = $new_group_name; - $form_values[$key] = $values; - } - - if (!empty($form[$key]['#row_type']) && $form[$key]['#row_type'] == 'group') { - // Gather up info about all groups. - $group_name = $form_values[$key]['group']['group_name']; - $groups[$group_name] = $form_values[$key]['group']; - } - if (!empty($form[$key]['#row_type']) && $form[$key]['#row_type'] == 'field') { - if ($values['prev_parent'] != $values['parent']) { - // Gather up fields that have moved in or out of a group. - $fields[$key] = $form_values[$key]['field']; - } - } - } - - if (!empty($fields)) { - foreach ($fields as $field_name => $field) { - $new_group = $form_values[$field_name]['parent']; - $old_group = $form_values[$field_name]['prev_parent']; - if (!empty($new_group) && isset($groups[$new_group]) && $groups[$new_group]['group_type'] == 'multigroup') { - $allowed_in = content_multigroup_allowed_in($field, $groups[$new_group]); - if (!$allowed_in['allowed']) { - form_set_error($field_name, $allowed_in['message']); - } - else { - if (!empty($allowed_in['message'])) { - drupal_set_message($allowed_in['message']); - } - module_load_include('inc', 'content', 'includes/content.crud'); - $content_type = content_types($type_name); - $multiple = $groups[$new_group]['settings']['multigroup']['multiple']; - $multiple_values = content_multigroup_multiple_values(); - $field = $content_type['fields'][$field_name]; - $field['multiple'] = $multiple; - $field = content_field_instance_collapse($field); - content_field_instance_update($field); - drupal_set_message(t('The field %field has been updated to use %multiple values, to match the multiple value setting of the Multigroup %group.', array( - '%field' => $field['label'], '%multiple' => $multiple_values[$multiple], '%group' => $groups[$new_group]['label']))); - } - } - elseif (!empty($old_group) && isset($groups[$old_group]) && $groups[$old_group]['group_type'] == 'multigroup') { - $allowed_out = content_multigroup_allowed_out($field, $groups[$old_group]); - if (!$allowed_out['allowed']) { - form_set_error($field_name, $allowed_out['message']); - } - elseif (!empty($allowed_out['message'])) { - drupal_set_message($allowed_out['message']); - } - } - } - } -} - -/** - * Helper function for deciding if a field is - * allowed into a Multigroup. - */ -function content_multigroup_allowed_in($field, $group) { - if ($group['group_type'] != 'multigroup') { - return array('allowed' => TRUE, 'message' => ''); - } - - // We can't allow fields with more multiple values than the group has - // to be moved into it. - $max_existing = content_max_delta($field['field_name']); - $group_max = $group['settings']['multigroup']['multiple']; - $multiple_values = content_multigroup_multiple_values(); - if ($group_max != 1 && $max_existing > $group_max) { - return array( - 'allowed' => FALSE, - 'message' => t('This change is not allowed. The field %field already has %multiple values in the database but the group %group only allows %group_max. Making this change would result in the loss of data.', array('%field' => $field['widget']['label'], '%multiple' => $max_existing, '%group' => $group['label'], '%group_max' => $multiple_values[$group_max])) - ); - } - - // Fields that handle their own multiple values may not have the same values - // in Multigroup fields and normal fields. We don't know if they will work or not. - - // Adding a hook here where widgets that handle their own multiple values - // that will work correctly in Multigroups can allow their fields in. - - if (content_handle('widget', 'multiple values', $field) != CONTENT_HANDLE_CORE) { - $allowed_widgets = array( - 'optionwidgets_select', - 'optionwidgets_buttons', - 'optionwidgets_onoff', - 'nodereference_buttons', - 'nodereference_select', - 'userreference_buttons', - 'userreference_select', - ); - $allowed_widgets = array_merge($allowed_widgets, module_invoke_all('content_multigroup_allowed_widgets')); - if (!in_array($field['widget']['type'], $allowed_widgets)) { - return array( - 'allowed' => FALSE, - 'message' => t('This change is not allowed. The field %field handles multiple values differently than the Content module. Making this change could result in the loss of data.', array('%field' => $field['widget']['label'])) - ); - } - } - - // Allow other modules to intervene. - // Any failure will prevent this action. - foreach (module_implements('content_multigroup_allowed_in') as $module) { - $function = $module .'_content_multigroup_allowed_in'; - $result = $function($field, $group); - if ($result['allowed'] === FALSE) { - return array('allowed' => FALSE, 'message' => $result['message']); - } - } - - $message = t('You are moving the field %field into a Multigroup.', array('%field' => $field['widget']['label'])); - return array('allowed' => TRUE, 'message' => $message); -} - -/** - * Helper function for deciding if a field is - * allowed out of a Multigroup. - */ -function content_multigroup_allowed_out($field, $group) { - if ($group['group_type'] != 'multigroup') { - return array('allowed' => TRUE, 'message' => ''); - } - // Optionwidgets do not behave the same in a Multigroup field as out of it. - // In a Multigroup the same option can be selected multiple times, - // but that is not possible in a normal group. - - // Adding a hook here where widgets that handle their own multiple values - // can indicate their fields should not be removed from Multigroups. - - $max_existing = content_max_delta($field['field_name']); - $no_remove_widgets = array( - 'optionwidgets_select', - 'optionwidgets_buttons', - 'optionwidgets_onoff', - 'nodereference_buttons', - 'nodereference_select', - 'userreference_buttons', - 'userreference_select', - ); - $no_remove_widgets = array_merge($no_remove_widgets, module_invoke_all('content_multigroup_no_remove_widgets')); - if (in_array($field['widget']['type'], $no_remove_widgets) && $max_existing > 0) { - return array( - 'allowed' => FALSE, - 'message' => t('This change is not allowed. The field %field already has data created and uses a widget that stores data differently in a Standard group than in a Multigroup. Making this change could result in the loss of data.', array('%field' => $field['widget']['label'])) - ); - } - - // Allow other modules to intervene. - // Any failure will prevent this action. - foreach (module_implements('content_multigroup_allowed_out') as $module) { - $function = $module .'_content_multigroup_allowed_out'; - $result = $function($field, $group); - if ($result['allowed'] === FALSE) { - return array('allowed' => FALSE, 'message' => $result['message']); - } - } - - $message = t('You are moving the field %field out of a Multigroup.', array('%field' => $field['widget']['label'])); - return array('allowed' => TRUE, 'message' => $message); -} - -/** - * Menu callback; presents a listing of fields display settings for a content type. + * After build callback for multigroups in node form. * - * Add an additional selector for setting multigroup field display format. + * This proxy function is necessary to prevent from breaking AHAH handlers. */ -function content_multigroup_display_overview_form(&$form, &$form_state) { - - $type_name = $form['#type_name']; - $contexts_selector = $form['#contexts']; - - // Gather type information. - $type = content_types($type_name); - $field_types = _content_field_types(); - $fields = $type['fields']; - - $groups = $group_options = array(); - if (module_exists('fieldgroup')) { - $groups = fieldgroup_groups($type['type']); - $group_options = _fieldgroup_groups_label($type['type']); - } - $contexts = content_build_modes($contexts_selector); - - // Multigroups, extra values. - $label_options = array( - 'above' => t('Above'), - 'hidden' => t(''), - ); - $options = array( - 'none' => t('none'), - 'fieldset' => t('Fieldset'), - 'hr' => t('Horizontal line'), - //'table' => t('Table'), // TODO add this later - 'hidden' => t(''), - ); - foreach ($groups as $name => $group) { - if ($group['group_type'] != 'multigroup') { - continue; - } - $defaults = $group['settings']['multigroup']['display_settings']; - - $form_name = $name .'_subgroup'; - $form['#fields'] = array_merge(array($form_name), $form['#fields']); - $form[$form_name] = array( - 'human_name' => array('#value' => t('[Subgroup format]')), - 'weight' => array('#type' => 'value', '#value' => -20), - 'parent' => array('#type' => 'value', '#value' => $name), - ); - if ($contexts_selector == 'basic') { - $form[$form_name]['label'] = array( - '#type' => 'select', - '#options' => $label_options, - '#default_value' => isset($defaults['label']) ? $defaults['label'] : 'above', - ); - } - foreach ($contexts as $key => $title) { - $form[$form_name][$key]['format'] = array( - '#type' => 'select', - '#options' => $options, - '#default_value' => isset($defaults[$key]) ? $defaults[$key] : 'fieldset', - ); - } - } - return $form; +function content_multigroup_node_form_after_build($form, &$form_state) { + module_load_include('inc', 'content_multigroup', 'content_multigroup.node_form'); + return _content_multigroup_node_form_after_build($form, $form_state); } /** - * Submit handler for the display overview form. - * - * Do this in pre_save so we catch it before the content module - * tries to use our 'field'. - */ -function content_multigroup_display_overview_form_submit($form, &$form_state) { - $form_values = $form_state['values']; - - // Find any groups we inserted into the display fields form, - // save our settings, and remove them from $form_state. - foreach ($form_values as $key => $values) { - if (in_array($key, $form['#fields']) && substr($key, -9) == '_subgroup') { - $group_name = str_replace('_subgroup', '', $key); - $groups = fieldgroup_groups($form['#type_name']); - $group = $groups[$group_name]; - - // We have some numeric keys here, so we can't use array_merge. - foreach ($values as $k => $v) { - $group['settings']['multigroup']['display_settings'][$k] = $v; - } - fieldgroup_save_group($form['#type_name'], $group); - - // Make sure group information is immediately updated. - cache_clear_all('fieldgroup_data', content_cache_tablename()); - fieldgroup_groups('', FALSE, TRUE); - unset($form_state['values'][$key]); - } - } -} - -/** - * Alter the Fieldgroup edit form - * to add Multigroup settings. - */ -function content_multigroup_group_edit_form(&$form, &$form_state) { - $type_name = $form['#content_type']['type']; - $group_name = $form['group_name']['#default_value']; - - $content_type = content_types($type_name); - $groups = fieldgroup_groups($content_type['type']); - $group = $groups[$group_name]; - - if ($group['group_type'] != 'multigroup') { - return; - } - - module_load_include('inc', 'content', 'includes/content.admin'); - module_load_include('inc', 'content', 'includes/content.crud'); - $form['group_type'] = array( - '#type' => 'hidden', - '#value' => $group['group_type'], - ); - $form['settings']['multigroup'] = array( - '#type' => 'fieldset', - '#title' => t('Other settings'), - '#collapsed' => FALSE, - '#collapsible' => TRUE, - ); - - $description = t('Number of times to repeat the collection of Multigroup fields.') . ' '; - $description .= t("'Unlimited' will provide an 'Add more' button so the users can add repeat it as many times as they like.") . ' '; - $description .= t('All fields in this group will automatically be set to allow this number of values.'); - - $multiple = isset($group['settings']['multigroup']['multiple']) ? $group['settings']['multigroup']['multiple'] : 1; - $form['settings']['multigroup']['multiple'] = array( - '#tree' => TRUE, - '#type' => 'select', - '#title' => t('Number of repeats'), - '#options' => content_multigroup_multiple_values(), - '#default_value' => $multiple, - '#description' => $description, - ); - - $form['settings']['multigroup']['labels'] = array( - '#type' => 'fieldset', - '#title' => t('Labels'), - '#description' => t("Labels for each subgroup of fields. Labels can be hidden or shown in various contexts using the 'Display fields' screen."), - ); - if ($multiple < 2) { - $multiple = 0; - } - for ($i = 0; $i < 10; $i++) { - $form['settings']['multigroup']['labels'][$i] = array( - '#type' => 'textfield', - '#title' => t('Subgroup %number label', array('%number' => $i + 1)), - '#default_value' => isset($group['settings']['multigroup']['labels'][$i]) ? $group['settings']['multigroup']['labels'][$i] : '', - ); - } - - $form['#validate'][] = 'content_multigroup_group_edit_form_validate'; - $form['#submit'][] = 'content_multigroup_group_edit_form_submit'; - return $form; -} - -/** - * Validate the Fieldgroup edit form. - */ -function content_multigroup_group_edit_form_validate($form, &$form_state) { - $form_values = $form_state['values']; - $group_type = $form_values['group_type']; - if ($group_type != 'multigroup') { - return; - } - $content_type = $form['#content_type']; - $groups = fieldgroup_groups($content_type['type']); - $group = $groups[$form_values['group_name']]; - foreach ($group['fields'] as $field_name => $data) { - // Make sure we don't set the multiple values to a number that - // would result in lost data. - $max_existing = content_max_delta($field_name); - if ($form_values['settings']['multigroup']['multiple'] != 1 - && $max_existing > $form_values['settings']['multigroup']['multiple']) { - form_set_error('settings][multigroup][multiple', t('The field %field in this group already has %multiple values in the database. To prevent the loss of data you cannot set the number of Multigroup values to less than this.', array('%field' => $data['label'], '%multiple' => $max_existing))); - } - } -} - -/** - * Submit the Fieldgroup edit form. - * - * Update multiple values of fields contained in Multigroups. - */ -function content_multigroup_group_edit_form_submit($form, &$form_state) { - $form_values = $form_state['values']; - $group_type = $form_values['group_type']; - if ($group_type != 'multigroup') { - return; - } - module_load_include('inc', 'content', 'includes/content.crud'); - $content_type = $form['#content_type']; - $groups = fieldgroup_groups($content_type['type']); - $group = $groups[$form_values['group_name']]; - $multiple = $form_values['settings']['multigroup']['multiple']; - foreach ($group['fields'] as $field_name => $data) { - $field = $content_type['fields'][$field_name]; - $field['multiple'] = $multiple; - $field = content_field_instance_collapse($field); - content_field_instance_update($field); - } -} - -/** * Implementation of hook_fieldgroup_form(). - * - * Align the delta values of each field in the Multigroup. - * - * Swap the field name and delta for each Multigroup so we can - * d-n-d each collection of fields as a single delta item. */ function content_multigroup_fieldgroup_form(&$form, &$form_state, $form_id, $group) { - if ($group['group_type'] != 'multigroup' || - !empty($form[$group['group_name']]['#access']) || empty($form[$group['group_name']])) { - return; - } - - $node = $form['#node']; - $fields = $group['fields']; - $content_fields = content_fields(); $group_name = $group['group_name']; - - // Use the first field in the group to get the item counts. - $first_field_name = array_shift(array_keys($group['fields'])); - $first_field = isset($content_fields[$first_field_name]) ? $content_fields[$first_field_name] : array(); - $first_field_items = isset($node->$first_field_name) ? $node->$first_field_name : array(); - - $group['multiple'] = $group['settings']['multigroup']['multiple']; - switch ($group['multiple']) { - case 0: - $max = 0; - break; - case 1: - // Is this a new node? - if (empty($first_field_items)) { - $max = 1; - } - else { - $filled_items = content_set_empty($first_field, $first_field_items); - $current_item_count = isset($form_state['item_count'][$group_name]) - ? $form_state['item_count'][$group_name] - : count($first_field_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 - : count($filled_items); - } - break; - default: - $max = $group['multiple'] - 1; - break; - } - - $form[$group_name]['#theme'] = 'content_multigroup_node_form'; - $form[$group_name]['#multiple'] = !empty($max); - $form[$group_name]['#type_name'] = $group['type_name']; - $form[$group_name]['#group_name'] = $group_name; - $form[$group_name]['#group_label'] = $group['label']; - $form[$group_name]['#element_validate'] = array('content_multigroup_node_form_validate'); - $form[$group_name]['#tree'] = TRUE; - - for ($delta = 0; $delta <= $max; $delta++) { - content_multigroup_group_form($form, $form_state, $group, $delta); - } - - // Unset the original group field values now that we've moved them. - foreach ($fields as $field_name => $field) { - unset($form[$group_name][$field_name]); - } - - if ($add_more = content_multigroup_add_more($form, $form_state, $group)) { - $form[$group_name] += $add_more; - } -} - -/** - * Create a new delta value for the group. - * - * Called in form_alter and by AHAH add more. - */ -function content_multigroup_group_form(&$form, &$form_state, $group, $delta) { - if ($group['group_type'] != 'multigroup' || - !empty($form[$group['group_name']]['#access']) || empty($form[$group['group_name']])) { - return; - } - module_load_include('inc', 'content', 'includes/content.node_form'); - - $node = $form['#node']; - $fields = $group['fields']; - $content_fields = content_fields(); - $group_name = $group['group_name']; - $group['multiple'] = $group['settings']['multigroup']['multiple']; - $form[$group_name]['#fields'] = array_keys($group['fields']); - - foreach ($fields as $field_name => $group_field) { - if (empty($form[$group_name][$delta])) { - $form[$group_name] += array($delta => array($field_name => array())); + if ($group['group_type'] == 'multigroup' && !empty($form[$group_name])) { + if (!isset($form[$group_name]['#access']) || $form[$group_name]['#access']) { + module_load_include('inc', 'content_multigroup', 'content_multigroup.node_form'); + _content_multigroup_fieldgroup_form($form, $form_state, $form_id, $group); } - else { - $form[$group_name][$delta][$field_name] = array(); - } - - $form[$group_name][$delta]['_weight'] = array( - '#type' => 'weight', - '#delta' => $delta, // this 'delta' is the 'weight' element's property - '#default_value' => $delta, - '#weight' => 100, - ); - $form[$group_name][$delta]['_delta'] = array( - '#type' => 'hidden', - '#value' => $delta, - ); - - - $field = $content_fields[$field_name]; - - // Make each field into a pseudo single value field - // with the right delta value. - $field['multiple'] = FALSE; - - // Make sure new fields after the first have an 'empty' option. - $field['required'] = $delta > 0 ? FALSE : $field['required']; - - $form['#field_info'][$field_name] = $field; - $node_copy = drupal_clone($node); - - // Set the form '#node' to the delta value we want so the Content - // module will feed the right $items to the field module in - // content_field_form(). - - // There may be missing delta values for fields that were - // never created, so check first. - if (!empty($node->$field_name) && count($node->$field_name) >= $delta + 1) { - $node_copy->$field_name = array($delta => $node->{$field_name}[$delta]); - } - else { - $node_copy->$field_name = array($delta => NULL); - } - $form['#node'] = $node_copy; - $field_form = content_field_form($form, $form_state, $field, $delta); - - // Place the new $field_form into the $delta position in the group form. - if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) { - $value = array_key_exists($delta, $field_form[$field_name]) ? $delta : 0; - $form[$group_name][$delta][$field_name] = $field_form[$field_name][$value]; - } - else { - $form[$group_name][$delta][$field_name] = $field_form[$field_name]; - } - $form[$group_name][$delta][$field_name]['#weight'] = $field['widget']['weight']; - - // Add in our validation step, and make sure it preceeds other - // processing so we can massage the element back to the normal position. - if (empty($form[$group_name][$delta][$field_name]['#element_validate'])) { - $form[$group_name][$delta][$field_name]['#element_validate'] = array(); - } - array_unshift($form[$group_name][$delta][$field_name]['#element_validate'], 'content_multigroup_node_item_validate'); } - - // Reset the form '#node' back to its original value. - $form['#node'] = $node; - - return $form; } /** - * Swap transposed field/delta values back - * to their normal positions in the node. - */ -function content_multigroup_node_item_validate($element, &$form_state) { - static $weights = array(); - - //dsm($form_state['values']); - $form_values = $form_state['values']; - $field_name = array_pop($element['#parents']); - $delta = array_pop($element['#parents']); - $group_name = array_pop($element['#parents']); - - // Identify the new delta value for each field. - - // Find the original delta values for this group, save as static value - // because the group will acquire and lose values while we process it. - if (!array_key_exists($group_name, $weights)) { - $items = $form_state['values'][$group_name]; - $weights[$group_name] = array(); - foreach ($items as $count => $value) { - // Allow for the possibility of matching _weights and missing deltas. - $weight = floatval($value['_weight']); - $old_delta = intval($value['_delta']); - if (empty($weights[$group_name][$weight]) || !in_array($old_delta, $weights[$group_name][$weight])) { - $weights[$group_name][$weight][] = $old_delta; - } - } - ksort($weights[$group_name]); - } - $count = 0; - foreach ($weights[$group_name] as $weight => $values) { - foreach ($values as $old_delta) { - if ($old_delta === $delta) { - $delta = $count; - //dsm('moving delta values: '.$group_name.'>'.$field_name.'>'.'from '. $old_delta .' to '. $delta); - break 2; - } - $count++; - } - } - // We figured out what the new order for the fields is, - // so set the value for the new delta. - - // We move these new values back up to the top level of the - // node and out of the group so the Content module will find and - // save the new values and so they don't get mixed into the - // remaining, unaltered, values in the group. - array_push($element['#parents'], $field_name); - array_push($element['#parents'], $delta); - - // It's very important to use $form_values instead of $element['#value'] - // here, because $element['#value'] is sometimes missing changes - // made in #element_validate processing done by other modules. - $value = isset($form_values[$group_name][$delta][$field_name]) ? $form_values[$group_name][$delta][$field_name] : NULL; - - // Fields that use optionwidgets have an extra array level in the value - // because of the optionwidgets transposition that forces a delta value - // into the result array. This works fine when a delta value is between - // the field name and the field value, as in normal nodes, but not when - // we reverse the field and the delta, so in this case we need to - // promote the nested delta value back up to the field level. - if (is_array($value) && content_multigroup_uses_optionwidgets($field_name, $element['#type_name'])) { - $value = array_shift($value); - } - - //dsm('setting value of '. $field_name.'>'.$delta); - //dsm($value); - form_set_value($element, $value, $form_state); -} - -/** - * Helper function for identifying fields that use - * optionwidgets transpositions. - */ -function content_multigroup_uses_optionwidgets($field_name, $type_name) { - static $optionwidgets; - if (empty($optionwidgets)) { - $optionwidgets = array( - 'optionwidgets_select', - 'optionwidgets_buttons', - 'optionwidgets_onoff', - 'nodereference_buttons', - 'nodereference_select', - 'userreference_buttons', - 'userreference_select', - ); - // Add hook where other widgets that use optionwidgets can announce it. - $optionwidgets = array_merge($optionwidgets, module_invoke_all('content_multigroup_uses_optionwidgets')); - } - - $types = content_types($type_name); - $fields = $types['fields']; - $field = $fields[$field_name]; - if (in_array($field['widget']['type'], $optionwidgets)) { - return TRUE; - } - return FALSE; -} - -/** - * Validation for the whole node group. - */ -function content_multigroup_node_form_validate($element, $form_state) { - // We moved all the new field values out of the field group - // and up to the top level of the node, now get rid of the - // original group values. - form_set_value($element, NULL, $form_state); - return; -} - -/** * Implementation of hook_fieldgroup_view(). */ function content_multigroup_fieldgroup_view(&$node, &$element, $group, $context) { - if ($group['group_type'] != 'multigroup') { - return; + if ($group['group_type'] == 'multigroup') { + module_load_include('inc', 'content_multigroup', 'content_multigroup.node_view'); + _content_multigroup_fieldgroup_view($node, $element, $group, $context); } - - $group_name = $group['group_name']; - $node_copy = drupal_clone($node); - $max = $group['settings']['multigroup']['multiple']; - - $count = 0; - foreach ($group['fields'] as $field_name => $field) { - $count = max($count, count($node->$field_name)); - } - - $group['multiple'] = isset($group['settings']['multigroup']['multiple']) ? $group['settings']['multigroup']['multiple'] : 1; - $labels = isset($group['settings']['multigroup']['labels']) ? $group['settings']['multigroup']['labels'] : array(); - $format = isset($group['settings']['multigroup']['display_settings'][$context]['format']) ? $group['settings']['multigroup']['display_settings'][$context]['format'] : 'fieldset'; - $show_label = isset($group['settings']['multigroup']['display_settings']['label']) ? $group['settings']['multigroup']['display_settings']['label'] : 'above'; - - switch ($group['multiple']) { - case 0: - $max = 0; - break; - case 1: - $max = $count; - break; - default: - $max = $group['multiple']; - break; - } - - for ($delta = 0; $delta < $max; $delta++) { - $element[$delta] = array('#weight' => $delta); - - $label = !empty($labels[$delta]) && $show_label == 'above' ? $labels[$delta] : ''; - - foreach ($group['fields'] as $field_name => $field) { - - // Create a pseudo node that only has the value we want - // in this group and pass it to the formatter. - if (isset($node->content[$field_name])) { - $node_copy->content[$field_name]['field']['items'] = array( - $delta => isset($node->content[$field_name]['field']['items'][$delta]) ? $node->content[$field_name]['field']['items'][$delta] : NULL, - ); - $element[$delta][$field_name] = $node_copy->content[$field_name]; - $element[$delta][$field_name]['#delta'] = $delta; - } - } - switch ($format) { - case 'table': - $element[$delta]['#theme'] = 'content_multigroup_display_table'; - $element[$delta]['#title'] = $label; - break; - case 'fieldset': - $element[$delta]['#type'] = 'fieldset'; - $element[$delta]['#title'] = $label; - break; - case 'hr': - $element[$delta]['#theme'] = 'content_multigroup_display_hr'; - $element[$delta]['#title'] = $label; - break; - default: - $element[$delta]['#theme'] = 'content_multigroup_display_simple'; - $element[$delta]['#title'] = $label; - break; - } - - } - - foreach ($group['fields'] as $field_name => $field) { - if (isset($element[$field_name])) { - unset($element[$field_name]); - } - } } - -/** - * Theme an individual form element. - * - * Combine multiple values into a table with drag-n-drop reordering. - */ -function theme_content_multigroup_node_form($element) { - $output = ''; - if ($element['#multiple'] >= 1) { - $table_id = $element['#group_name'] .'_values'; - $order_class = $element['#group_name'] .'-delta-order'; - - $header = array( - array( - 'data' => '', - 'colspan' => 2 - ), - t('Order'), - ); - $rows = array(); - $groups = fieldgroup_groups($element['#type_name']); - $group = $groups[$element['#group_name']]; - $labels = isset($group['settings']['multigroup']['labels']) ? $group['settings']['multigroup']['labels'] : array(); - $multiple = isset($group['settings']['multigroup']['multiple']) ? $group['settings']['multigroup']['multiple'] : 1; - - $i = 0; - foreach (element_children($element) as $delta => $key) { - if ($key !== $element['#group_name'] .'_add_more') { - $label = !empty($labels[$i]) ? theme('content_multigroup_node_label', $labels[$i]) : ''; - $element[$key]['_weight']['#attributes']['class'] = $order_class; - $delta_element = drupal_render($element[$key]['_weight']); - $cells = array( - array('data' => '', 'class' => 'content-multiple-drag'), - $label . drupal_render($element[$key]), - array('data' => $delta_element, 'class' => 'delta-order'), - ); - $rows[] = array( - 'data' => $cells, - // TODO Tablesort drag n drop is not working with complex - // field validation. The fields appear to work correctly, - // but element validation seems to get missed or confused - // causing validation errors. Need to investigate why. - 'class' => 'draggable', - ); - } - $i++; - } - - $output .= theme('table', $header, $rows, array('id' => $table_id, 'class' => 'content-multiple-table')); - $output .= $element['#description'] ? '
'. $element['#description'] .'
' : ''; - $output .= drupal_render($element[$element['#group_name'] .'_add_more']); - - drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class); - } - else { - foreach (element_children($element) as $key) { - $output .= drupal_render($element[$key]); - } - } - - return $output; -} - -function content_multigroup_add_more(&$form, &$form_state, $group) { - // Add AHAH add more button, if not working with a programmed form. - $multiple = $group['settings']['multigroup']['multiple']; - $form_element = array(); - if ($multiple != 1 || !empty($form['#programmed'])) { - return $form_element; - } - else { - // Make sure the form is cached so ahah can work. - $form['#cache'] = TRUE; - $content_type = content_types($group['type_name']); - $group_name = $group['group_name']; - $group_name_css = str_replace('_', '-', $group_name); - - $form_element[$group_name .'_add_more'] = array( - '#type' => 'submit', - '#name' => $group_name .'_add_more', - '#value' => t('Add more values'), - '#weight' => $multiple + 1, - // Submit callback for disabled JavaScript. drupal_get_form() might get - // the form from the cache, so we can't rely on content_form_alter() - // including this file. Therefore, call a proxy function to do this. - '#submit' => array('content_multigroup_add_more_submit_proxy'), - '#ahah' => array( - 'path' => 'content_multigroup/js_add_more/'. $content_type['url_str'] .'/'. $group_name, - 'wrapper' => $group_name_css .'-items', - 'method' => 'replace', - 'effect' => 'fade', - ), - // When JS is disabled, the content_add_more_submit handler will find - // the relevant field using these entries. - '#group_name' => $group_name, - '#type_name' => $group['type_name'], - ); - - // Add wrappers for the group and 'more' button. - // TODO: could be simplified ? - $form_element['#prefix'] = '
'; - $form_element[$group_name .'_add_more']['#prefix'] = '
'; - $form_element[$group_name .'_add_more']['#suffix'] = '
'; - } - return $form_element; -} - -/** - * Submit handler to add more choices to a content form. This handler is used when - * JavaScript is not available. It makes changes to the form state and the - * entire form is rebuilt during the page reload. - */ -function content_multigroup_add_more_submit($form, &$form_state) { - // Set the form to rebuild and run submit handlers. - node_form_submit_build_node($form, $form_state); - $group_name = $form_state['clicked_button']['#group_name']; - $type_name = $form_state['clicked_button']['#type_name']; - - // Make the changes we want to the form state. - if ($form_state['values'][$group_name][$group_name .'_add_more']) { - $form_state['item_count'][$group_name] = count($form_state['values'][$group_name]); - } -} - -/** - * Menu callback for AHAH addition of new empty widgets. - * - * Adapted from content_add_more_js to work with groups instead of fields. - */ -function content_multigroup_add_more_js($type_name_url, $group_name) { - $type = content_types($type_name_url); - $groups = fieldgroup_groups($type['type']); - $group = $groups[$group_name]; - $group['multiple'] = $group['settings']['multigroup']['multiple']; - - if (($group['multiple'] != 1) || empty($_POST['form_build_id'])) { - // Invalid request. - drupal_json(array('data' => '')); - exit; - } - - // Retrieve the cached form. - $form_state = array('submitted' => FALSE); - $form_build_id = $_POST['form_build_id']; - $form = form_get_cache($form_build_id, $form_state); - if (!$form) { - // Invalid form_build_id. - drupal_json(array('data' => '')); - exit; - } - - // We don't simply return a new empty widget to append to existing ones, because - // - ahah.js won't simply let us add a new row to a table - // - attaching the 'draggable' behavior won't be easy - // So we resort to rebuilding the whole table of widgets including the existing ones, - // which makes us jump through a few hoops. - - // The form that we get from the cache is unbuilt. We need to build it so that - // _value callbacks can be executed and $form_state['values'] populated. - // We only want to affect $form_state['values'], not the $form itself - // (built forms aren't supposed to enter the cache) nor the rest of $form_data, - // so we use copies of $form and $form_data. - $form_copy = $form; - $form_state_copy = $form_state; - $form_copy['#post'] = array(); - form_builder($_POST['form_id'], $form_copy, $form_state_copy); - // Just grab the data we need. - $form_state['values'] = $form_state_copy['values']; - // Reset cached ids, so that they don't affect the actual form we output. - form_clean_id(NULL, TRUE); - - // Sort the $form_state['values'] we just built *and* the incoming $_POST data - // according to d-n-d reordering. - unset($form_state['values'][$group_name][$group['group_name'] .'_add_more']); - foreach ($_POST[$group_name] as $delta => $item) { - $form_state['values'][$group_name][$delta]['_weight'] = $item['_weight']; - $form_state['values'][$group_name][$delta]['_delta'] = $item['_delta']; - } - $form_state['values'][$group_name] = _content_sort_items($group, $form_state['values'][$group_name]); - $_POST[$group_name] = _content_sort_items($group, $_POST[$group_name]); - - // Build our new form element for the whole group, asking for one more element. - - $form_state['item_count'] = array($group_name => count($_POST[$group_name]) + 1); - $delta = max(array_keys($_POST[$group_name])) + 1; - content_multigroup_group_form($form, $form_state, $group, $delta); - - // Save the new definition of the form. - $form_state['values'] = array(); - form_set_cache($form_build_id, $form, $form_state); - - // Build the new form against the incoming $_POST values so that we can - // render the new element. - $_POST[$group_name][$delta]['_weight'] = $delta; - $form_state = array('submitted' => FALSE); - $form += array( - '#post' => $_POST, - '#programmed' => FALSE, - ); - $form = form_builder($_POST['form_id'], $form, $form_state); - - // Render the new output. - $group_form = $form[$group_name]; - - // We add a div around the new content to receive the ahah effect. - $group_form[$delta]['#prefix'] = '
'. (isset($group_form[$delta]['#prefix']) ? $group_form[$delta]['#prefix'] : ''); - $group_form[$delta]['#suffix'] = (isset($group_form[$delta]['#suffix']) ? $group_form[$delta]['#suffix'] : '') .'
'; - - // If a newly inserted widget contains AHAH behaviors, they normally won't - // work because AHAH doesn't know about those - it just attaches to the exact - // form elements that were initially specified in the Drupal.settings object. - // The new ones didn't exist then, so we need to update Drupal.settings - // by ourselves in order to let AHAH know about those new form elements. - $javascript = drupal_add_js(NULL, NULL); - $output_js = isset($javascript['setting']) ? '' : ''; - - $output = theme('status_messages') . drupal_render($group_form) . $output_js; - drupal_json(array('status' => TRUE, 'data' => $output)); - exit; -} - -/** - * Theme the sub group label in the node form. - */ -function theme_content_multigroup_node_label($text) { - if (!empty($text)) { - return '

'. check_plain($text) .'

'; - } -} - -function theme_content_multigroup_display_simple($element) { - $label = ''; - if (!empty($element['#title'])) { - $label .= ''; - } - $output = $label; - foreach (element_children($element) as $key) { - $output .= drupal_render($element[$key]); - } - return $output; -} - -function theme_content_multigroup_display_hr($element) { - $label = ''; - if (!empty($element['#title'])) { - $label .= ''; - } - $output = '
'. $label; - foreach (element_children($element) as $key) { - $output .= drupal_render($element[$key]); - } - return $output; -} - -function theme_content_multigroup_display_table($element) { - $label = ''; - if (!empty($element['#title'])) { - $label .= ''; - } - $output = $label; - foreach (element_children($element) as $key) { - $output .= drupal_render($element[$key]); - } - return $output; -} \ No newline at end of file Index: modules/content_multigroup/content_multigroup.node_form.inc =================================================================== --- modules/content_multigroup/content_multigroup.node_form.inc (revision 0) +++ modules/content_multigroup/content_multigroup.node_form.inc (revision 0) @@ -0,0 +1,971 @@ + $field) { + if (isset($group['fields'][$field_name]) && isset($form[$group_name][$field_name])) { + if (!isset($form[$group_name][$field_name]['#access']) || $form[$group_name][$field_name]['#access']) { + $group_fields[$field_name] = $field; + } + } + } + + // Quit if there are no field in the form for this group. + if (empty($group_fields)) { + return; + } + + switch ($group_multiple) { + case 0: + $group_deltas = array(0); + $max_delta = 0; + break; + + case 1: + // Compute unique deltas from all deltas used by fields in this multigroup. + $group_deltas = array(); + $max_delta = -1; + foreach (array_keys($group_fields) as $field_name) { + if (!empty($node->$field_name) && is_array($node->$field_name)) { + foreach (array_keys($node->$field_name) as $delta) { + $group_deltas[$delta] = $delta; + } + sort($group_deltas); + $max_delta = max($max_delta, max($group_deltas)); + } + } + $current_item_count = isset($form_state['item_count'][$group_name]) ? $form_state['item_count'][$group_name] : max(1, count($group_deltas)); + while (count($group_deltas) < $current_item_count) { + $max_delta++; + $group_deltas[] = $max_delta; + } + break; + + default: + $max_delta = $group_multiple - 1; + $group_deltas = range(0, $max_delta); + break; + } + + $form[$group_name]['#theme'] = 'content_multigroup_node_form'; + $form[$group_name]['#item_count'] = count($group_deltas); + $form[$group_name]['#type_name'] = $group['type_name']; + $form[$group_name]['#group_name'] = $group_name; + $form[$group_name]['#group_label'] = $group['label']; + $form[$group_name]['#group_fields'] = $group_fields; + $form[$group_name]['#tree'] = TRUE; + if (!isset($form['#multigroups'])) { + $form['#multigroups'] = array(); + } + $form['#multigroups'][$group_name] = $group_fields; + + // Add a visual indication to the fieldgroup title if the multigroup is required. + if (!empty($group['settings']['multigroup']['required'])) { + $form[$group_name]['#title'] .= ' *'; + } + + // Attach our own after build handler to the form, used to fix posting data + // and the form structure, moving fields back to their original positions. + // That is, move them from group->delta->field back to field->delta. + if (!isset($form['#after_build'])) { + $form['#after_build'] = array(); + } + if (!in_array('content_multigroup_node_form_after_build', $form['#after_build'])) { + array_unshift($form['#after_build'], 'content_multigroup_node_form_after_build'); + } + + // Attach our own validation handler to the form, used to check for empty fields. + if (!isset($form['#validate'])) { + $form['#validate'] = array(); + } + if (!in_array('content_multigroup_node_form_validate', $form['#validate'])) { + array_unshift($form['#validate'], 'content_multigroup_node_form_validate'); + } + + // Attach our own pre_render handler to the form, used to fix the required + // attribute of all fields in multigroups. + if (!isset($form['#pre_render'])) { + $form['#pre_render'] = array(); + } + if (!in_array('content_multigroup_node_form_pre_render', $form['#pre_render'])) { + array_unshift($form['#pre_render'], 'content_multigroup_node_form_pre_render'); + } + $elements[$group_name] = array(); + foreach ($group_deltas as $delta) { + $element = content_multigroup_group_form($form, $form_state, $group, $delta); + $elements[$group_name] = array_merge($elements[$group_name], $element[$group_name]); + } + $form[$group_name] = $elements[$group_name]; + + // Unset the original group field values now that we've moved them. + foreach (array_keys($group_fields) as $field_name) { + unset($form[$group_name][$field_name]); + } + + if (($add_more = content_multigroup_add_more($form, $form_state, $group)) !== FALSE) { + $form[$group_name] += $add_more; + } +} + +/** + * Create a new delta value for the group. + * + * Called in form_alter and by AHAH add more. + */ +function content_multigroup_group_form(&$form, &$form_state, $group, $delta) { + module_load_include('inc', 'content', 'includes/content.node_form'); + $element = array(); + $type_name = $group['type_name']; + $content_type = content_types($type_name); + $group_name = $group['group_name']; + + if (!isset($form[$group_name])) {//nested AHAH, not initial build + $element[$group_name] = content_get_form_element($group_name, $type_name, $form, TRUE); + } + else {//initial build (via content_multigroup_fieldgroup_form) or non-nested AHAH + $element[$group_name] = $form[$group_name]; + } + if (($group['group_type'] != 'multigroup') + || (!(empty($element[$group['group_name']]['#access'])) && $element[$group['group_name']]['#access'] != TRUE) + || empty($element[$group['group_name']])) { + return; + } + + $group_fields = $form['#multigroups'][$group_name]; + $element[$group_name]['#fields'] = array_keys($group_fields); + $node = $form['#node']; + $group_multiple = $group['settings']['multigroup']['multiple']; + + foreach ($group_fields as $field_name => $field) { + if (empty($element[$group_name][$delta])) { + $element[$group_name] += array($delta => array($field_name => array())); + } + else { + $element[$group_name][$delta][$field_name] = array(); + } + + $item_count = (isset($form_state['item_count'][$group_name]) ? $form_state['item_count'][$group_name] : $element[$group_name]['#item_count']); + $element[$group_name][$delta]['_weight'] = array( + '#type' => 'weight', + '#delta' => $item_count, // this 'delta' is the 'weight' element's property + '#default_value' => $delta, + '#weight' => 100, + ); + + // Add a checkbox to allow users remove a single delta subgroup. + // See content_set_empty() and theme_content_multigroup_node_form(). + if ($group_multiple == 1) { + $element[$group_name][$delta]['_remove'] = array( + '#type' => 'checkbox', + '#attributes' => array('class' => 'content-multiple-remove-checkbox'), + '#default_value' => isset($form_state['multigroup_removed'][$group_name][$delta]) ? $form_state['multigroup_removed'][$group_name][$delta] : 0, + ); + } + + // Make each field into a pseudo single value field + // with the right delta value. + $field['multiple'] = 0; + + $form['#field_info'][$field_name] = $field; + $node_copy = drupal_clone($node); + + // Set the form '#node' to the delta value we want so the Content + // module will feed the right $items to the field module in + // content_field_form(). + + // There may be missing delta values for fields that were + // never created, so check first. + if (!empty($node->$field_name) && isset($node->{$field_name}[$delta])) { + $node_copy->$field_name = array($delta => $node->{$field_name}[$delta]); + } + else { + $value = NULL; + // Try to obtain default values only if the node is being created. + if (!isset($node->nid) && content_callback('widget', 'default value', $field) != CONTENT_CALLBACK_NONE) { + // If a module wants to insert custom default values here, + // it should provide a hook_default_value() function to call, + // otherwise the content module's content_default_value() function + // will be used. + $callback = content_callback('widget', 'default value', $field) == CONTENT_CALLBACK_CUSTOM ? $field['widget']['module'] .'_default_value' : 'content_default_value'; + if (function_exists($callback)) { + $items = $callback($form, $form_state, $field, 0); + $value = $items[0]; + } + } + $node_copy->$field_name = array($delta => $value); + } + $form['#node'] = $node_copy; + + // Place the new element into the $delta position in the group form. + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_CORE) { + $field_form = content_field_form($form, $form_state, $field, $delta); + $value = array_key_exists($delta, $field_form[$field_name]) ? $delta : 0; + $element[$group_name][$delta][$field_name] = $field_form[$field_name][$value]; + } + else { + // When the form is submitted, get the element data from the form values. + if (isset($form_state['values'][$field_name])) { + $form_state_copy = $form_state; + if (isset($form_state_copy['values'][$field_name][$delta])) { + $form_state_copy['values'][$field_name] = array($delta => $form_state_copy['values'][$field_name][$delta]); + } + else { + $form_state_copy['values'][$field_name] = array($delta => NULL); + } + $field_form = content_field_form($form, $form_state_copy, $field, $delta); + } + else { + $field_form = content_field_form($form, $form_state, $field, $delta); + } + + // Multiple value fields have an additional level in the array form that + // needs to get fixed in $form_state['values']. + if (!isset($field_form[$field_name]['#element_validate'])) { + $field_form[$field_name]['#element_validate'] = array(); + } + $field_form[$field_name]['#element_validate'][] = 'content_multigroup_fix_multivalue_fields'; + + $element[$group_name][$delta][$field_name] = $field_form[$field_name]; + } + $element[$group_name][$delta][$field_name]['#weight'] = $field['widget']['weight']; + } + + // Reset the form '#node' back to its original value. + $form['#node'] = $node; + + return $element; +} + +/** + * Fix required flag during form rendering stage. + * + * Required fields should display the required star in the rendered form. + */ +function content_multigroup_node_form_pre_render(&$form) { + foreach ($form['#multigroups'] as $group_name => $group_fields) { + $required_fields = array(); + foreach ($group_fields as $field_name => $field) { + if ($field['required']) { + $required_fields[] = $field_name; + } + } + if (!empty($required_fields)) { + content_multigroup_node_form_fix_required($form[$group_name], $required_fields, TRUE); + } + } + return $form; +} + +/** + * Fix form and posting data when the form is submitted. + * + * FormAPI uses form_builder() during form processing to map incoming $_POST + * data to the proper elements in the form. It builds the '#parents' array, + * copies the $_POST array to the '#post' member of all form elements, and it + * also builds the $form_state['values'] array. Then the '#after_build' hook is + * invoked to allow custom processing of the form structure, and that happens + * just before validation and submit handlers are executed. + * + * During hook_form_alter(), the multigroup module altered the form structure + * moving elements from field->delta to multigroup->delta->field position, + * which is what has been processed by FormAPI to build the form structures, + * but field validation (and submit) handlers expect their data to be located + * in their original positions. + * + * We now need to move the fields back to their original positions in the form, + * and we need to do so without altering the form rendering process, which is + * now reflecting the structure the multigroup is interested in. We just need + * to fix the parts of the form that affect validation and submit processing. + */ +function _content_multigroup_node_form_after_build($form, &$form_state) { + // Disable required flag during FormAPI validation, except when building the + // form for an 'Add more values' request. + $required = !empty($form_state['multigroup_add_more']); + foreach ($form['#multigroups'] as $group_name => $group_fields) { + $required_fields = array(); + foreach ($group_fields as $field_name => $field) { + if ($field['required']) { + $required_fields[] = $field_name; + } + } + if (!empty($required_fields)) { + content_multigroup_node_form_fix_required($form[$group_name], $required_fields, $required); + } + } + + if ($form_state['submitted']) { + // Fix value positions in $form_state for the fields in multigroups. + foreach (array_keys($form['#multigroups']) as $group_name) { + content_multigroup_node_form_transpose_elements($form, $form_state, $form['#node']->type, $group_name); + } + + // Fix form element parents for all fields in multigroups. + content_multigroup_node_form_fix_parents($form, $form['#multigroups']); + + // Update posting data to reflect delta changes in the form structure. + if (!empty($_POST)) { + content_multigroup_node_form_fix_post($form); + } + } + + return $form; +} + +/** + * Fix required flag for required fields. + * + * We need to let the user enter an empty set of fields for a delta subgroup, + * even if it contains required fields, which is equivalent to say a subgroup + * should be ignored, not to be stored into the database. + * So, we need to check for required fields, but only for non-empty subgroups. + * + * When the form is processed for rendering, the required flag is enabled for + * all required fields, so the user can see what's required and what's not. + * + * When the form is processed for validation, the required flag is disabled, + * so that FormAPI does not report errors for empty fields. + * + * @see content_multigroup_node_form_validate(). + */ +function content_multigroup_node_form_fix_required(&$elements, $required_fields, $required) { + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key]) { + + if (count($elements[$key]['#array_parents']) >= 3 && in_array($elements[$key]['#array_parents'][2], $required_fields) && isset($elements[$key]['#required'])) { + $elements[$key]['#required'] = $required; + } + + // Recurse through all children elements. + content_multigroup_node_form_fix_required($elements[$key], $required_fields, $required); + } + } +} + +/** + * Node form validation handler. + * + * Perform validation for empty fields ignoring subgroups flagged for removal. + * Note that FormAPI validation for required fields is disabled because we need + * to accept empty fields that are flagged for removal. + */ +function content_multigroup_node_form_validate($form, &$form_state) { + $type_name = $form['#node']->type; + $groups = fieldgroup_groups($type_name); + + foreach ($form['#multigroups'] as $group_name => $group_fields) { + $group = $groups[$group_name]; + $group_required = isset($group['settings']['multigroup']['required']) ? $group['settings']['multigroup']['required'] : 1; + + $non_empty_subgroups = $non_removed_subgroups = $required_field_errors = array(); + foreach ($group_fields as $field_name => $field) { + // Tell the content module that it is not needed to enforce requirement + // of fields in this multigroup because we are doing it here. + // See content_multiple_value_nodeapi_validate(). + $form_state['values']['_content_ignore_required_fields'][$field_name] = TRUE; + + // FIXME: why is this happening? + // $form_state['values'][$field_name] ends up missing sometimes and $form_state['values'][$group_name][0][$field_name] contains the actual data + // This is a hack/temporary-fix such that whenever this situation happens, the data ends up where it should be + // + if (!array_key_exists($field_name, $form_state['values'])){ + foreach ($form_state['values'][$group_name] as $delta => $item) { + if (is_numeric($delta) && is_array($form_state['values'][$group_name][$delta]) && array_key_exists('_remove', $form_state['values'][$group_name][$delta])){ + // In the case where '_remove' is set to 1 (TRUE), then the field is to be removed. + // Do not bother copying removed fields and they will be auto-deleted from the form and the database! + if (!$form_state['values'][$group_name][$delta]['_remove']){ + $form_state['values'][$field_name][$delta] = $form_state['values'][$group_name][$delta][$field_name]; + } + } + } + } + + foreach ($form_state['values'][$field_name] as $delta => $item) { + // Ignore subgroups flagged for removal. + if ($form_state['multigroup_removed'][$group_name][$delta]) { + continue; + } + // Keep track of non-removed subgroups. + $non_removed_subgroups[$delta] = TRUE; + + $is_empty_function = $field['module'] .'_content_is_empty'; + if ($is_empty_function($form_state['values'][$field_name][$delta], $field)) { + // Ignore fields that are not required. + if (!$field['required']) { + continue; + } + + // Build an error message for this field in this subgroup, but do + // not flag it, yet. + if (!empty($item['_error_element'])) { + // Here we don't know the number of elements and subelements a + // widget could have added to the form, so we need to extract + // components from the top, where we have group/delta/field, and + // then push back field/delta on top of the list. + $error_element = explode('][', $item['_error_element']); + array_shift($error_element); + array_shift($error_element); + array_shift($error_element); + array_unshift($error_element, $field_name, $delta); + $error_element = implode('][', $error_element); + } + else { + $error_element = ''; + } + $required_field_errors[$delta][$field_name] = array( + 'element' => $error_element, + 'message' => t('!name field is required in group @group.', array( + '!name' => $form[$group_name][$delta][$field_name]['#title'], + '@group' => t($group['label']), + )), + ); + } + else { + $non_empty_subgroups[$delta] = TRUE; + } + } + } + + // Required multigroups require at least one non-empty subgroup of fields. + if ($group_required && empty($non_empty_subgroups)) { + form_set_error('', t('Group @name requires one collection of fields minimum.', array('@name' => t($group['label'])))); + continue; + } + + // Do not enforce field requirements if there is only one non-removed + // subgroup of fields, and that subgroup is empty. + if (count($non_removed_subgroups) == 1) { + $delta = key($non_removed_subgroups); + if (isset($required_field_errors[$delta]) && !isset($non_empty_subgroups[$delta])) { + unset($required_field_errors[$delta]); + } + } + + // Ok, now we can flag errors for all required fields that have not been + // filled in when they should. + foreach ($required_field_errors as $delta => $error_list) { + foreach ($error_list as $field_name => $error_info) { + form_set_error($error_info['element'], $error_info['message']); + } + } + } +} + +/** + * Transpose element positions in $form_state for the fields in a multigroup. + */ +function content_multigroup_node_form_transpose_elements(&$form, &$form_state, $type_name, $group_name) { + $groups = fieldgroup_groups($type_name); + $group = $groups[$group_name]; + $group_fields = $form['#multigroups'][$group_name]; + + // Save the remove state of multigroup items in the $form_state array. + if (!isset($form_state['multigroup_removed'])) { + $form_state['multigroup_removed'] = array(); + } + if (!isset($form_state['multigroup_removed'][$group_name])) { + $form_state['multigroup_removed'][$group_name] = array(); + } + + // Move group data from group->delta->field to field->delta. + $group_data = array(); + foreach ($form_state['values'][$group_name] as $delta => $items) { + // Skip 'add more' button. + if (!is_array($items) || !isset($items['_weight'])) { + continue; + } + foreach ($group_fields as $field_name => $field) { + if (!isset($group_data[$field_name])) { + $group_data[$field_name] = array(); + } + // Get field weight and remove state from the group and keep track of the + // current delta for each field item. + $item_defaults = array( + '_weight' => $items['_weight'], + '_remove' => $items['_remove'], + '_old_delta' => $delta, + ); + $group_data[$field_name][$delta] = (is_array($items[$field_name]) ? array_merge($items[$field_name], $item_defaults) : $item_defaults); + // Store the remove state and the element weight in the form element as + // well, so we can restore them later. + // See content_multigroup_fix_multivalue_fields(). + // See content_multigroup_fix_element_values(). + $form[$group_name][$delta][$field_name]['#_weight'] = $items['_weight']; + $form[$group_name][$delta][$field_name]['#removed'] = $items['_remove']; + + // Insert an element valitation callback of our own at the end of the + // list to ensure the drag'n'drop weight of the element is not lost by + // a form_set_value() operation made by the validation callback of the + // widget element. + if (!isset($form[$group_name][$delta][$field_name]['#element_validate'])) { + $form[$group_name][$delta][$field_name]['#element_validate'] = array(); + } + $form[$group_name][$delta][$field_name]['#element_validate'][] = 'content_multigroup_fix_element_values'; + } + $form_state['multigroup_removed'][$group_name][$delta] = $items['_remove']; + } + + $form_group_sorted = FALSE; + foreach ($group_data as $field_name => $items) { + + // Sort field items according to drag-n-drop reordering. Deltas are also + // rebuilt to start counting from 0 to n. Note that since all fields in the + // group share the same weight, their deltas remain in sync. + usort($items, '_content_sort_items_helper'); + + // Now we need to apply the same ordering to the form elements. Also, + // note that deltas have changed during the sort operation, so we need + // to reflect this delta conversion in the form. + if (!$form_group_sorted) { + $form_group_items = array(); + $form_deltas = array(); + foreach ($items as $new_delta => $item) { + $form_deltas[$item['_old_delta']] = $new_delta; + $form_group_items[$new_delta] = $form[$group_name][$item['_old_delta']]; + unset($form[$group_name][$item['_old_delta']]); + } + foreach ($form_group_items as $new_delta => $form_group_item) { + $form[$group_name][$new_delta] = $form_group_item; + } + content_multigroup_node_form_fix_deltas($form[$group_name], $form_deltas); + $form_group_sorted = TRUE; + } + + // Get rid of the old delta value. + foreach (array_keys($items) as $delta) { + unset($items[$delta]['_old_delta']); + } + + // Fix field and delta positions in the $_POST array. + if (!empty($_POST)) { + $_POST[$field_name] = array(); + foreach ($items as $new_delta => $item) { + $_POST[$field_name][$new_delta] = $item; + } + if (isset($_POST[$group_name])) { + unset($_POST[$group_name]); + } + } + + // Move field items back to their original positions. + $form_state['values'][$field_name] = $items; + } + + // Finally, get rid of the group data in form values. + unset($form_state['values'][$group_name]); +} + +/** + * Fix deltas for all affected form elements. + */ +function content_multigroup_node_form_fix_deltas(&$elements, $form_deltas) { + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key]) { + + // Fix the second item, the delta value, of the element's '#parents' array. + $elements[$key]['#parents'][1] = $form_deltas[$elements[$key]['#parents'][1]]; + + // If present, fix delta value in '#delta' attribute of the element. + if (isset($elements[$key]['#delta']) && isset($form_deltas[$elements[$key]['#delta']])) { + $elements[$key]['#delta'] = $form_deltas[$elements[$key]['#delta']]; + } + + // Recurse through all children elements. + content_multigroup_node_form_fix_deltas($elements[$key], $form_deltas); + } + } +} + +/** + * Fix form element parents for all fields in multigroups. + * + * The $element['#parents'] array needs to reflect the position of the fields + * in the $form_state['values'] array so that form_set_value() can be safely + * used by field validation handlers. + */ +function content_multigroup_node_form_fix_parents(&$elements, $multigroups) { + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key]) { + + // Check if the current element is child of a multigroup. The #parents + // array for field values has, at least, 3 parent elements, being the + // first one the name of a multigroup. + if (count($elements[$key]['#parents']) >= 3 && isset($multigroups[$elements[$key]['#parents'][0]])) { + + // Extract group name, delta and field name from the #parents array. + array_shift($elements[$key]['#parents']); + $delta = array_shift($elements[$key]['#parents']); + $field_name = array_shift($elements[$key]['#parents']); + + // Now, insert field name and delta to the #parents array. + array_unshift($elements[$key]['#parents'], $field_name, $delta); + } + + // Recurse through all children elements. + content_multigroup_node_form_fix_parents($elements[$key], $multigroups); + } + } +} + +/** + * Update posting data to reflect delta changes in the form structure. + * + * The $_POST array is fixed in content_multigroup_node_form_transpose_elements(). + */ +function content_multigroup_node_form_fix_post(&$elements) { + foreach (element_children($elements) as $key) { + if (isset($elements[$key]) && $elements[$key]) { + + // Update the element copy of the $_POST array. + $elements[$key]['#post'] = $_POST; + + // Recurse through all children elements. + content_multigroup_node_form_fix_post($elements[$key]); + } + } + + // Update the form copy of the $_POST array. + $elements['#post'] = $_POST; +} + +/** + * Make sure the '_weight' and '_remove' attributes of the element exist. + * + * @see content_multigroup_node_form_transpose_elements() + */ +function content_multigroup_fix_element_values($element, &$form_state) { + $field_name = $element['#field_name']; + $delta = $element['#delta']; + if (!isset($form_state['values'][$field_name][$delta]['_weight']) || !isset($form_state['values'][$field_name][$delta]['_remove'])) { + $value = array('_weight' => $element['#_weight'], '_remove' => $element['#removed']); + if (isset($form_state['values'][$field_name][$delta]) && is_array($form_state['values'][$field_name][$delta])) { + $value = array_merge($form_state['values'][$field_name][$delta], $value); + } + form_set_value($element, $value, $form_state); + } +} + +/** + * Fix the value for fields that deal with multiple values themselves. + */ +function content_multigroup_fix_multivalue_fields($element, &$form_state) { + $field_name = $element['#field_name']; + $delta = $element['#delta']; + if (isset($form_state['values'][$field_name][$delta][0]) && is_array($form_state['values'][$field_name][$delta][0])) { + $value = array_merge($form_state['values'][$field_name][$delta][0], array('_remove' => $element['#removed'])); + } + else { + $value = array('_remove' => $element['#removed']); + } + form_set_value($element, $value, $form_state); +} + +/** + * Add AHAH add more button, if not working with a programmed form. + */ +function content_multigroup_add_more(&$form, &$form_state, $group) { + $group_multiple = $group['settings']['multigroup']['multiple']; + if ($group_multiple != 1 || !empty($form['#programmed'])) { + return FALSE; + } + + // Make sure the form is cached so ahah can work. + $form['#cache'] = TRUE; + $content_type = content_types($group['type_name']); + $group_name = $group['group_name']; + $group_name_css = str_replace('_', '-', $group_name); + + $form_element = array(); + $form_element[$group_name .'_add_more'] = array( + '#type' => 'submit', + '#name' => $group_name .'_add_more', + '#value' => t('Add more values'), + '#weight' => $group_multiple + 1, + '#submit' => array('content_multigroup_add_more_submit'), + '#ahah' => array( + 'path' => 'content_multigroup/js_add_more/'. $content_type['url_str'] .'/'. $group_name, + 'wrapper' => $group_name_css .'-items', + 'method' => 'replace', + 'effect' => 'fade', + ), + // When JS is disabled, the content_multigroup_add_more_submit handler will + // find the relevant group information using these entries. + '#group_name' => $group_name, + '#type_name' => $group['type_name'], + '#item_count' => $form[$group_name]['#item_count'], + ); + + // Add wrappers for the group and 'more' button. + $form_element['#prefix'] = '
'; + $form_element['#suffix'] = '
'; + $form_element[$group_name .'_add_more']['#prefix'] = '
'; + $form_element[$group_name .'_add_more']['#suffix'] = '
'; + + return $form_element; +} + +/** + * Submit handler to add more choices to a content form. This handler is used when + * JavaScript is not available. It makes changes to the form state and the + * entire form is rebuilt during the page reload. + */ +function content_multigroup_add_more_submit($form, &$form_state) { + // Set the form to rebuild and run submit handlers. + node_form_submit_build_node($form, $form_state); + $group_name = $form_state['clicked_button']['#group_name']; + $type_name = $form_state['clicked_button']['#type_name']; + + // Make the changes we want to the form state. + if (isset($form_state['clicked_button']['#item_count'])) { + $form_state['item_count'][$group_name] = $form_state['clicked_button']['#item_count'] + 1; + } +} + +/** + * Menu callback for AHAH addition of new empty widgets. + * + * Adapted from content_add_more_js to work with groups instead of fields. + */ +function content_multigroup_add_more_js($type_name_url, $group_name) { + $content_type = content_types($type_name_url); + $groups = fieldgroup_groups($content_type['type']); + $group = $groups[$group_name]; + + if (($group['settings']['multigroup']['multiple'] != 1) || empty($_POST['form_build_id'])) { + // Invalid request. + drupal_json(array('data' => '')); + exit; + } + + // Retrieve the cached form. + $form_state = array('submitted' => FALSE); + $form_build_id = $_POST['form_build_id']; + $form = form_get_cache($form_build_id, $form_state); + if (!$form) { + // Invalid form_build_id. + drupal_json(array('data' => '')); + exit; + } + + // We don't simply return a new empty widget to append to existing ones, because + // - ahah.js won't simply let us add a new row to a table + // - attaching the 'draggable' behavior won't be easy + // So we resort to rebuilding the whole table of widgets including the existing ones, + // which makes us jump through a few hoops. + + // The form that we get from the cache is unbuilt. We need to build it so that + // _value callbacks can be executed and $form_state['values'] populated. + // We only want to affect $form_state['values'], not the $form itself + // (built forms aren't supposed to enter the cache) nor the rest of $form_data, + // so we use copies of $form and $form_data. + $form_copy = $form; + $form_state_copy = $form_state; + $form_copy['#post'] = array(); + form_builder($_POST['form_id'], $form_copy, $form_state_copy); + // Just grab the data we need. + $form_state['values'] = $form_state_copy['values']; + // Reset cached ids, so that they don't affect the actual form we output. + form_clean_id(NULL, TRUE); + + // Sort the $form_state['values'] we just built *and* the incoming $_POST data + // according to d-n-d reordering. + unset($form_state['values'][$group_name][$group['group_name'] .'_add_more']); + foreach ($_POST[$group_name] as $delta => $item) { + $form_state['values'][$group_name][$delta]['_weight'] = $item['_weight']; + $form_state['values'][$group_name][$delta]['_remove'] = isset($item['_remove']) ? $item['_remove'] : 0; + } + $group['multiple'] = $group['settings']['multigroup']['multiple']; + $form_state['values'][$group_name] = _content_sort_items($group, $form_state['values'][$group_name]); + $_POST[$group_name] = _content_sort_items($group, $_POST[$group_name]); + + // Build our new form element for the whole group, asking for one more element. + $delta = max(array_keys($_POST[$group_name])) + 1; + $form_state['item_count'] = array($group_name => count($_POST[$group_name]) + 1); + $form_element = content_multigroup_group_form($form, $form_state, $group, $delta); + + // Rebuild weight deltas to make sure they all are equally dimensioned. + foreach ($form_element[$group_name] as $key => $item) { + if (is_numeric($key) && isset($item['_weight']) && is_array($item['_weight'])) { + $form_element[$group_name][$key]['_weight']['#delta'] = $delta; + } + } + // Add the new element at the right place in the (original, unbuilt) form. + content_set_form_element($group_name, $content_type['type'], $form, $form_element[$group_name], TRUE); + + // Save the new definition of the form. + $form_state['values'] = array(); + form_set_cache($form_build_id, $form, $form_state); + + // Build the new form against the incoming $_POST values so that we can + // render the new element. + $_POST[$group_name][$delta]['_weight'] = $delta; + $form_state = array('submitted' => FALSE, 'multigroup_add_more' => TRUE); + $form += array( + '#post' => $_POST, + '#programmed' => FALSE, + ); + $form = form_builder($_POST['form_id'], $form, $form_state); + + // Render the new output. + $group_form = content_get_form_element($group_name, $content_type['type'], $form, TRUE); + // We add a div around the new content to receive the ahah effect. + $group_form[$delta]['#prefix'] = '
'. (isset($group_form[$delta]['#prefix']) ? $group_form[$delta]['#prefix'] : ''); + $group_form[$delta]['#suffix'] = (isset($group_form[$delta]['#suffix']) ? $group_form[$delta]['#suffix'] : '') .'
'; + // Prevent duplicate wrapper. + unset($group_form['#prefix'], $group_form['#suffix']); + // We're in the AHAH handler, so the fieldset was expanded. + $group_form['#collapsed'] = FALSE; + + // If a newly inserted widget contains AHAH behaviors, they normally won't + // work because AHAH doesn't know about those - it just attaches to the exact + // form elements that were initially specified in the Drupal.settings object. + // The new ones didn't exist then, so we need to update Drupal.settings + // by ourselves in order to let AHAH know about those new form elements. + $javascript = drupal_add_js(NULL, NULL); + $output_js = isset($javascript['setting']) ? '' : ''; + + $output = theme('status_messages') . drupal_render($group_form) . $output_js; + + // Using drupal_json() breaks filefield's file upload, because the jQuery + // Form plugin handles file uploads in a way that is not compatible with + // 'text/javascript' response type. + $GLOBALS['devel_shutdown'] = FALSE; + print drupal_to_js(array('status' => TRUE, 'data' => $output)); + exit; +} + +/** + * Theme an individual form element. + * + * Combine multiple values into a table with drag-n-drop reordering. + */ +function theme_content_multigroup_node_form($element) { + $group_name = $element['#group_name']; + $groups = fieldgroup_groups($element['#type_name']); + $group = $groups[$group_name]; + $group_multiple = $group['settings']['multigroup']['multiple']; + $group_fields = $element['#group_fields']; + + $table_id = $element['#group_name'] .'_values'; + $table_class = 'content-multiple-table'; + $order_class = $element['#group_name'] .'-delta-order'; + $subgroup_settings = isset($group['settings']['multigroup']['subgroup']) ? $group['settings']['multigroup']['subgroup'] : array(); + $show_label = isset($subgroup_settings['label']) ? $subgroup_settings['label'] : 'above'; + $subgroup_labels = isset($group['settings']['multigroup']['labels']) ? $group['settings']['multigroup']['labels'] : array(); + $multiple_columns = isset($group['settings']['multigroup']['multiple-columns']) ? $group['settings']['multigroup']['multiple-columns'] : 0; + + $headers = array(); + if ($group_multiple >= 1) { + $headers[] = array('data' => ''); + } + if ($multiple_columns) { + foreach ($group_fields as $field_name => $field) { + $required = !empty($field['required']) ? ' *' : ''; + $headers[] = array( + 'data' => check_plain(t($field['widget']['label'])) . $required, + 'class' => 'content-multigroup-cell-'. str_replace('_', '-', $field_name), + ); + } + $table_class .= ' content-multigroup-edit-table-multiple-columns'; + } + else { + if ($group_multiple >= 1) { + $headers[0]['colspan'] = 2; + } + $table_class .= ' content-multigroup-edit-table-single-column'; + } + if ($group_multiple >= 1) { + $headers[] = array('data' => t('Order'), 'class' => 'content-multiple-weight-header'); + if ($group_multiple == 1) { + $headers[] = array('data' => ''. t('Remove') .'', 'class' => 'content-multiple-remove-header'); + } + } + $rows = array(); + + $i = 0; + foreach (element_children($element) as $delta => $key) { + if (is_numeric($key)) { + $cells = array(); + $label = ($show_label == 'above' && !empty($subgroup_labels[$i]) ? theme('content_multigroup_node_label', check_plain(t($subgroup_labels[$i]))) : ''); + $element[$key]['_weight']['#attributes']['class'] = $order_class; + if ($group_multiple >= 1) { + $cells[] = array('data' => '', 'class' => 'content-multiple-drag'); + $delta_element = drupal_render($element[$key]['_weight']); + if ($group_multiple == 1) { + $remove_element = drupal_render($element[$key]['_remove']); + } + } + else { + $element[$key]['_weight']['#type'] = 'hidden'; + } + if ($multiple_columns) { + foreach ($group_fields as $field_name => $field) { + $cell = array( + 'data' => (isset($element[$key][$field_name]) ? drupal_render($element[$key][$field_name]) : ''), + 'class' => 'content-multigroup-cell-'. str_replace('_', '-', $field_name), + ); + if (!empty($cell['data']) && !empty($element[$key][$field_name]['#description'])) { + $cell['title'] = $element[$key][$field_name]['#description']; + } + $cells[] = $cell; + } + } + else { + $cells[] = $label . drupal_render($element[$key]); + } + if ($group_multiple >= 1) { + $row_class = 'draggable'; + $cells[] = array('data' => $delta_element, 'class' => 'delta-order'); + if ($group_multiple == 1) { + if (!empty($element[$key]['_remove']['#value'])) { + $row_class .= ' content-multiple-removed-row'; + } + $cells[] = array('data' => $remove_element, 'class' => 'content-multiple-remove-cell'); + } + $rows[] = array('data' => $cells, 'class' => $row_class); + } + else { + $rows[] = array('data' => $cells); + } + } + $i++; + } + + drupal_add_css(drupal_get_path('module', 'content_multigroup') .'/content_multigroup.css'); + $output = theme('table', $headers, $rows, array('id' => $table_id, 'class' => $table_class)); + $output .= drupal_render($element[$group_name .'_add_more']); + + // Enable drag-n-drop only if the group really allows multiple values. + if ($group_multiple >= 1) { + drupal_add_tabledrag($table_id, 'order', 'sibling', $order_class); + drupal_add_js(drupal_get_path('module', 'content') .'/js/content.node_form.js'); + } + + return $output; +} + +/** + * Theme the sub group label in the node form. + */ +function theme_content_multigroup_node_label($text) { + return !empty($text) ? '

'. $text .'

' : ''; +} Index: modules/content_multigroup/content_multigroup.node_view.inc =================================================================== --- modules/content_multigroup/content_multigroup.node_view.inc (revision 0) +++ modules/content_multigroup/content_multigroup.node_view.inc (revision 0) @@ -0,0 +1,237 @@ + $field) { + if (isset($group['fields'][$field_name]) && isset($element[$field_name])) { + if (!isset($element[$field_name]['#access']) || $element[$field_name]['#access']) { + $group_fields[$field_name] = $field; + } + } + } + + switch ($group_multiple) { + case 0: + $group_deltas = array(0); + break; + + case 1: + // Compute unique deltas from all deltas used by fields in this multigroup. + $group_deltas = array(); + foreach (array_keys($group_fields) as $field_name) { + if (is_array($node->content[$field_name]['field']['items'])) { + foreach (array_keys($node->content[$field_name]['field']['items']) as $delta) { + $group_deltas[$delta] = $delta; + } + } + } + sort($group_deltas); + break; + + default: + $group_deltas = range(0, $group_multiple - 1); + break; + } + + foreach ($group_deltas as $index => $delta) { + $element[$delta] = array( + '#title' => ($show_label == 'above' && !empty($subgroup_labels[$index]) ? check_plain(t($subgroup_labels[$index])) : ''), + '#attributes' => array('class' => 'content-multigroup-wrapper content-multigroup-'. $index), + '#weight' => $delta, + ); + + // Create a pseudo node that only has the value we want in this group and + // pass it to the formatter. + // Default implementation of content-field.tpl.php uses a different CSS + // class for inline labels when delta is zero, but this is not needed in + // the context of multigroup, so we place the field into index 1 of the + // item list. Note that CSS class "field-label-inline" is overridden in the + // multigroup stylesheet because here labels should always be visible. + foreach (array_keys($group_fields) as $field_name) { + if (isset($node->content[$field_name])) { + $node_copy->content[$field_name]['field']['items'] = array( + 1 => isset($node->content[$field_name]['field']['items'][$delta]) ? $node->content[$field_name]['field']['items'][$delta] : NULL, + ); + $element[$delta][$field_name] = $node_copy->content[$field_name]; + $element[$delta][$field_name]['#delta'] = $delta; + } + } + + switch ($subgroup_format) { + case 'simple': + $element['#attributes']['class'] = $group_class; + $element[$delta]['#theme'] = 'content_multigroup_display_simple'; + $element[$delta]['#fields'] = $group_fields; + break; + case 'fieldset_collapsed': + $element[$delta]['#collapsed'] = TRUE; + case 'fieldset_collapsible': + $element[$delta]['#collapsible'] = TRUE; + case 'fieldset': + $element['#attributes']['class'] = $group_class; + $element[$delta]['#type'] = 'content_multigroup_display_fieldset'; + $element[$delta]['#fields'] = $group_fields; + break; + case 'hr': + $element['#attributes']['class'] = $group_class; + $element[$delta]['#theme'] = 'content_multigroup_display_hr'; + $element[$delta]['#fields'] = $group_fields; + break; + case 'table-single': + $element['#theme'] = 'content_multigroup_display_table_single'; + $element['#attributes']['class'] = $group_class .' content-multigroup-display-table-single-column'; + $element['#fields'] = $group_fields; + break; + case 'table-multiple': + $element['#theme'] = 'content_multigroup_display_table_multiple'; + $element['#attributes']['class'] = $group_class .' content-multigroup-display-table-multiple-columns'; + $element['#fields'] = $group_fields; + break; + } + } + + // Unset the original group field values now that we've moved them. + foreach (array_keys($group_fields) as $field_name) { + if (isset($element[$field_name])) { + unset($element[$field_name]); + } + } +} + +/** + * Theme a subgroup of fields in 'simple' format. + * + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_simple($element) { + $children = $output = ''; + foreach (element_children($element) as $key) { + $children .= drupal_render($element[$key]); + } + if (!empty($children)) { + $output .= ''; + if (!empty($element['#title'])) { + $output .= ''; + } + $output .= $children .'
'; + } + return $output; +} + +/** + * Theme a subgroup of fields in 'fieldset' format. + * + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_fieldset($element) { + if (empty($element['#children']) && empty($element['#value'])) { + return ''; + } + return theme('fieldset', $element); +} + +/** + * Theme a subgroup of fields in 'hr' format. + * + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_hr($element) { + $children = $output = ''; + foreach (element_children($element) as $key) { + $children .= drupal_render($element[$key]); + } + if (!empty($children)) { + $output .= '
'; + if (!empty($element['#title'])) { + $output .= ''; + } + $output .= $children .''; + } + return $output; +} + +/** + * Theme a multigroup in 'table-single' format. + * + * Each subgroup has its own table row with a single cell for all fields. + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_table_single($element) { + $headers = array(); + $rows = array(); + foreach (element_children($element) as $delta) { + $items = array(); + foreach ($element['#fields'] as $field_name => $field) { + $item = drupal_render($element[$delta][$field_name]); + if (!empty($item)) { + $items[] = $item; + } + } + if (!empty($items)) { + if (!empty($element[$delta]['#title'])) { + array_unshift($items, ''); + } + $rows[] = array('data' => array(implode("\n", $items)), 'class' => $element[$delta]['#attributes']['class']); + } + } + return count($rows) ? theme('table', $headers, $rows, $element['#attributes']) : ''; +} + +/** + * Theme a multigroup in 'table-multiple' format. + * + * Each subgroup has its own table row with a separate cell for each field. + * No output is generated if all fields are empty. + */ +function theme_content_multigroup_display_table_multiple($element) { + $headers = array(); + foreach ($element['#fields'] as $field_name => $field) { + $label_display = isset($field['display_settings']['label']['format']) ? $field['display_settings']['label']['format'] : 'above'; + $headers[] = array( + 'data' => ($label_display != 'hidden' ? check_plain(t($field['widget']['label'])) : ''), + 'class' => 'content-multigroup-cell-'. str_replace('_', '-', $field_name), + ); + } + $rows = array(); + foreach (element_children($element) as $delta) { + $cells = array(); + $empty = TRUE; + foreach ($element['#fields'] as $field_name => $field) { + $item = drupal_render($element[$delta][$field_name]); + $cells[] = array( + 'data' => $item, + 'class' => $element[$delta]['#attributes']['class'] .' content-multigroup-cell-'. str_replace('_', '-', $field_name), + ); + if (!empty($item)) { + $empty = FALSE; + } + } + // Get the row only if there is at least one non-empty field. + if (!$empty) { + $rows[] = $cells; + } + } + return count($rows) ? theme('table', $headers, $rows, $element['#attributes']) : ''; +} Index: modules/content_multigroup/panels/content_types/content_multigroup.inc =================================================================== --- modules/content_multigroup/panels/content_types/content_multigroup.inc (revision 0) +++ modules/content_multigroup/panels/content_types/content_multigroup.inc (revision 0) @@ -0,0 +1,175 @@ + t('Content multigroup'), + 'defaults' => array('label' => 'hidden', 'format' => 'simple', 'subgroup' => 'fieldset', 'empty' => ''), + ); +} + +/** + * Return all multigroup content types available. + */ +function content_multigroup_content_multigroup_content_type_content_types() { + // This will hold all the individual multigroup content types. + $types = array(); + + // The outer loop goes through each node type with groups. + foreach (fieldgroup_groups() as $node_type_groups) { + // The inner loop gives us each fieldgroup on each node type with groups. + foreach ($node_type_groups as $group) { + // Skip field groups that are not of multigroup type. + if ($group['group_type'] != 'multigroup') { + continue; + } + + // Name the content type a combination of fieldgroup and node type names. + $content_type_name = $group['type_name'] . ':' . $group['group_name']; + + // Assemble the information about the content type. + $info = array( + 'category' => t('Node'), + 'icon' => 'icon_cck_field_group.png', + 'title' => t('Multigroup: @group in @type', array( + '@group' => t($group['label']), + '@type' => node_get_types('name', $group['type_name']), + )), + 'description' => t('All fields from this field group on the referenced node.'), + 'required context' => new ctools_context_required(t('Node'), 'node', array('type' => array($group['type_name']))), + ); + + $types[$content_type_name] = $info; + } + } + + return $types; +} + +/** + * Output function for the 'multigroup' content type. + */ +function content_multigroup_content_multigroup_content_type_render($subtype, $conf, $panel_args, $context) { + if (!isset($context->data)) { + return; + } + $node = drupal_clone($context->data); + + // Extract the node type and fieldgroup name from the subtype. + list($node_type, $group_name) = explode(':', $subtype, 2); + + // Get a list of all fieldgroups for this node type. + $groups = fieldgroup_groups($node_type); + + if (!isset($groups[$group_name])) { + return; + } + $group = $groups[$group_name]; + + // Render the field group. + $node->build_mode = NODE_BUILD_NORMAL; + $group['settings']['display']['label'] = $conf['label'] == 'normal' || !empty($conf['override_title']) ? 'hidden' : $conf['label']; + $group['settings']['display']['full']['format'] = $conf['format']; + $group['settings']['display']['full']['exclude'] = 0; + $group['settings']['multigroup']['subgroup']['full']['format'] = $conf['subgroup']; + $group['settings']['multigroup']['subgroup']['full']['exclude'] = 0; + $output = fieldgroup_view_group($group, $node); + + $block = new stdClass(); + if ($conf['label'] == 'normal') { + $block->title = t($group['label']); + } + $block->content = !empty($output) ? $output : $conf['empty']; + return $block; +} + +/** + * Returns a settings form for the custom type. + */ +function content_multigroup_content_multigroup_content_type_edit_form(&$form, &$form_state) { + $conf = $form_state['conf']; + + $label_options = array( + 'normal' => t('Block title'), + 'above' => t('Above'), + ); + $form['label'] = array( + '#type' => 'select', + '#title' => t('Multigroup label'), + '#default_value' => !empty($conf['label']) && isset($label_options[$conf['label']]) ? $conf['label'] : 'hidden', + '#options' => $label_options, + '#description' => t('Configure how the field group label is going to be displayed. This option takes no effect when "Override title" option is enabled, the specified block title is displayed instead.'), + ); + + $format_options = array( + 'simple' => t('Simple'), + 'fieldset' => t('Fieldset'), + 'fieldset_collapsible' => t('Fieldset - Collapsible'), + 'fieldset_collapsed' => t('Fieldset - Collapsed'), + ); + $form['format'] = array( + '#type' => 'select', + '#title' => t('Multigroup format'), + '#default_value' => !empty($conf['format']) && isset($format_options[$conf['format']]) ? $conf['format'] : 'simple', + '#options' => $format_options, + '#description' => t('This option allows you to configure the field group format.'), + ); + + $subgroup_options = array( + 'simple' => t('Simple'), + 'fieldset' => t('Fieldset'), + 'fieldset_collapsible' => t('Fieldset - collapsible'), + 'fieldset_collapsed' => t('Fieldset - collapsed'), + 'hr' => t('Horizontal line'), + 'table-single' => t('Table - Single column'), + 'table-multiple' => t('Table - Multiple columns'), + ); + $form['subgroup'] = array( + '#type' => 'select', + '#title' => t('Subgroup format'), + '#default_value' => !empty($conf['subgroup']) && isset($subgroup_options[$conf['subgroup']]) ? $conf['subgroup'] : 'fieldset', + '#options' => $subgroup_options, + '#description' => t('This option allows you to configure the format of the subgroups in the multigroup.'), + ); + + $form['empty'] = array( + '#type' => 'textarea', + '#title' => t('Empty text'), + '#description' => t('Text to display if group has no data. Note that title will not display unless overridden.'), + '#rows' => 5, + '#default_value' => $conf['empty'], + ); +} + +function content_multigroup_content_multigroup_content_type_edit_form_submit(&$form, &$form_state) { + // Copy everything from our defaults. + foreach (array_keys($form_state['plugin']['defaults']) as $key) { + $form_state['conf'][$key] = $form_state['values'][$key]; + } +} + +/** + * Admin title for multigroup content type. + */ +function content_multigroup_content_multigroup_content_type_admin_title($subtype, $conf, $context) { + // Extract the node type and fieldgroup name from the subtype. + list($node_type, $group_name) = explode(':', $subtype, 2); + + // Get information about this field group for this node type. + $groups = fieldgroup_groups($node_type); + $group = $groups[$group_name]; + + return t('"@s" multigroup: @group in @type', array( + '@s' => $context->identifier, + '@group' => t($group['label']), + '@type' => node_get_types('name', $node_type), + )); +} Index: modules/content_multigroup/panels/content_types/icon_cck_field_group.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: modules\content_multigroup\panels\content_types\icon_cck_field_group.png ___________________________________________________________________ Added: svn:mime-type + application/octet-stream Index: modules/content_multigroup/panels/content_types/translations/de.po =================================================================== --- modules/content_multigroup/panels/content_types/translations/de.po (revision 0) +++ modules/content_multigroup/panels/content_types/translations/de.po (revision 0) @@ -0,0 +1,51 @@ +# $Id$ +# +# LANGUAGE translation of Drupal (modules-content_multigroup-panels-content_types) +# Copyright 2009 NAME +# Generated from file: content_multigroup.inc,v 1.1.2.3 2009/11/01 10:44:16 markuspetrux +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"POT-Creation-Date: 2009-12-06 15:33+0100\n" +"PO-Revision-Date: 2009-12-06 22:17+0100\n" +"Last-Translator: Thomas Zahreddin \n" +"Language-Team: German \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-Language: German\n" +"X-Poedit-Country: German\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: modules/content_multigroup/panels/content_types/content_multigroup.inc:42 +#, fuzzy +msgid "Multigroup: @group in @type" +msgstr "Vielfachgruppe: @group in @type" + +#: modules/content_multigroup/panels/content_types/content_multigroup.inc:106 +#, fuzzy +msgid "Multigroup label" +msgstr "Bezeichnung der Vielfachgruppe" + +#: modules/content_multigroup/panels/content_types/content_multigroup.inc:120 +#, fuzzy +msgid "Multigroup format" +msgstr "Format für Vielfachgruppe" + +#: modules/content_multigroup/panels/content_types/content_multigroup.inc:137 +#, fuzzy +msgid "Subgroup format" +msgstr "Format für Untergruppe" + +#: modules/content_multigroup/panels/content_types/content_multigroup.inc:140 +#, fuzzy +msgid "This option allows you to configure the format of the subgroups in the multigroup." +msgstr "Konfigurieren des Formats für eine Subgruppe innerhalb einer Vielfachgruppe." + +#: modules/content_multigroup/panels/content_types/content_multigroup.inc:170 +#, fuzzy +msgid "\"@s\" multigroup: @group in @type" +msgstr "\"@s\" Vielfachgruppe: @group in @type" + Index: modules/content_multigroup/translations/modules-content_multigroup.de.po =================================================================== --- modules/content_multigroup/translations/modules-content_multigroup.de.po (revision 0) +++ modules/content_multigroup/translations/modules-content_multigroup.de.po (revision 0) @@ -0,0 +1,128 @@ +# $Id$ +# +# LANGUAGE translation of Drupal (modules-content_multigroup) +# Copyright 2009 NAME +# Generated from files: +# content_multigroup.admin.inc,v 1.1.2.5 2009/11/02 19:22:37 markuspetrux +# content_multigroup.node_form.inc,v 1.1.2.12 2009/11/03 19:16:25 markuspetrux +# content_multigroup.module,v 1.1.4.6 2009/11/02 19:22:37 markuspetrux +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"POT-Creation-Date: 2009-12-06 15:33+0100\n" +"PO-Revision-Date: 2009-12-06 22:25+0100\n" +"Last-Translator: Thomas Zahreddin \n" +"Language-Team: German \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-Language: German\n" +"X-Poedit-Country: Germany\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: modules/content_multigroup/content_multigroup.admin.inc:90 +msgid "The field %field has been updated to use %multiple values, to match the multiple value setting of the Multigroup %group." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:129 +msgid "This change is not allowed. The field %field already has %multiple values in the database but the group %group only allows %group_max. Making this change would result in the loss of data." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:153 +msgid "This change is not allowed. The field %field handles multiple values differently than the Content module. Making this change could result in the loss of data." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:168 +msgid "You are moving the field %field into a Multigroup." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:201 +msgid "This change is not allowed. The field %field already has data created and uses a widget that stores data differently in a Standard group than in a Multigroup. Making this change could result in the loss of data." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:215 +msgid "You are moving the field %field out of a Multigroup." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:250 +msgid "The widget type cannot be changed because the field %field already has data created and this widget stores data differently in a Standard group than in a Multigroup. Allowing this change could result in the loss of data." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:313 +msgid "[Subgroup format]" +msgstr "[Subgroup format]" + +#: modules/content_multigroup/content_multigroup.admin.inc:399 +msgid "Multigroup settings" +msgstr "Multigroup Einstellungen" + +#: modules/content_multigroup/content_multigroup.admin.inc:414 +msgid "Multiple columns" +msgstr "Mehrere Spalten" + +#: modules/content_multigroup/content_multigroup.admin.inc:416 +msgid "Enable this option to render each field on a separate column on the node edit form." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:423 +msgid "Enable this option to require a minimum of one collection of fields in this Multigroup." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:426 +msgid "Number of times to repeat the collection of Multigroup fields." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:427 +msgid "'Unlimited' will provide an 'Add more' button so the users can add items as many times as they like." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:428 +msgid "All fields in this group will automatically be set to allow this number of values." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:433 +msgid "Number of repeats" +msgstr "Anzahl von Wiederholungen" + +#: modules/content_multigroup/content_multigroup.admin.inc:441 +msgid "Labels" +msgstr "Bezeichner" + +#: modules/content_multigroup/content_multigroup.admin.inc:442 +msgid "Labels for each subgroup of fields. Labels can be hidden or shown in various contexts using the 'Display fields' screen." +msgstr "" + +#: modules/content_multigroup/content_multigroup.admin.inc:450 +msgid "Subgroup %number label" +msgstr "Untergruppe %number Bezeichner" + +#: modules/content_multigroup/content_multigroup.admin.inc:477 +msgid "The field %field in this group already has %multiple values in the database. To prevent the loss of data you cannot set the number of Multigroup values to less than this." +msgstr "" + +#: modules/content_multigroup/content_multigroup.node_form.inc:84 +msgid "This group requires one collection of fields minimum." +msgstr "" + +#: modules/content_multigroup/content_multigroup.node_form.inc:385 +msgid "!name field is required in group @group." +msgstr "" + +#: modules/content_multigroup/content_multigroup.node_form.inc:401 +msgid "Group @name requires one collection of fields minimum." +msgstr "" + +#: modules/content_multigroup/content_multigroup.node_form.inc:648 +msgid "Add more values" +msgstr "weitere Werte hinzufügen" + +#: modules/content_multigroup/content_multigroup.module:12 +msgid "The fields in a Standard group are independent of each other and each can have either single or multiple values. The fields in a Multigroup are treated as a repeating collection of single value fields." +msgstr "" + +#: modules/content_multigroup/content_multigroup.module:99 +msgid "Multigroup" +msgstr "" + Index: modules/content_multigroup/views/content_multigroup.views.inc =================================================================== --- modules/content_multigroup/views/content_multigroup.views.inc (revision 0) +++ modules/content_multigroup/views/content_multigroup.views.inc (revision 0) @@ -0,0 +1,95 @@ + $groups) { + $type_label = node_get_types('name', $type_name); + + foreach ($groups as $group_name => $group) { + // Let's focus on multigroups that really accept multiple values. + if ($group['group_type'] == 'multigroup' && !empty($group['settings']['multigroup']['multiple'])) { + + // Scan all fields in this multigroup. + $field_labels = array(); + foreach (array_keys($group['fields']) as $field_name) { + // Load information about the field for this particular content type. + $field = content_fields($field_name, $type_name); + + // Get the Views table alias. + $table_alias = content_views_tablename($field); + + // Discard this field if not already exposed to Views. + if (isset($data[$table_alias])) { + $field_labels[$field_name] = t($field['widget']['label']); + } + } + + if (!empty($field_labels)) { + // Build the name for this filter. + // The scope of field groups is the content type itself. You can + // have more than one group with the same name but different fields + // in different content types. Therefore, we need the type name and + // multigroup name. + $db_field = 'multigroup_'. $type_name .'_'. $group_name; + + // Build the labels for the filter. + $label_truncated = truncate_utf8(t($group['label']), 10, TRUE); + $title = t('@group_label multigroup in @type_label', array( + '@group_label' => t($group['label']), + '@type_label' => $type_label, + )); + $title_short = t('@label-truncated in @type_label', array( + '@label-truncated' => $label_truncated, + '@type_label' => $type_label, + )); + $help_text = t('Synchronize multiple values for fields in @group_label multigroup, @type_label type. Fields in this group: @fields.', array( + '@group_label' => t($group['label']), + '@type_label' => $type_label, + '@fields' => implode(', ', $field_labels), + )); + + // Attach the new filter to the node table. + $data['node'][$db_field] = array( + 'group' => t('Content multigroup'), + 'title' => $title, + 'title short' => $title_short, + 'help' => $help_text, + 'filter' => array( + 'field' => $db_field, + 'table' => 'node', + 'handler' => 'content_multigroup_handler_filter', + 'content_type_name' => $type_name, + 'content_group_name' => $group_name, + 'allow empty' => TRUE, + ), + ); + } + } + } + } +} + +/** + * Implementation of hook_views_handlers(). + */ +function content_multigroup_views_handlers() { + return array( + 'info' => array( + 'path' => drupal_get_path('module', 'content_multigroup') . '/views/handlers', + ), + 'handlers' => array( + 'content_multigroup_handler_filter' => array( + 'parent' => 'views_handler_filter', + ), + ), + ); +} Index: modules/content_multigroup/views/handlers/content_multigroup_handler_filter.inc =================================================================== --- modules/content_multigroup/views/handlers/content_multigroup_handler_filter.inc (revision 0) +++ modules/content_multigroup/views/handlers/content_multigroup_handler_filter.inc (revision 0) @@ -0,0 +1,100 @@ +definition['content_type_name']); + return $groups[$this->definition['content_group_name']]; + } + + /** + * Get information about the fields in this multigroup. + */ + function _get_multigroup_fields() { + if (!isset($this->content_multigroup_fields)) { + $group = $this->_get_multigroup(); + $this->content_multigroup_fields = array(); + foreach (array_keys($group['fields']) as $field_name) { + $field = content_fields($field_name, $this->definition['content_type_name']); + $table_alias = content_views_tablename($field); + $this->content_multigroup_fields[$table_alias] = $field; + } + } + return $this->content_multigroup_fields; + } + + /** + * Define default value for master field. + */ + function options(&$options) { + $multigroup_fields = $this->_get_multigroup_fields(); + // Find the first required field. + foreach ($multigroup_fields as $table_alias => $field) { + if ($field['required']) { + $options['content_multigroup_master_field'] = $table_alias; + break; + } + } + // Default to first field in the multigroup. + if (empty($options['content_multigroup_master_field'])) { + $options['content_multigroup_master_field'] = current(array_keys($multigroup_fields)); + } + } + + /** + * Options from to ask the user for a master field. + */ + function options_form(&$form, &$form_state) { + $group = $this->_get_multigroup(); + $options = array(); + foreach ($this->_get_multigroup_fields() as $table_alias => $field) { + $options[$table_alias] = t($field['widget']['label']); + } + $form['content_multigroup_master_field'] = array( + '#title' => t('Available fields in @group_label multigroup', array('@group_label' => t($group['label']))), + '#type' => 'radios', + '#options' => $options, + '#default_value' => $this->options['content_multigroup_master_field'], + '#description' => t('Select the field in this multigroup that will be used to build the primary join with the content table.'), + ); + } + + /** + * Add joins to the query to synchronize the fields in this multigroup. + */ + function query() { + // Create the join between the master field table and the node table. + $base_alias = $this->query->ensure_table($this->options['content_multigroup_master_field'], $this->relationship); + + // Now we want to join the master field table with all other tables + // related to fields in the same multigroup, but adding the delta + // key to the join condition. This is what allows us to keep delta + // values in sync for all fields in the same multigroup. + foreach ($this->_get_multigroup_fields() as $table_alias => $field) { + if ($table_alias != $this->options['content_multigroup_master_field']) { + $alias = $this->query->ensure_table($table_alias, $this->relationship); + $this->query->table_queue[$alias]['join']->extra = $base_alias .'.delta = '. $alias .'.delta'; + } + } + } +} Index: modules/content_multigroup/views/handlers/translations/modules-content_multigroup-views-handlers.de.po =================================================================== --- modules/content_multigroup/views/handlers/translations/modules-content_multigroup-views-handlers.de.po (revision 0) +++ modules/content_multigroup/views/handlers/translations/modules-content_multigroup-views-handlers.de.po (revision 0) @@ -0,0 +1,33 @@ +# $Id$ +# +# LANGUAGE translation of Drupal (modules-content_multigroup-views-handlers) +# Copyright 2009 NAME +# Generated from file: content_multigroup_handler_filter.inc,v 1.1.2.2 2009/10/31 20:31:09 markuspetrux +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"POT-Creation-Date: 2009-12-06 15:33+0100\n" +"PO-Revision-Date: 2009-12-06 22:28+0100\n" +"Last-Translator: Thomas Zahreddin \n" +"Language-Team: German \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-Language: German\n" +"X-Poedit-Country: Germany\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: modules/content_multigroup/views/handlers/content_multigroup_handler_filter.inc:19 +msgid "+ delta" +msgstr "+ delta" + +#: modules/content_multigroup/views/handlers/content_multigroup_handler_filter.inc:74 +msgid "Available fields in @group_label multigroup" +msgstr "" + +#: modules/content_multigroup/views/handlers/content_multigroup_handler_filter.inc:78 +msgid "Select the field in this multigroup that will be used to build the primary join with the content table." +msgstr "" + Index: modules/content_multigroup/views/translations/modules-content_multigroup-views.de.po =================================================================== --- modules/content_multigroup/views/translations/modules-content_multigroup-views.de.po (revision 0) +++ modules/content_multigroup/views/translations/modules-content_multigroup-views.de.po (revision 0) @@ -0,0 +1,33 @@ +# $Id$ +# +# LANGUAGE translation of Drupal (modules-content_multigroup-views) +# Copyright 2009 NAME +# Generated from file: content_multigroup.views.inc,v 1.1.2.2 2009/08/10 03:53:05 markuspetrux +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"POT-Creation-Date: 2009-12-06 15:33+0100\n" +"PO-Revision-Date: 2009-12-06 22:29+0100\n" +"Last-Translator: Thomas Zahreddin \n" +"Language-Team: German\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-Language: German\n" +"X-Poedit-Country: Germany\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: modules/content_multigroup/views/content_multigroup.views.inc:46 +msgid "@group_label multigroup in @type_label" +msgstr "@group_label multigroup in @type_label" + +#: modules/content_multigroup/views/content_multigroup.views.inc:50 +msgid "@label-truncated in @type_label" +msgstr "@label-truncated in @type_label" + +#: modules/content_multigroup/views/content_multigroup.views.inc:54 +msgid "Synchronize multiple values for fields in @group_label multigroup, @type_label type. Fields in this group: @fields." +msgstr "" + Index: modules/fieldgroup/fieldgroup.install =================================================================== --- modules/fieldgroup/fieldgroup.install (revision 1263) +++ modules/fieldgroup/fieldgroup.install (working copy) @@ -52,6 +52,7 @@ 'group_type' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => 'standard'), 'type_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), 'group_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'parent_group_name' => array('type' => 'varchar', 'length' => 32, 'not null' => FALSE, 'default' => ''), 'label' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), 'settings' => array('type' => 'text', 'size' => 'medium', 'not null' => TRUE), 'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), @@ -313,4 +314,16 @@ $ret = array(); $ret[] = update_sql("DELETE FROM {content_group_fields} WHERE (field_name, type_name) NOT IN (SELECT field_name, type_name FROM {content_node_field_instance})"); return $ret; -} \ No newline at end of file +} + +/** + * allow for nesting of fieldgroups + */ +function fieldgroup_update_6008() { + if ($abort = content_check_update('fieldgroup')) { + return $abort; + } + $ret = array(); + db_add_field($ret, 'content_group', 'parent_group_name', array('type' => 'varchar', 'length' => 32, 'not null' => FALSE, 'default' => '')); + return $ret; +} Index: modules/fieldgroup/fieldgroup.module =================================================================== --- modules/fieldgroup/fieldgroup.module (revision 1263) +++ modules/fieldgroup/fieldgroup.module (working copy) @@ -199,6 +199,7 @@ $form['#content_type'] = $content_type; $form['#group_name'] = $group_name; + return confirm_form($form, t('Are you sure you want to remove the group %label?', array('%label' => t($group['label']))), @@ -210,6 +211,9 @@ $form_values = $form_state['values']; $content_type = $form['#content_type']; $group_name = $form['#group_name']; + $parent_group_name = db_fetch_array(db_query("SELECT parent_group_name FROM {". fieldgroup_tablename() ."} WHERE group_name = '%s' and type_name = '%s'", $group_name, $content_type['type'])); + $result = db_query("UPDATE {". fieldgroup_tablename() ."} SET parent_group_name = '%s' WHERE parent_group_name = '%s'", $parent_group_name['parent_group_name'], $group_name); + $result = db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET group_name = '%s' WHERE group_name = '%s'", $parent_group_name['parent_group_name'], $group_name); fieldgroup_delete($content_type['type'], $group_name); drupal_set_message(t('The group %group_name has been removed.', array('%group_name' => $group_name))); $form_state['redirect'] = 'admin/content/node-type/'. $content_type['url_str'] .'/fields'; @@ -227,46 +231,16 @@ $groups = $data['groups']; $groups_sorted = $data['groups_sorted']; } - else { - $result = db_query("SELECT * FROM {". fieldgroup_tablename() ."} ORDER BY weight, group_name"); + else {; + $result = db_query("SELECT * FROM {". fieldgroup_tablename() ."} ORDER BY type_name, weight"); $groups = array(); $groups_sorted = array(); while ($group = db_fetch_array($result)) { - $group['settings'] = unserialize($group['settings']); - $group['fields'] = array(); - - // Allow external modules to translate field group strings. - $group_strings = array( - 'label' => $group['label'], - 'form_description' => $group['settings']['form']['description'], - 'display_description' => $group['settings']['display']['description'], - ); - drupal_alter('content_fieldgroup_strings', $group_strings, $group['type_name'], $group['group_name']); - $group['label'] = $group_strings['label']; - $group['settings']['form']['description'] = $group_strings['form_description']; - $group['settings']['display']['description'] = $group_strings['display_description']; - - $groups[$group['type_name']][$group['group_name']] = $group; + $groups[$group['type_name']] = _fieldgroup_get_tree($group['type_name']); $groups_sorted[$group['type_name']][] = &$groups[$group['type_name']][$group['group_name']]; } //load fields - $result = db_query("SELECT nfi.*, ng.group_name FROM {". fieldgroup_tablename() ."} ng ". - "INNER JOIN {". fieldgroup_fields_tablename() ."} ngf ON ngf.type_name = ng.type_name AND ngf.group_name = ng.group_name ". - "INNER JOIN {". content_instance_tablename() ."} nfi ON nfi.field_name = ngf.field_name AND nfi.type_name = ngf.type_name ". - "WHERE nfi.widget_active = 1 ORDER BY nfi.weight"); - while ($field = db_fetch_array($result)) { - // Allow external modules to translate field strings. - $field_strings = array( - 'widget_label' => $field['label'], - 'widget_description' => $field['description'], - ); - drupal_alter('content_field_strings', $field_strings, $field['type_name'], $field['field_name']); - $field['label'] = $field_strings['widget_label']; - $field['description'] = $field_strings['widget_description']; - - $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']] = $field; - } - cache_set('fieldgroup_data:'. $language->language, array('groups' => $groups, 'groups_sorted' => $groups_sorted), content_cache_tablename()); + cache_set('fieldgroup_data', array('groups' => $groups, 'groups_sorted' => $groups_sorted), content_cache_tablename()); } } if (empty($content_type)) { @@ -278,7 +252,82 @@ return $sorted ? $groups_sorted[$content_type] : $groups[$content_type]; } +/** + * create a tree of fieldgroups for nesting them + */ +function _fieldgroup_get_tree($type_name, $parent_group_name = '', $depth = -1, $max_depth = null) { + static $children, $parents, $groups; + $depth++; + // We cache trees, so it's not CPU-intensive to call get_tree() on a term + // and its children, too. + if (!isset($children[$type_name])) { + $children[$type_name] = array(); + + $s = "SELECT * FROM {". fieldgroup_tablename() ."} WHERE type_name='%s' ORDER BY weight"; + $r = db_query($s, $type_name); + while ($group = db_fetch_array($r)) { + $group['settings'] = unserialize($group['settings']); + $group['fields'] = array(); + + // Allow external modules to translate field group strings. + $group_strings = array( + 'label' => $group['label'], + 'form_description' => $group['settings']['form']['description'], + 'display_description' => $group['settings']['display']['description'], + ); + drupal_alter('content_fieldgroup_strings', $group_strings, $type_name, $group['group_name']); + $group['label'] = $group_strings['label']; + $group['settings']['form']['description'] = $group_strings['form_description']; + $group['settings']['display']['description'] = $group_strings['display_description']; + + $children[$type_name][$group['parent_group_name']][] = $group['group_name']; + $parents[$type_name][$group['group_name']][] = $group['parent_group_name']; + $groups[$type_name][$group['group_name']] = $group; + } + //load fields + $result = db_query("SELECT nfi.*, ng.group_name FROM {". fieldgroup_tablename() ."} ng ". + "INNER JOIN {". fieldgroup_fields_tablename() ."} ngf ON ngf.type_name = ng.type_name AND ngf.group_name = ng.group_name ". + "INNER JOIN {". content_instance_tablename() ."} nfi ON nfi.field_name = ngf.field_name AND nfi.type_name = ngf.type_name ". + "WHERE nfi.widget_active = 1 ORDER BY nfi.weight"); + while ($field = db_fetch_array($result)) { + $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']] = $field; + } + } + + $max_depth = (is_null($max_depth)) ? count($children[$type_name]) : $max_depth; + if (isset($children[$type_name][$parent_group_name])) { + foreach ($children[$type_name][$parent_group_name] as $child_group_name) { + if ($max_depth > $depth) { + $group = $groups[$type_name][$child_group_name]; + $group['depth'] = $depth; + $group['parents'] = $parents[$type_name][$child_group_name]; + $tree[$group['group_name']] = $group; + if ($children[$type_name][$child_group_name]) { + $tree = array_merge($tree, _fieldgroup_get_tree($type_name, $child_group_name, $depth, $max_depth)); + } + } + } + } + return $tree ? $tree : array(); +} + +/** + * go through a set of fieldgroups and construct a simple representation of their hierarchy + */ +function _fieldgroup_plain_tree($items) { + $rows = array(); + $rows[''] = '<'. t('none') .'>'; + foreach ($items as $item) { + $group_name = $item['group_name']; + $label = t($item['label']); + if ($group_name) { + $rows[$group_name] = str_repeat('--', $item['depth']) . ' ' . $label; + } + } + return $rows; +} + function _fieldgroup_groups_label($content_type) { $groups = fieldgroup_groups($content_type); @@ -293,51 +342,159 @@ return db_result(db_query("SELECT group_name FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $content_type, $field_name)); } +function _fieldgroup_field_get_parents($content_type, $name, $is_group = FALSE) { + $counter = 0; + if ($is_group) { + $parents[$counter] = $name; + } + else { + if ($result = db_result(db_query("SELECT group_name FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $content_type, $name))) { + $parents[$counter] = $result; + } + } + while ($result = db_result(db_query("SELECT parent_group_name FROM {". fieldgroup_tablename() ."} WHERE type_name = '%s' AND group_name = '%s'", $content_type, $parents[$counter]))) { + $counter++; + $parents[$counter] = $result; + } + return $parents; +} + +function _fieldgroup_add_group_to_form(&$form, &$form_state, $form_id, $group_name, $group, $groups) { + $form[$group_name] = array( + '#type' => 'fieldset', + '#title' => check_plain(t($group['label'])), + '#collapsed' => $group['settings']['form']['style'] == 'fieldset_collapsed', + '#collapsible' => in_array($group['settings']['form']['style'], array('fieldset_collapsed', 'fieldset_collapsible')), + '#weight' => $group['weight'], + '#depth' => $group['depth'], + '#group_parent' => $group['parent_group_name'], + '#description' => content_filter_xss(t($group['settings']['form']['description'])), + '#attributes' => array('class' => strtr($group['group_name'], '_', '-')), + ); + $has_accessible_field = FALSE; + foreach ($group['fields'] as $field_name => $field) { + if (isset($form[$field_name])) { + $form[$field_name]['#weight'] = $field['weight']; + $form[$group_name][$field_name] = $form[$field_name]; + //Track whether this group has any accessible fields within it. + if (!isset($form[$field_name]['#access']) || $form[$field_name]['#access'] !== FALSE) { + $has_accessible_field = TRUE; + } + unset($form[$field_name]); + } + } + if (!empty($group['fields']) && !element_children($form[$group_name])) { + //hide the fieldgroup, because the fields are hidden too + unset($form[$group_name]); + } + + if (!$has_accessible_field) { + // Hide the fieldgroup, because the fields are inaccessible. + $form[$group_name]['#access'] = FALSE; + } + else { + //cascade visibility up + $form[$group_name]['#access'] = TRUE; + } + + // Allow other modules to alter the form. + // Can't use module_invoke_all because we want + // to be able to use a reference to $form and $form_state. + foreach (module_implements('fieldgroup_form') as $module) { + $function = $module .'_fieldgroup_form'; + $function($form, $form_state, $form_id, $group); + } +} + + /** + * This is function fieldgroup_order_fields_and_groups + * + * @param array $group_rows An empty array that we will fill. + * @param array $groups All of the info we need about all of the groups for the content type we're working on. + * @param array $field_check_off This contains the fields. We will unset them as we process them. + * + */ +function fieldgroup_order_fields_groups(&$group_rows, &$groups, &$field_check_off) { + $max_depth = 0; + foreach ($group_rows as $name) { + $depth = $groups[$name]['depth']; + if ($depth > $max_depth) { + $max_depth = $depth; + } + $parent = $groups[$name]['parent_group_name']; + + //run through the fields and come up with new weights for display purposes + if (isset($groups[$name]['fields'])) { + foreach ($groups[$name]['fields'] as $name2 => $elements) { + $depth2 = $groups[$name]['depth'] + 1; + $groups[$name]['fields'][$name2]['depth'] = $depth2; + if (in_array($name2, $field_check_off)) { + $index = array_search($name2, $field_check_off); + unset($field_check_off[$index]); + } + } + } + } + return $max_depth; +} + + +/** * Implementation of hook_form_alter() */ function fieldgroup_form_alter(&$form, $form_state, $form_id) { if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { - foreach (fieldgroup_groups($form['type']['#value']) as $group_name => $group) { - $form[$group_name] = array( - '#type' => 'fieldset', - '#title' => check_plain(t($group['label'])), - '#collapsed' => $group['settings']['form']['style'] == 'fieldset_collapsed', - '#collapsible' => in_array($group['settings']['form']['style'], array('fieldset_collapsed', 'fieldset_collapsible')), - '#weight' => $group['weight'], - '#description' => content_filter_xss(t($group['settings']['form']['description'])), - '#attributes' => array('class' => strtr($group['group_name'], '_', '-')), - ); + $group_rows = array(); + $field_rows = array(); - $has_accessible_field = FALSE; - foreach ($group['fields'] as $field_name => $field) { - if (isset($form[$field_name])) { - $form[$group_name][$field_name] = $form[$field_name]; - // Track whether this group has any accessible fields within it. - if (!isset($form[$field_name]['#access']) || $form[$field_name]['#access'] !== FALSE) { - $has_accessible_field = TRUE; - } - unset($form[$field_name]); - } + //prepare data that will make this easier + $groups = fieldgroup_groups($form['type']['#value']); + if (!empty($groups)) { + foreach ($groups as $name => $more) { + $group_rows[] = $name; } - if (!empty($group['fields']) && !element_children($form[$group_name])) { - //hide the fieldgroup, because the fields are hidden too - unset($form[$group_name]); + } + + $fields = $form['#field_info']; + if (!empty($fields)) { + foreach ($fields as $name => $more) { + $field_rows[] = $name; } + } - if (!$has_accessible_field) { - // Hide the fieldgroup, because the fields are inaccessible. - $form[$group_name]['#access'] = FALSE; + $max_depth = fieldgroup_order_fields_groups($group_rows, $groups, $field_rows); + + //cover the top level fields that aren't in fieldgroups + if (isset($field_rows)) { + foreach ($field_rows as $name) { + $form[$name]['#depth'] = 0; } + } - // Allow other modules to alter the form. - // Can't use module_invoke_all because we want - // to be able to use a reference to $form and $form_state. - foreach (module_implements('fieldgroup_form') as $module) { - $function = $module .'_fieldgroup_form'; - $function($form, $form_state, $form_id, $group); + //now that we have the order of things as we want them, let's create the fieldsets for the fieldgroups + foreach ($groups as $group_name => $group) { + _fieldgroup_add_group_to_form($form, $form_state, $form_id, $group_name, $group, $groups); + } + + //reorder the groups from the inside-out in order to avoid a recursive function + while ($max_depth >= 0) { + foreach ($group_rows as $name) { + if ($form[$name]['#depth'] == $max_depth) { + $parent = $form[$name]['#group_parent']; + if (isset($parent) && $parent != '') { + $form[$parent][$name] = $form[$name]; + if ($form[$name]['#access']) { + $form[$parent]['#access'] = TRUE; + } + unset($form[$name]); + $index = array_search($name, $group_rows); + unset($group_rows[$index]); + } + } } + $max_depth--; } } @@ -463,6 +620,8 @@ // Parse incoming rows. $add_field_rows = array('_add_new_field', '_add_existing_field'); $field_rows = array_merge($form['#fields'], $add_field_rows); + $add_group_rows = array($new_group_name); + $group_rows = array_merge($form['#groups'], $add_group_rows); foreach ($form_values as $key => $values) { // If 'field' row: update field parenting. if (in_array($key, $field_rows)) { @@ -487,11 +646,15 @@ fieldgroup_update_fields(array('field_name' => $key, 'group' => $parent, 'type_name' => $type_name)); } - // If 'group' row: update groups weights + } + + foreach ($form_state['values'] as $key => $values) { + // If 'group' row: update groups weights and parent // (possible newly created group has already been taken care of). - elseif (in_array($key, $form['#groups'])) { - db_query("UPDATE {". fieldgroup_tablename() ."} SET weight = %d WHERE type_name = '%s' AND group_name = '%s'", - $values['weight'], $type_name, $key); + if (in_array($key, $group_rows)) { + $parent = ($values['parent'] == '_add_new_group' && isset($new_group_name)) ? $new_group_name : $values['parent']; + $weight = $values['weight']; + db_query("UPDATE {". fieldgroup_tablename() ."} SET weight = %d, parent_group_name = '%s' WHERE type_name = '%s' AND group_name = '%s'", $weight, $parent, $type_name, $key); } } @@ -543,10 +706,50 @@ case 'view': // Prevent against invalid 'nodes' built by broken 3rd party code. if (isset($node->type)) { + //prepare data that will make this easier + $group_rows = array(); + $field_rows = array(); + $groups = fieldgroup_groups($node->type); + if (!empty($groups)) { + foreach ($groups as $name => $more) { + $group_rows[] = $name; + } + } + + $fields = $node->content; + if (!empty($fields)) { + foreach ($fields as $name => $more) { + if (strstr($name, 'field_')) { + $field_rows[] = $name; + } + } + } + + $max_depth = fieldgroup_order_fields_groups($group_rows, $groups, $field_rows); + + //cover the top level fields that aren't in fieldgroups + if (isset($field_rows)) { + foreach ($field_rows as $name) { + $node->content[$name]['#depth'] = 0; + } + } // Build the node content element needed to render each fieldgroup. - foreach (fieldgroup_groups($node->type) as $group) { + foreach ($groups as $group) { fieldgroup_build_content($group, $node, $teaser, $page); } + //reorder the groups from the inside-out in order to avoid writing a recursive function + while ($max_depth >= 0) { + foreach ($group_rows as $name) { + if ($node->content[$name]['#depth'] == $max_depth) { + $parent = $node->content[$name]['#group_parent']; + if (isset($parent) && $parent != '') { + $node->content[$parent]['group'][$name] = $node->content[$name]; + unset($node->content[$name]); + } + } + } + $max_depth--; + } } break; } @@ -611,6 +814,8 @@ foreach ($group['fields'] as $field_name => $field) { if (isset($node->content[$field_name])) { $element[$field_name] = $node->content[$field_name]; + $element[$field_name]['#weight'] = $field['weight']; + $element[$field_name]['#depth'] = $field['depth']; } } @@ -636,10 +841,12 @@ $wrapper = array( 'group' => $element, '#weight' => $group['weight'], + '#depth' => $group['depth'], '#post_render' => array('fieldgroup_wrapper_post_render'), '#group_name' => $group_name, '#type_name' => $node->type, '#context' => $context, + '#group_parent' => $group['parent_group_name'], ); $node->content[$group_name] = $wrapper; @@ -806,15 +1013,16 @@ if (!isset($groups[$group['group_name']])) { // Accept group name from programmed submissions if valid. - db_query("INSERT INTO {". fieldgroup_tablename() ."} (group_type, type_name, group_name, label, settings, weight)". - " VALUES ('%s', '%s', '%s', '%s', '%s', %d)", $group['group_type'], $type_name, $group['group_name'], $group['label'], serialize($group['settings']), $group['weight']); + db_query("INSERT INTO {". fieldgroup_tablename() ."} (parent_group_name, group_type, type_name, group_name, label, settings, weight)". + " VALUES ('%s','%s', '%s', '%s', '%s', '%s', %d)", + isset($group['parent']) ? $group['parent'] : $group['parent_group_name'], $group['group_type'], $type_name, $group['group_name'], $group['label'], serialize($group['settings']), $group['weight']); cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); return SAVED_NEW; } else { - db_query("UPDATE {". fieldgroup_tablename() ."} SET group_type = '%s', label = '%s', settings = '%s', weight = %d ". - "WHERE type_name = '%s' AND group_name = '%s'", - $group['group_type'], $group['label'], serialize($group['settings']), $group['weight'], $type_name, $group['group_name']); + db_query("UPDATE {". fieldgroup_tablename() ."} SET parent_group_name = '%s', group_type = '%s', label = '%s', settings = '%s', weight = %d ". + "WHERE type_name = '%s' AND group_name = '%s'", + isset($group['parent']) ? $group['parent'] : $group['parent_group_name'], $group['group_type'], $group['label'], serialize($group['settings']), $group['weight'], $type_name, $group['group_name']); cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); return SAVED_UPDATED; } @@ -884,6 +1092,7 @@ function fieldgroup_preprocess_fieldgroup_simple(&$vars) { $element = $vars['element']; + $vars['parent_group_name'] = $element['#parent_group_name']; $vars['group_name'] = $element['#group_name']; $vars['group_name_css'] = strtr($element['#group_name'], '_', '-'); $vars['label'] = isset($element['#title']) ? $element['#title'] : '';; @@ -910,4 +1119,4 @@ // '#chilren' might not be set if the group is empty. $vars[$group_name .'_rendered'] = isset($node->content[$group_name]['#children']) ? $node->content[$group_name]['#children'] : ''; } -} \ No newline at end of file +} Index: theme/theme.inc =================================================================== --- theme/theme.inc (revision 1263) +++ theme/theme.inc (working copy) @@ -28,6 +28,8 @@ $add_rows[] = $key; } } + $parent_list = array(); + $parent_list['top'] = 'top'; while ($order) { $key = reset($order); $element = &$form[$key]; @@ -48,7 +50,16 @@ $row = new stdClass(); // Add target classes for the tabledrag behavior. - $element['weight']['#attributes']['class'] = 'field-weight'; + if ($element['#row_type'] == 'group') { + $parent_list[$element['group_name']['#value']] = strtr($element['group_name']['#value'], '_', '-'); + } + if (empty($element['parent']['#value']) || !isset($element['parent']['#value'])) { + $element['weight']['#attributes']['class'] = 'field-weight field-weight-' . 'top'; + } + else { + $element['weight']['#attributes']['class'] = 'field-weight field-weight-' . strtr($element['parent']['#value'], '_', '-'); + } + $element['parent']['#attributes']['class'] = 'group-parent'; $element['hidden_name']['#attributes']['class'] = 'field-name'; // Add target classes for the update selects behavior. @@ -74,6 +85,8 @@ $row->class .= isset($element['#add_new']) ? ' content-add-new' : ''; $row->class .= isset($element['#leaf']) ? ' tabledrag-leaf' : ''; $row->class .= isset($element['#root']) ? ' tabledrag-root' : ''; + $row->class .= (isset($element['group_type']['#value']) && $element['group_type']['#value'] == 'Standard group') ? ' tabledrag-standardgroup' : ''; + $row->class .= (isset($element['group_type']['#value']) && $element['group_type']['#value'] == 'Multigroup') ? ' tabledrag-multigroup' : ''; $rows[] = $row; array_shift($order); @@ -82,11 +95,16 @@ $vars['submit'] = drupal_render($form); // Add tabledrag behavior. -// drupal_add_tabledrag('content-field-overview', 'match', 'parent', 'group-parent', 'group-parent', 'field-name', FALSE, 1); - drupal_add_tabledrag('content-field-overview', 'match', 'parent', 'group-parent', 'group-parent', 'field-name', TRUE, 1); -// drupal_add_tabledrag('content-field-overview', 'order', 'sibling', 'field-weight', NULL, NULL, FALSE); - drupal_add_tabledrag('content-field-overview', 'order', 'sibling', 'field-weight'); + //drupal_add_tabledrag('content-field-overview', 'match', 'parent', 'group-parent', 'group-parent', 'field-name', FALSE, 10); + drupal_add_tabledrag('content-field-overview', 'match', 'parent', 'group-parent', 'group-parent', 'field-name', TRUE, 10); + foreach ($parent_list as $name => $parent) { + //drupal_add_tabledrag('content-field-overview', 'order', 'sibling', 'field-weight', 'field-weight-' . $parent, NULL, FALSE); + drupal_add_tabledrag('content-field-overview', 'order', 'sibling', 'field-weight', 'field-weight-' . $parent, NULL, TRUE); + } + // Override methods in Drupal core tabledrag.js. + drupal_add_js(drupal_get_path('module', 'fieldgroup') .'/fieldgroup.tabledrag.js'); + // Add settings for the update selects behavior. $js_fields = array(); foreach (array_keys(content_existing_field_options($form['#type_name'])) as $field_name) { @@ -142,4 +160,4 @@ } $vars['rows'] = $rows; $vars['submit'] = drupal_render($form); -} \ No newline at end of file +}