array( 'arguments' => array('element' => NULL), ), 'content_multigroup_node_label' => array( 'arguments' => array('text' => NULL), ), 'content_multigroup_display_simple' => array( 'arguments' => array('element' => NULL), ), 'content_multigroup_display_fieldset' => array( 'arguments' => array('element' => NULL), ), 'content_multigroup_display_hr' => array( 'arguments' => array('element' => NULL), ), 'content_multigroup_display_table' => array( 'arguments' => array('element' => NULL), ), ); } /** * 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() { return array('multigroup' => t('Multigroup')); } /** * Implementation of hook_fieldgroup_default_settings(). */ function content_multigroup_fieldgroup_default_settings($group_type) { if ($group_type == 'multigroup') { module_load_include('inc', 'content', 'includes/content.admin'); $settings = array('multigroup' => array('multiple' => 1)); foreach (array_keys(content_build_modes()) as $key) { $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'])) { $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']); $group = isset($groups[$group_name]) ? $groups[$group_name] : array(); if (!empty($group) && $group['group_type'] == 'multigroup') { $form['field']['multiple']['#value'] = $group['settings']['multigroup']['multiple']; $form['field']['multiple']['#access'] = FALSE; } } elseif ($form_id == 'content_field_overview_form') { content_multigroup_field_overview_form($form, $form_state); $form['#validate'][] = 'content_multigroup_field_overview_form_validate'; } elseif ($form_id == 'content_display_overview_form' && !empty($form['#groups'])) { 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 == 'fieldgroup_group_edit_form') { content_multigroup_group_edit_form($form, $form_state, $form_id); } } 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']; } } } 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); 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']); } } } } /** * 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); } /** * Menu callback; presents a listing of fields display settings for a content type. * * 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 = $group_options = array(); if (module_exists('fieldgroup')) { $groups = fieldgroup_groups($type_name); $group_options = _fieldgroup_groups_label($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'), 'hr' => t('Horizontal line'), //'table' => t('Table'), // TODO add this later ); 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); } } } /** * 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) { // Find any groups 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']; $groups = fieldgroup_groups($form['#type_name']); $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; } 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($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('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.'); $group_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' => $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']); 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); } } /** * 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) { $group_name = $group['group_name']; if ($group['group_type'] != 'multigroup' || !empty($form[$group_name]['#access']) || empty($form[$group_name])) { return; } $node = $form['#node']; $content_type = content_types($group['type_name']); $group_fields = array_intersect_key($content_type['fields'], $group['fields']); $group_multiple = $group['settings']['multigroup']['multiple']; switch ($group_multiple) { case 0: $group_deltas = array(0); $max_delta = 0; break; case 1: // Compute deltas based on the field with the highest number of items. $group_deltas = array(); $max_delta = -1; foreach ($group_fields as $field_name => $field) { $field_items = isset($node->$field_name) ? $node->$field_name : array(); if (!empty($field_items)) { $field = $group_fields[$field_name]; $field_deltas = array_keys(content_set_empty($field, $field_items)); $field_max = (!empty($field_deltas) ? max($field_deltas) : 0); if ($field_max > $max_delta || empty($group_deltas)) { $max_delta = $field_max; $group_deltas = $field_deltas; } } } $current_item_count = (isset($form_state['item_count'][$group_name]) ? $form_state['item_count'][$group_name] : count($group_deltas)); if ($current_item_count > 0) { // We always want at least one empty item for the user to fill in. $current_item_count++; } else { // Default number of empty items when none is present. $current_item_count = 1; } while (count($group_deltas) < $current_item_count) { $max_delta++; $group_deltas[] = $max_delta; } break; default: $group_deltas = array_keys(array_fill(0, $group_multiple, 0)); $max_delta = $group_multiple - 1; 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]['#tree'] = TRUE; if (!isset($form['#multigroups'])) { $form['#multigroups'] = array(); } $form['#multigroups'][$group_name] = $group_fields; // 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'); } foreach ($group_deltas as $delta) { content_multigroup_group_form($form, $form_state, $group, $delta); } // 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) { 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']; $type_name = $group['type_name']; $content_type = content_types($type_name); $group_fields = array_intersect_key($content_type['fields'], $group['fields']); $group_name = $group['group_name']; $group_multiple = $group['settings']['multigroup']['multiple']; $form[$group_name]['#fields'] = array_keys($group_fields); foreach ($group_fields as $field_name => $field) { if (empty($form[$group_name][$delta])) { $form[$group_name] += array($delta => array($field_name => array())); } else { $form[$group_name][$delta][$field_name] = array(); } $item_count = (isset($form_state['item_count'][$group_name]) ? $form_state['item_count'][$group_name] : $form[$group_name]['#item_count']); $form[$group_name][$delta]['_weight'] = array( '#type' => 'weight', '#delta' => $item_count, // this 'delta' is the 'weight' element's property '#default_value' => $delta, '#weight' => 100, ); // 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; // 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; $form[$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. 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'; $form[$group_name][$delta][$field_name] = $field_form[$field_name]; } $form[$group_name][$delta][$field_name]['#weight'] = $field['widget']['weight']; } // Reset the form '#node' back to its original value. $form['#node'] = $node; } /** * 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) { 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_fix_values($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; } /** * Node form validation handler. * * If all fields in a delta subgroup are empty, then we can let the * content module remove them. See content_set_empty(). */ function content_multigroup_node_form_validate($form, &$form_state) { $type_name = $form['#node']->type; $content_type = content_types($type_name); $groups = fieldgroup_groups($type_name); foreach ($form['#multigroups'] as $group_name => $group_fields) { $empty_deltas = array(); foreach ($group_fields as $field_name => $field) { $is_empty_function = $field['module'] .'_content_is_empty'; foreach ($form_state['values'][$field_name] as $delta => $item) { if (!isset($empty_deltas[$delta])) { $empty_deltas[$delta] = TRUE; } if (!$is_empty_function($item, $field)) { $empty_deltas[$delta] = FALSE; } } } foreach ($empty_deltas as $delta => $is_empty) { foreach ($group_fields as $field_name => $field) { $form_state['values'][$field_name][$delta]['_keep_empty'] = $is_empty ? 0 : 1; } } } } /** * Fix value positions in $form_state for the fields in a multigroup. */ function content_multigroup_node_form_fix_values(&$form, &$form_state, $type_name, $group_name) { $content_type = content_types($type_name); $groups = fieldgroup_groups($type_name); $group = $groups[$group_name]; $group_fields = array_intersect_key($content_type['fields'], $group['fields']); // 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 the field weight from the group and keep track of the current // delta for each field item. $group_data[$field_name][$delta] = array_merge($items[$field_name], array( '_weight' => $items['_weight'], '_old_delta' => $delta, )); } } $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_fix_values(). */ 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; } /** * 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']; $value = $form_state['values'][$field_name][$delta][0]; form_set_value($element, $value, $form_state); } /** * Implementation of hook_fieldgroup_view(). */ function content_multigroup_fieldgroup_view(&$node, &$element, $group, $context) { if ($group['group_type'] != 'multigroup') { return; } $group_name = $group['group_name']; $node_copy = drupal_clone($node); $group_multiple = $group['settings']['multigroup']['multiple']; $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(); $subgroup_format = isset($subgroup_settings[$context]['format']) ? $subgroup_settings[$context]['format'] : 'fieldset'; $group_field_names = array_keys($group['fields']); switch ($group_multiple) { case 0: $group_deltas = array(0); break; case 1: // Compute deltas based on the field with the highest number of items. $group_deltas = array(); $max_delta = -1; foreach ($group_field_names as $field_name) { $field_deltas = is_array($node->content[$field_name]['field']['items']) ? array_keys($node->content[$field_name]['field']['items']) : array(); $field_max = (!empty($field_deltas) ? max($field_deltas) : 0); if ($field_max > $max_delta) { $max_delta = $field_max; $group_deltas = $field_deltas; } } break; default: $group_deltas = array_keys(array_fill(0, $group_multiple - 1, 0)); break; } foreach ($group_deltas as $i => $delta) { $element[$delta] = array( '#title' => ($show_label == 'above' && !empty($subgroup_labels[$i]) ? check_plain(t($subgroup_labels[$i])) : ''), '#attributes' => array('class' => 'multigroup multigroup-'. $i), '#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 ($group_field_names 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[$delta]['#theme'] = 'content_multigroup_display_simple'; break; case 'fieldset': $element[$delta]['#type'] = 'content_multigroup_display_fieldset'; break; case 'hr': $element[$delta]['#theme'] = 'content_multigroup_display_hr'; break; case 'table': $element[$delta]['#theme'] = 'content_multigroup_display_table'; break; } } foreach ($group_field_names as $field_name) { 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) { $groups = fieldgroup_groups($element['#type_name']); $group_name = $element['#group_name']; $group = $groups[$group_name]; $group_multiple = $group['settings']['multigroup']['multiple']; $output = ''; if ($group_multiple >= 1) { $table_id = $element['#group_name'] .'_values'; $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(); $header = array( array( 'data' => '', 'colspan' => 2 ), t('Order'), ); $rows = array(); $i = 0; foreach (element_children($element) as $delta => $key) { if ($key !== $group_name .'_add_more') { $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; $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, '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[$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; } /** * 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 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]) - 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']; } $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])); content_multigroup_group_form($form, $form_state, $group, $delta); // Rebuild weight deltas to make sure they all are equally dimensioned. foreach ($form[$group_name] as $key => $item) { if (is_numeric($key) && isset($item['_weight']) && is_array($item['_weight'])) { $form[$group_name][$key]['_weight']['#delta'] = $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) { return !empty($text) ? '

'. $text .'

' : ''; } /** * 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 subgroup of fields in 'table' format. * * No output is generated if all fields are empty. * * @TODO: It is still a copy of 'simple' format. */ function theme_content_multigroup_display_table($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; }