content. * - hook_fieldgroup_form: Alter the group portion of the node form. * - hook_fieldgroup_types: Add additional fieldgroup group_types. * - hook_fieldgroup_default_settings: Add additional fieldgroup default settings. * - hook_fieldgroup_save: Do additional processing when a fieldgroup is saved. */ /** * Implementation of hook_init(). */ function fieldgroup_init() { drupal_add_css(drupal_get_path('module', 'fieldgroup') .'/fieldgroup.css'); } /** * Implementation of hook_ctools_plugin_directory(). */ function fieldgroup_ctools_plugin_directory($module, $plugin) { if ($module == 'ctools' && $plugin == 'content_types') { return 'panels/' . $plugin; } } /** * Implementation of hook_menu(). */ function fieldgroup_menu() { $items = array(); // Make sure this doesn't fire until content_types is working, // needed to avoid errors on initial installation. if (!defined('MAINTENANCE_MODE')) { foreach (node_get_types() as $type) { $type_name = $type->type; $content_type = content_types($type_name); $type_url_str = $content_type['url_str']; $items['admin/content/node-type/'. $type_url_str .'/groups/%'] = array( 'title' => 'Edit group', 'page callback' => 'drupal_get_form', 'page arguments' => array('fieldgroup_group_edit_form', $type_name, 5), 'access arguments' => array('administer content types'), 'type' => MENU_CALLBACK, ); $items['admin/content/node-type/'. $type_url_str .'/groups/%/remove'] = array( 'title' => 'Edit group', 'page callback' => 'drupal_get_form', 'page arguments' => array('fieldgroup_remove_group', $type_name, 5), 'access arguments' => array('administer content types'), 'type' => MENU_CALLBACK, ); } } return $items; } /** * Implementation of hook_theme(). */ function fieldgroup_theme() { return array( 'fieldgroup_simple' => array( 'template' => 'fieldgroup-simple', 'arguments' => array('element' => NULL), ), 'fieldgroup_fieldset' => array( 'arguments' => array('element' => NULL), ), 'fieldgroup_display_overview_form' => array( 'arguments' => array('form' => NULL), ), ); } /** * Implementation of hook_elements(). */ function fieldgroup_elements() { return array( 'fieldgroup_simple' => array(), 'fieldgroup_fieldset' => array('#collapsible' => FALSE, '#collapsed' => FALSE, '#value' => NULL,), ); } /** * Implementation of hook_fieldapi(). */ function fieldgroup_content_fieldapi($op, $field) { switch ($op) { case 'delete instance': db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE field_name = '%s' AND type_name = '%s'", $field['field_name'], $field['type_name']); cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); break; } } function fieldgroup_group_edit_form(&$form_state, $type_name, $group_name) { $content_type = content_types($type_name); $groups = fieldgroup_groups($content_type['type']); if (!$group = $groups[$group_name]) { drupal_not_found(); exit; } $form['label'] = array( '#type' => 'textfield', '#title' => t('Label'), '#default_value' => $group['label'], '#required' => TRUE, ); // Set a default value for group type early in the form so it // can be overridden by subsequent form elements added by other modules. $group_type = !empty($group['group_type']) ? $group['group_type'] : 'standard'; $form['group_type'] = array('#type' => 'hidden', '#default_value' => $group_type); $form['settings']['#tree'] = TRUE; $form['settings']['form'] = array( '#type' => 'fieldset', '#title' => t('Form settings'), '#description' => t('These settings apply to the group in the node editing form.'), ); $form['settings']['form']['style'] = array( '#type' => 'radios', '#title' => t('Style'), '#default_value' => $group['settings']['form']['style'], '#options' => array( 'fieldset' => t('always open'), 'fieldset_collapsible' => t('collapsible'), 'fieldset_collapsed' => t('collapsed'), ) ); $form['settings']['form']['description'] = array( '#type' => 'textarea', '#title' => t('Help text'), '#default_value' => $group['settings']['form']['description'], '#rows' => 5, '#description' => t('Instructions to present to the user on the editing form.'), '#required' => FALSE, ); $form['settings']['display'] = array( '#type' => 'fieldset', '#title' => t('Display settings'), '#description' => t('These settings apply to the group on node display.'), ); $form['settings']['display']['description'] = array( '#type' => 'textarea', '#title' => t('Description'), '#default_value' => $group['settings']['display']['description'], '#rows' => 5, '#description' => t('A description of the group.'), '#required' => FALSE, ); foreach (array_keys(content_build_modes()) as $key) { $form['settings']['display'][$key]['format'] = array('#type' => 'value', '#value' => isset($group['settings']['display'][$key]['format']) ? $group['settings']['display'][$key]['format'] : 'fieldset'); $form['settings']['display'][$key]['exclude'] = array('#type' => 'value', '#value' => isset($group['settings']['display'][$key]['exclude']) ? $group['settings']['display'][$key]['exclude'] : 0); } $form['settings']['display']['label'] = array('#type' => 'value', '#value' => $group['settings']['display']['label']); $form['weight'] = array('#type' => 'hidden', '#default_value' => $group['weight']); $form['group_name'] = array('#type' => 'hidden', '#default_value' => $group_name); $form['parent'] = array('#type' => 'hidden', '#default_value' => $group['parent']); $form['#content_type'] = $content_type; $form['submit'] = array( '#type' => 'submit', '#value' => t('Save'), '#weight' => 10, ); return $form; } function fieldgroup_group_edit_form_submit($form, &$form_state) { $form_values = $form_state['values']; $content_type = $form['#content_type']; fieldgroup_save_group($content_type['type'], $form_values); $form_state['redirect'] = 'admin/content/node-type/'. $content_type['url_str'] .'/fields'; } function fieldgroup_remove_group(&$form_state, $type_name, $group_name) { $content_type = content_types($type_name); $groups = fieldgroup_groups($content_type['type']); $group = isset($groups[$group_name]) ? $groups[$group_name] : ''; if (empty($group)) { drupal_not_found(); exit; } $form['#submit'][] = 'fieldgroup_remove_group_submit'; $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']))), 'admin/content/node-type/'. $content_type['url_str'] .'/fields', t('This action cannot be undone.'), t('Remove'), t('Cancel')); } function fieldgroup_remove_group_submit($form, &$form_state) { $form_values = $form_state['values']; $content_type = $form['#content_type']; $group_name = $form['#group_name']; $parent = db_fetch_array(db_query("SELECT parent FROM {". fieldgroup_tablename() ."} WHERE group_name = '%s' AND type_name = '%s'", $group_name, $content_type['type'])); $result = db_query("UPDATE {". fieldgroup_tablename() ."} SET parent = '%s' WHERE parent = '%s' AND type_name = '%s'", $parent['parent'], $group_name, $content_type['type']); $result = db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET group_name = '%s' WHERE group_name = '%s' AND type_name = '%s'", $parent['parent'], $group_name, $content_type['type']); 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'; } /** * Returns all groups for a content type */ function fieldgroup_groups($content_type = '', $sorted = FALSE, $reset = FALSE) { global $language; static $groups, $groups_sorted; if (!isset($groups) || $reset) { if ($cached = cache_get('fieldgroup_data:'. $language->language, content_cache_tablename())) { $data = $cached->data; $groups = $data['groups']; $groups_sorted = $data['groups_sorted']; } else { $result = db_query("SELECT * FROM {". fieldgroup_tablename() ."} ORDER BY type_name, weight"); $groups = array(); $groups_sorted = array(); while ($group = db_fetch_array($result)) { $groups[$group['type_name']] = _fieldgroup_get_tree($group['type_name']); $groups_sorted[$group['type_name']][] = &$groups[$group['type_name']][$group['group_name']]; } cache_set('fieldgroup_data:'. $language->language, array('groups' => $groups, 'groups_sorted' => $groups_sorted), content_cache_tablename()); } } if (empty($content_type)) { return $groups; } elseif (empty($groups) || empty($groups[$content_type])) { return array(); } return $sorted ? $groups_sorted[$content_type] : $groups[$content_type]; } /** * create a tree of fieldgroups for nesting them */ function _fieldgroup_get_tree($type_name, $parent = '', $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(); $result = db_query("SELECT * FROM {". fieldgroup_tablename() ."} WHERE type_name='%s' ORDER BY weight", $type_name); 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'], ); $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['group_name']; $parents[$type_name][$group['group_name']][] = $group['parent']; $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; // Unserialize arrays. foreach (array('widget_settings', 'display_settings', 'global_settings', 'db_columns') as $key) { $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']] = (!empty($groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']][$key])) ? (array) unserialize($groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']][$key]) : array(); } // For fields inside of groups, use the weight given by fieldgroup $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']]['weight'] = $field['weight']; } } $max_depth = (is_null($max_depth)) ? count($children[$type_name]) : $max_depth; if (isset($children[$type_name][$parent])) { foreach ($children[$type_name][$parent] 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 (!empty($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); $labels[''] = '<'. t('none') .'>'; foreach ($groups as $group_name => $group) { $labels[$group_name] = t($group['label']); } return $labels; } function _fieldgroup_field_get_group($content_type, $field_name) { return db_result(db_query("SELECT group_name FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $content_type, $field_name)); } /** * Return an array of the parents for a field or group. * * @param $content_type The content type of the field/group. * @param $name The name of a group or field. */ function fieldgroup_get_parents($content_type, $name) { $is_group = substr($name, 0, 6) == 'group_'; $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 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'], '#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']; //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) { $group_rows = array(); $field_rows = array(); //prepare data that will make this easier $groups = fieldgroup_groups($form['type']['#value']); if (!empty($groups)) { foreach ($groups as $name => $more) { $group_rows[] = $name; } } $fields = $form['#field_info']; if (!empty($fields)) { foreach ($fields as $name => $more) { $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) { $form[$name]['#depth'] = 0; } } //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--; } } // The group is only added here so it will appear in the export // when using Content Copy. elseif ($form_id == 'content_field_edit_form' && isset($form['widget'])) { $content_type = content_types($form['type_name']['#value']); $form['widget']['group'] = array( '#type' => 'value', '#value' => _fieldgroup_field_get_group($content_type['type'], $form['field_name']['#value']), ); } elseif ($form_id == 'content_field_overview_form') { $form['#validate'][] = 'fieldgroup_field_overview_form_validate'; $form['#submit'][] = 'fieldgroup_field_overview_form_submit'; } elseif ($form_id == 'content_display_overview_form' && !empty($form['#groups'])) { $form['#submit'][] = 'fieldgroup_display_overview_form_submit'; if (!isset($form['submit'])) { $form['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#weight' => 10); } } elseif ($form_id == 'content_field_remove_form') { $form['#submit'][] = 'fieldgroup_field_remove_form_submit'; } } /** * API for group name validation. * * Pulled into separate function to be re-usable. */ function fieldgroup_validate_name($group, $type_name) { $errors = array(); // No label. if (!$group['label']) { $errors['label'][] = t('You need to provide a label.'); } // No group name. if (!$group['group_name']) { $errors['group_name'][] = t('You need to provide a group name.'); } // Group name validation. else { $group_name = $group['group_name']; $group['group_type'] = !empty($group['group_type']) ? $group['group_type'] : 'standard'; // Add the 'group_' prefix. if (substr($group_name, 0, 6) != 'group_') { $group_name = 'group_'. $group_name; } // Invalid field name. if (!preg_match('!^group_[a-z0-9_]+$!', $group_name)) { $errors['group_name'][] = t('The group name %group_name is invalid. The name must include only lowercase unaccentuated letters, numbers, and underscores.', array('%group_name' => $group_name)); } if (strlen($group_name) > 32) { $errors['group_name'][] = t('The group name %group_name is too long. The name is limited to 32 characters, including the \'group_\' prefix.', array('%group_name' => $group_name)); } // Group name already exists. $groups = fieldgroup_groups($type_name); if (isset($groups[$group_name])) { $errors['group_name'][] = t('The group name %group_name already exists.', array('%group_name' => $group_name)); } if (empty($errors['group_name'])) { $group['group_name'] = $group_name; } } return array('group_name' => $group['group_name'], 'errors' => $errors); } function fieldgroup_field_overview_form_validate($form, &$form_state) { $form_values = $form_state['values']; $group = $form_values['_add_new_group']; if (array_filter(array($group['label'], $group['group_name']))) { $validation = fieldgroup_validate_name($group, $form['#type_name']); if (!empty($validation['errors'])) { foreach ($validation['errors'] as $type => $messages) { foreach ($messages as $message) { if ($type == 'label') { form_set_error('_add_new_group][label', t('Add new group:') .' '. $message); } else { form_set_error('_add_new_group][group_name', t('Add new group:') .' '. $message); } } } } $group_name = $validation['group_name']; form_set_value($form['_add_new_group']['group_name'], $group_name, $form_state); } else { // Fail validation if attempt to nest fields under a new group without the // proper information. Not raising an error would cause the nested fields // to get weights the user doesn't expect. foreach ($form_values as $key => $values) { if (isset($values['parent']) && $values['parent'] == '_add_new_group') { form_set_error('_add_new_group][label', t('Add new group: you need to provide a label.')); form_set_error('_add_new_group][group_name', t('Add new group: you need to provide a group name.')); break; } } } } function fieldgroup_field_overview_form_submit($form, &$form_state) { $form_values = $form_state['values']; $type_name = $form['#type_name']; // Create new group if needed. if (!empty($form_values['_add_new_group']['label'])) { $group = $form_values['_add_new_group']; $group['settings'] = field_group_default_settings($group['group_type']); fieldgroup_save_group($type_name, $group); $new_group_name = $group['group_name']; } // 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)) { // If newly added fields were added to a group: if (in_array($key, $add_field_rows)) { // We replace the '_add_*_field' key with the actual name of // the field that got added. // content_field_overview_form_submit() placed those // in $form_state['fields_added'] for us. if (isset($form_state['fields_added'][$key])) { $key = $form_state['fields_added'][$key]; } else { // No field was actually created : skip to next row. continue; } } // If the field was added to the newly created group, replace the // '_add_new_group' value with the actual name of the group. $parent = ($values['parent'] == '_add_new_group' && isset($new_group_name)) ? $new_group_name : $values['parent']; // TODO: check the parent group does exist ? fieldgroup_update_fields(array('field_name' => $key, 'group' => $parent, 'type_name' => $type_name)); } } 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). 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 = '%s' WHERE type_name = '%s' AND group_name = '%s'", $weight, $parent, $type_name, $key); } } cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); } function field_group_default_settings($group_type) { $settings = array( 'form' => array('style' => 'fieldset', 'description' => ''), 'display' => array('description' => '', 'label' => 'above'), ); module_load_include('inc', 'content', 'includes/content.admin'); foreach (array_keys(content_build_modes()) as $key) { $settings['display'][$key]['format'] = 'fieldset'; $settings['display'][$key]['exclude'] = 0; } // Allow other modules to add new default settings. $settings = array_merge($settings, module_invoke_all('fieldgroup_default_settings', $group_type)); return $settings; } function fieldgroup_display_overview_form_submit($form, &$form_state) { $form_values = $form_state['values']; $groups = fieldgroup_groups($form['#type_name']); foreach ($form_values as $key => $values) { if (in_array($key, $form['#groups'])) { $group = $groups[$key]; // Multigroup data (namely, subgroup data) ends up in the settings array as well // Prevent this data from ending up inside of ['settings']['display'] if (is_array($values['settings']) && array_key_exists('multigroup', $values['settings'])){ if (is_array($group['settings']['multigroup'])){ foreach ($values['settings']['multigroup'] as $group_key => $group_values){ $group['settings']['multigroup'][$group_key] = $values['settings']['multigroup'][$group_key]; } } else { $group['settings']['multigroup'] = $values['settings']['multigroup']; } unset($values['settings']['multigroup']); } // We have some numeric keys here, so we can't use array_merge. $group['settings']['display'] = $values + $group['settings']['display']; fieldgroup_save_group($form['#type_name'], $group); } } } function fieldgroup_field_remove_form_submit($form, &$form_state) { $form_values = $form_state['values']; // TODO: // - when a (non last) field is removed from a group, a 'ghost row' remains in the fields overview // - when the last field is removed, the group disappears // seems to be fixed when emptying the cache. db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $form_values['type_name'], $form_values['field_name']); } /** * Implementation of hook_nodeapi(). */ function fieldgroup_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { switch ($op) { 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 (is_string($name) && 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 ($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 (!empty($node->content[$name]) && $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; } } /** * Build the node content element needed to render a fieldgroup. * * @param $group * The field group definition. * @param $node * The node containing the field group to display. Can be a 'pseudo-node', * containing at least 'type', 'nid', 'vid', and the content for fields * required for the group. * @param $teaser * @param $page * Similar to hook_nodeapi('view'). * * @see fieldgroup_nodeapi() * @see fieldgroup_view_group() */ function fieldgroup_build_content($group, &$node, $teaser, $page) { // NODE_BUILD_NORMAL is 0, and ('whatever' == 0) is TRUE, so we need a ===. if ($node->build_mode === NODE_BUILD_NORMAL || $node->build_mode == NODE_BUILD_PREVIEW) { $context = $teaser ? 'teaser' : 'full'; } else { $context = $node->build_mode; } $group_name = $group['group_name']; // Do not include group labels when indexing content. if ($context == NODE_BUILD_SEARCH_INDEX) { $group['settings']['display']['label'] = 'hidden'; } $label = $group['settings']['display']['label'] == 'above'; $element = array( '#title' => $label ? check_plain(t($group['label'])) : '', '#description' => $label && !empty($group['settings']['display']['description']) ? content_filter_xss(t($group['settings']['display']['description'])) : '', ); $format = isset($group['settings']['display'][$context]['format']) ? $group['settings']['display'][$context]['format'] : 'fieldset'; switch ($format) { case 'simple': $element['#type'] = 'fieldgroup_simple'; $element['#group_name'] = $group_name; $element['#node'] = $node; break; case 'hidden': $element['#access'] = FALSE; break; case 'fieldset_collapsed': $element['#collapsed'] = TRUE; case 'fieldset_collapsible': $element['#collapsible'] = TRUE; case 'fieldset': $element['#type'] = 'fieldgroup_fieldset'; $element['#attributes'] = array('class' => 'fieldgroup '. strtr($group['group_name'], '_', '-')); break; } 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']; } } // Allow other modules to alter the group view. // Can't use module_invoke_all because we want // to be able to use a reference to $node and $element. foreach (module_implements('fieldgroup_view') as $module) { $function = $module .'_fieldgroup_view'; $function($node, $element, $group, $context); } // Unset the original field values now that we've moved them. foreach ($group['fields'] as $field_name => $field) { if (isset($node->content[$field_name])) { unset($node->content[$field_name]); } } // The wrapper lets us get the themed output for the group // to populate the $GROUP_NAME_rendered variable for node templates, // and hide it from the $content variable if needed. // See fieldgroup_preprocess_node(), theme_fieldgroup_wrapper(). $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'], ); $node->content[$group_name] = $wrapper; } /** * Render a single field group, fully themed with label. * * To be used by third-party code (Panels, ...) that needs to output an * isolated field group. Do *not* use inside node templates, use the * $GROUP_NAME_rendered variables instead. You can also use the 'simple' * style format and override the template fieldgroup-simple.tpl.php. * * By default, the field group is displayed using the settings defined for the * 'full node' or 'teaser' contexts (depending on the value of the $teaser param). * Set $node->build_mode to a different value to use a different context. * * Different settings can be specified by adjusting $group['settings']['display']. * * @param $group * The field group definition. * @param $node * The node containing the field group to display. Can be a 'pseudo-node', * containing at least 'type', 'nid', 'vid', and the field data required * for the group. * @param $teaser * @param $page * Similar to hook_nodeapi('view'). * @return * The themed output for the field group. * * @see content_view_field() */ function fieldgroup_view_group($group, &$node, $teaser = FALSE, $page = FALSE) { $group_name = $group['group_name']; $field_types = _content_field_types(); // Clone the node to prevent from altering the original. $node_copy = drupal_clone($node); // Use 'full'/'teaser' if not specified otherwise. $node_copy->build_mode = isset($node_copy->build_mode) ? $node_copy->build_mode : NODE_BUILD_NORMAL; // Build the content element for individual fields in the field group. if (!isset($node_copy->content)) { $node_copy->content = array(); } foreach (array_keys($group['fields']) as $field_name) { $field = content_fields($field_name, $node_copy->type); if (isset($node_copy->{$field_name})) { $items = $node_copy->{$field_name}; // One-field equivalent to _content_field_invoke('sanitize'). $module = $field_types[$field['type']]['module']; $function = $module .'_field'; if (function_exists($function)) { $function('sanitize', $node_copy, $field, $items, $teaser, $page); $node_copy->{$field_name} = $items; } $field_view = content_field('view', $node_copy, $field, $items, $teaser, $page); // content_field('view') adds a wrapper to handle variables and 'excluded' // fields for node templates. We bypass it and get the actual field. $node_copy->content[$field_name] = $field_view[$field_name]; } } // Build the content element of the field group itself. fieldgroup_build_content($group, $node_copy, $teaser, $page); // fieldgroup_build_content() adds a wrapper to handle variables and 'excluded' // groups for node templates. We bypass it and render the actual field group. $output = drupal_render($node_copy->content[$group_name]['group']); return $output; } /** * Hide specified fields from the $content variable in node templates. */ function fieldgroup_wrapper_post_render($content, $element) { $groups = fieldgroup_groups($element['#type_name']); $group = $groups[$element['#group_name']]; // The display settings are not in quite the same place in the // group and the field, so create the value the theme will expect. $group['display_settings'] = $group['settings']['display']; if (theme('content_exclude', $content, $group, $element['#context'])) { return ''; } return $content; } /** * Get the group name for a field. * If the field isn't in a group, FALSE will be returned. * @return The name of the group, or FALSE. */ function fieldgroup_get_group($content_type, $field_name) { foreach (fieldgroup_groups($content_type) as $group_name => $group) { if (in_array($field_name, array_keys($group['fields']))) { return $group_name; } } return FALSE; } /** * Implementation of hook_node_type() * React to change in node types */ function fieldgroup_node_type($op, $info) { if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) { // update the tables db_query("UPDATE {". fieldgroup_tablename() ."} SET type_name='%s' WHERE type_name='%s'", array($info->type, $info->old_type)); db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET type_name='%s' WHERE type_name='%s'", array($info->type, $info->old_type)); cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); } elseif ($op == 'delete') { db_query("DELETE FROM {". fieldgroup_tablename() ."} WHERE type_name = '%s'", $info->type); db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s'", $info->type); } } function fieldgroup_types() { $types = array('standard' => t('Standard group')); // Allow other modules to add new group_types. $types = array_merge($types, module_invoke_all('fieldgroup_types')); return $types; } function fieldgroup_tablename($version = NULL) { if (is_null($version)) { $version = variable_get('fieldgroup_schema_version', 0); } return $version < 6000 ? 'node_group' : 'content_group'; } function fieldgroup_fields_tablename($version = NULL) { if (is_null($version)) { $version = variable_get('fieldgroup_schema_version', 0); } return $version < 6000 ? 'node_group_fields' : 'content_group_fields'; } /** * CRUD API for fieldgroup module. * * @todo * Make this into more of a real API for groups. */ /** * Saves the given group for this content-type */ function fieldgroup_save_group($type_name, $group) { $groups = fieldgroup_groups($type_name); // Allow other modules to intervene when the group is saved. foreach (module_implements('fieldgroup_save_group') as $module) { $function = $module .'_fieldgroup_save_group'; $function($group); } if (!isset($groups[$group['group_name']])) { // Accept group name from programmed submissions if valid. db_query("INSERT INTO {". fieldgroup_tablename() ."} (parent, 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['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 parent = '%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['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; } } function fieldgroup_update_fields($form_values) { $default = _fieldgroup_field_get_group($form_values['type_name'], $form_values['field_name']); if ($default != $form_values['group']) { if ($form_values['group'] && !$default) { db_query("INSERT INTO {". fieldgroup_fields_tablename() ."} (type_name, group_name, field_name) VALUES ('%s', '%s', '%s')", $form_values['type_name'], $form_values['group'], $form_values['field_name']); } elseif ($form_values['group']) { db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET group_name = '%s' WHERE type_name = '%s' AND field_name = '%s'", $form_values['group'], $form_values['type_name'], $form_values['field_name']); } else { db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $form_values['type_name'], $form_values['field_name']); } cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); } } function fieldgroup_delete($content_type, $group_name) { db_query("DELETE FROM {". fieldgroup_tablename() ."} WHERE type_name = '%s' AND group_name = '%s'", $content_type, $group_name); db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND group_name = '%s'", $content_type, $group_name); cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE); } /** * Format a fieldgroup using a 'fieldset'. * * Derived from core's theme_fieldset, with no output if the content is empty. */ function theme_fieldgroup_fieldset($element) { if (empty($element['#children']) && empty($element['#value'])) { return ''; } if ($element['#collapsible']) { drupal_add_js('misc/collapse.js'); if (!isset($element['#attributes']['class'])) { $element['#attributes']['class'] = ''; } $element['#attributes']['class'] .= ' collapsible'; if ($element['#collapsed']) { $element['#attributes']['class'] .= ' collapsed'; } } return '
\n"; } /** * Process variables for fieldgroup.tpl.php. * * The $variables array contains the following arguments: * - $group_name * - $group_name_css * - $label * - $description * - $content * * @see fieldgroup-simple.tpl.php */ function template_preprocess_fieldgroup_simple(&$vars) { $element = $vars['element']; $vars['parent'] = isset($element['#parent']) ? $element['#parent'] : NULL; $vars['group_name'] = $element['#group_name']; $vars['group_name_css'] = strtr($element['#group_name'], '_', '-'); $vars['label'] = isset($element['#title']) ? $element['#title'] : '';; $vars['description'] = isset($element['#description']) ? $element['#description'] : '';; $vars['content'] = isset($element['#children']) ? $element['#children'] : ''; $vars['template_files'] = array( 'fieldgroup-simple-', 'fieldgroup-simple-'. $element['#group_name'], 'fieldgroup-simple-'. $element['#node']->type, 'fieldgroup-simple-'. $element['#group_name'] .'-'. $element['#node']->type, ); } /** * Theme preprocess function for node. * * Adds $GROUP_NAME_rendered variables, * containing the themed output for the whole group. */ function fieldgroup_preprocess_node(&$vars) { $node = $vars['node']; foreach (fieldgroup_groups($node->type) as $group_name => $group) { // '#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'] : ''; } }