' . t('The Conditional Fields module allows to set fields with allowed values as "controlling fields" for other fields and groups. When a field or group is "controlled", it will only be available for editing and displayed if the selected values of the controlling field match the "trigger values" assigned to it. You can, for example, make a custom "article teaser" field that is shown only if a "Has teaser" checkbox is checked.') . '

'; $output .= '

' . t('When editing a node, the controlled fields are dynamically shown and hidden with javascript.') . '

'; $output .= '

' . t('On node view, the controlled fields which were left untriggered are hidden.') . '

'; $output .= '

' . t('Once the module is activated, a new set of options will appear in the editing form of cck fields, from where you can select which of the allowed values available of candidate "controlling" fields will make the field "controlled". If - Not controlling - or no value is selected, the field will be shown as usual.') . '

'; $output .= '

' . t('These are the requisites to make a field controllable:') . '

'; $output .= ''; $output .= '

' . t('There is also a "Conditional fields" settings tab in every content type admin page.') . '

'; $output .= '

' . t('The Conditional Fields Handbook contains further explanations and examples.', array('@handbook' => url('http://drupal.org/node/475488'))) . '

'; return $output; break; } } /** * Implementation of hook_menu(). */ function conditional_fields_menu() { $items = array(); foreach (node_get_types() as $type) { $content_type = content_types($type->type); $items['admin/content/node-type/' . $content_type['url_str'] . '/conditional'] = array( 'title' => 'Conditional fields', 'page callback' => 'drupal_get_form', 'page arguments' => array('_conditional_fields_admin', $content_type['type']), 'access arguments' => array('administer conditional fields'), 'type' => MENU_LOCAL_TASK, 'weight' => 5 ); } return $items; } /** * Implementation of hook_perm(). */ function conditional_fields_perm() { return array('administer conditional fields'); } /** * Administration form for conditional fields */ function _conditional_fields_admin($form, $type) { $form = array(); $form['js_set'] = array( '#type' => 'fieldset', '#title' => t('User Interface options'), '#collapsible' => TRUE ); $form['js_set']['js'] = array( '#type' => 'radios', '#options' => array( C_FIELDS_JS_NO => t("Don't use javascript. Fields are only hidden on node view."), C_FIELDS_JS_HIDE => t('Hide untriggered fields.'), C_FIELDS_JS_DISABLE => t('Disable untriggered fields.') ), '#title' => 'Javascript', '#description' => t('Choose the desired javascript behaviour in node editing forms.'), '#default_value' => variable_get('c_fields_js_' . $type, C_FIELDS_JS_HIDE) ); $form['js_set']['anim'] = array( '#type' => 'fieldset', '#title' => t('Animation'), '#description' => t('These settings have effect only if you select the "Hide untriggered fields" option above.'), ); $form['js_set']['anim']['animation'] = array( '#type' => 'radios', '#title' => t('Type'), '#default_value' => variable_get('c_fields_animation_' . $type, C_FIELDS_ANIMATION_NO), '#options' => array( C_FIELDS_ANIMATION_NO => t('No animation'), C_FIELDS_ANIMATION_FADE => t('Slide down'), C_FIELDS_ANIMATION_SLIDE => t('Fade'), ), ); $form['js_set']['anim']['anim_speed'] = array( '#type' => 'radios', '#title' => t('Speed'), '#description' => t('The speed at which the animation is performed. Slow = 600ms; Normal = 400ms; Fast = 200ms.'), '#default_value' => variable_get('c_fields_anim_speed_' . $type, 'normal'), '#options' => array( 'slow' => t('Slow'), 'normal' => t('Normal'), 'fast' => t('Fast'), ), ); $form['orphaned'] = array( '#type' => 'fieldset', '#title' => t('Orphaned controlled fields settings'), '#description' => t('Configure the visibility/editability of controlled fields whose controlling fields are not visible/editable.'), '#collapsible' => TRUE, '#collapsed' => TRUE ); $options = array(C_FIELDS_ORPHANED_HIDE => t('Hide'), C_FIELDS_ORPHANED_SHOW_TRIGGERED => t('Show only if triggered'), C_FIELDS_ORPHANED_SHOW_ALL => t('Show')); $form['orphaned']['orphaned_view'] = array( '#type' => 'radios', '#title' => t('On node view'), '#options' => $options, '#default_value' => variable_get('c_fields_view_' . $type, C_FIELDS_ORPHANED_SHOW_TRIGGERED) ); $form['orphaned']['orphaned_edit'] = array( '#type' => 'radios', '#title' => t('On node edit'), '#options' => $options, '#default_value' => variable_get('c_fields_edit_' . $type, C_FIELDS_ORPHANED_SHOW_TRIGGERED) ); $form['reset_default'] = array( '#type' => 'checkbox', '#title' => t('Reset untriggered fields to default values'), '#description' => t('Select this box to reset untriggered controlled fields to their default values when saving a node.'), '#default_value' => variable_get('c_fields_reset_default_' . $type, 1) ); $form['show_all'] = array( '#type' => 'checkbox', '#title' => t('Administrators see all fields'), '#description' => t('Select this box to let users with the administer conditional fields permission to view all controlled fields of a node.', array('@access-control-page' => url('admin/user/permissions', array('fragment' => 'module-conditional_fields')))), '#default_value' => variable_get('c_fields_show_all_' . $type, 0) ); $form['delete'] = array( '#type' => 'checkbox', '#title' => t('Delete'), '#description' => t('Delete all conditional fields configured for this content type. This will delete the conditional fields settings, not the fields themselves.'), '#default_value' => 0 ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Save'), ); return $form; } function _conditional_fields_admin_submit($form, &$form_state) { $type = $form['#parameters'][2]; variable_set('c_fields_js_' . $type, $form_state['values']['js']); variable_set('c_fields_animation_' . $type, $form_state['values']['animation']); variable_set('c_fields_anim_speed_' . $type, $form_state['values']['anim_speed']); variable_set('c_fields_view_' . $type, $form_state['values']['orphaned_view']); variable_set('c_fields_edit_' . $type, $form_state['values']['orphaned_edit']); variable_set('c_fields_reset_default_' . $type, $form_state['values']['reset_default']); variable_set('c_fields_show_all_' . $type, $form_state['values']['show_all']); drupal_set_message(t('Conditional fields options for this content type saved.') . $message); if ($form_state['values']['delete'] == 1) { conditional_fields_node_type_delete($type); drupal_set_message(t('All configured conditional fields for this content type have been deleted.')); } } /** * Implementation of hook_nodeapi(). */ function conditional_fields_nodeapi(&$node, $op, $teaser, $page) { if ($op == 'view') { // First we check if there any conditional fields in this node type $type = content_types($node->type); if (!$data = conditional_fields_load_data($type['type'])) { return; } // Then we check if user is an administrator and this content type // and has the show hidden fields pref enabled if (user_access('administer conditional fields') && variable_get('c_fields_show_all_' . $type['type'], 0)) { return; } foreach ($data as $field) { // We might have to look for the field in a group $controlled_group = conditional_fields_get_group($node->type, $field['field_name']); $controlling_group = conditional_fields_get_group($node->type, $field['control_field_name']); // The controlled field is not in a group and is not in the form for other reasons. Skip. if (!$controlled_group && !$node->content[$field['field_name']]) { continue; } // The controlled field is in a group and is not the form for other reasons. Skip. if ($controlled_group && !$node->content[$controlled_group]['group'][$field['field_name']]) { continue; } $viewed = FALSE; $current_values[$field['control_field_name']] = array(); // Create an array with the selected controlling field's values // Check if the controlling field is viewed as well $field_key = array_keys($type['fields'][$field['control_field_name']]['columns']); foreach ((array)$node->$field['control_field_name'] as $value) { $current_values[$field['control_field_name']][] = $value[$field_key[0]]; if (!empty($value[$field_key[0]])) { if ($node->content[$field['control_field_name']]['field']['#access'] == TRUE || ($controlling_group && $node->content[$controlling_group]['group'][$field['control_field_name']]['field']['#access'] == TRUE)) { $viewed = TRUE; } } } if ($viewed) { // Hide the controlled field if it is not triggered if (!conditional_fields_is_triggered($current_values[$field['control_field_name']], $field['trigger_values'])) { if ($controlled_group) { $node->content[$controlled_group]['group'][$field['field_name']]['#access'] = FALSE; } else { $node->content[$field['field_name']]['#access'] = FALSE; } } } else { // Apply orphaned fields settings $orphaned_settings = variable_get('c_fields_view_' . $node->type, C_FIELDS_ORPHANED_SHOW_TRIGGERED); switch ($orphaned_settings) { case C_FIELDS_ORPHANED_SHOW_TRIGGERED: // If the field was triggered, don't hide it if (conditional_fields_is_triggered($current_values[$field['control_field_name']], $field['trigger_values'])) { break; } case C_FIELDS_ORPHANED_HIDE: // We hide the field if ($controlled_group) { $node->content[$controlled_group]['group'][$field['field_name']]['#access'] = FALSE; } else { $node->content[$field['field_name']]['#access'] = FALSE; } case C_FIELDS_ORPHANED_SHOW_ALL: // Nothing to do... break; } } } } } /** * Implementation of hook_form_alter(). */ function conditional_fields_form_alter(&$form, $form_state, $form_id) { switch ($form_id) { case 'content_field_edit_form': if (!isset($form_state['clicked_button']) || $form_state['clicked_button']['#parents'][0] != 'change') { conditional_fields_content_admin_field($form); } break; case 'fieldgroup_group_edit_form': conditional_fields_fieldgroup_group_edit_form($form); break; case 'content_field_overview_form': // Find conditional fields, mark them, and disable group select for them $conditional_fields = conditional_fields_field_overview_form($form); break; case '_content_admin_field_remove': $form['#submit'] = $form['#submit'] + array('_conditional_fields_content_admin_field_remove_submit' => array()); break; case 'fieldgroup_remove_group': $form['#submit'][] = 'conditional_fields_fieldgroup_remove_group_submit'; break; case 'content_add_more_js': // Handle ahah multiple fields foreach ($form as $item_name => $item) { if (db_result(db_query("SELECT COUNT(*) FROM {conditional_fields} WHERE field_name = '%s'", $item_name))) { $form[$item_name]['#post_render'] = array_merge(array('conditional_fields_add_more_post_render'), (array)$form[$item_name]['#post_render']); foreach (element_children($form[$item_name]) as $element) { conditional_fields_custom_required_field($form[$item_name][$element]); } } } break; case 'content_copy_import_form': $form['#submit'][] = 'conditional_fields_import'; break; } if (isset($form['type']['#value']) && $form_id == $form['type']['#value'] . '_node_form') { conditional_fields_node_editing_form($form, $form_state); } } /** * Alteration of the field editing form */ function conditional_fields_content_admin_field(&$form) { if (!user_access('administer conditional fields')) { return; } $type = (array)content_types($form['type_name']['#value']); // Load conditional fields data. $data = conditional_fields_load_data($type['type']); // Get all fields controlled by this one. $controlled_fields = array(); foreach ($data as $row) { if ($row['control_field_name'] == $form['field_name']['#value']) { $controlled_fields[$row['field_name']] = $row['trigger_values']; } } // Build controlling field form. if (!empty($controlled_fields)) { // Needed to handle nested fields. $form['#controlled_fields'] = $controlled_fields; // Add validation function $form['#validate'] = array_merge(array('conditional_fields_content_admin_field_validate'), $form['#validate']); $form['controlled_fields'] = array( '#type' => 'value', '#value' => $controlled_fields, ); $controlled_fieldset = conditional_fields_content_admin_field_controlling($type, $controlled_fields); } // Check if field is in a group if (isset($form['widget']['group'])) { $group_of_controlled_field = $form['widget']['group']['#value']; } // Get candidate controlling fields foreach ($type['fields'] as $field) { // - Exclude this one :) if ($field['field_name'] == $form['field_name']['#value']) { continue; } // - Exlude fields already controlled by this one if ($controlled_fields[$field['field_name']]) { continue; } // - Exclude fields without Allowed values if (!$allowed_values[$field['field_name']] = conditional_fields_allowed_values($field)) { continue; } // If a field is inside a controlled group, it can control or be controlled only by fields in the same group. // This is needed to avoid uncontrollable loops. // First we gather data about groups $group_of_candidate_field = conditional_fields_get_group($field['type_name'], $field['field_name']); if ($group_of_controlled_field || $group_of_candidate_field) { $group_of_controlled_field_is_controlled = FALSE; $group_of_candidate_field_is_controlled = FALSE; foreach ($data as $row) { if ($row['field_name'] == $group_of_controlled_field) { $group_of_controlled_field_is_controlled = TRUE; } elseif ($row['field_name'] == $group_of_candidate_field) { $group_of_candidate_field_is_controlled = TRUE; } } } // If the controlled field is in a group... if ($group_of_controlled_field) { // - Exclude candidates outside the controlled field's group if the group is controlled if (!$group_of_candidate_field && $group_of_controlled_field_is_controlled) { continue; } // - Exclude candidates in different groups if either group is controlled elseif ($group_of_candidate_field && $group_of_candidate_field != $group_of_controlled_field && ($group_of_candidate_field_is_controlled || $group_of_controlled_field_is_controlled)) { continue; } } // Else if the controlled field is not in a group... else { // - Exclude fields in controlled groups if ($group_of_candidate_field && $group_of_candidate_field_is_controlled) { continue; } } $available_fields[$field['field_name']] = $field; } if (isset($available_fields)) { $default_values = conditional_fields_available_fields_default_values($form['field_name']['#value'], $available_fields); $available_fieldset = conditional_fields_content_admin_field_controllable($type, $default_values, $available_fields, $allowed_values, 'field'); // Add validation function $form['#validate'] = array_merge(array('conditional_fields_content_admin_field_validate'), $form['#validate']); // Add submission function $form['#submit'] = array_merge(array('conditional_fields_forms_submit'), $form['#submit']); } // Add the fieldsets to the form if ($controlled_fieldset || $available_fieldset) { $form['widget']['conditional_fields'] = array( '#type' => 'fieldset', '#title' => t('Conditional fields settings'), '#tree' => TRUE, '#collapsible' => TRUE, '#collapsed' => is_null($default_values), '#weight' => 8, '#attributes' => array('id' => 'conditional-fields-settings'), ); if ($controlled_fieldset) { $form['widget']['conditional_fields']['controlling_fields'] = $controlled_fieldset; } if ($available_fieldset) { if ($controlled_fieldset) { $available_fieldset['#description'] .= '

' . t('Note: this field is already controlling other fields. If you make it controlled, its own controlled fields will be assigned the same settings.') . '

'; } $form['widget']['conditional_fields']['available_fields'] = $available_fieldset; } } return; } /** * Controlling field settings form * (just a table containing information about controlled fields) */ function conditional_fields_content_admin_field_controlling($type, $controlled_fields) { foreach ($controlled_fields as $field => $trigger_values) { if (strpos($field , 'group_') === 0) { // It's a group $rows[] = array($field, implode($trigger_values, ', ') . ' (' . t('edit', array('@edit-group' => url('admin/content/node-type/' . $type['url_str'] . '/groups/' . $field, array('fragment' => 'conditional-fields-settings')))) . ')', t('group') ); } else { // It's a field $rows[] = array($field, implode($trigger_values, ', ') . ' ('. t('edit', array('@edit-field' => url('admin/content/node-type/' . $type['url_str'] . '/fields/' . $field, array('fragment' => 'conditional-fields-settings')))) . ')', t('field'), ); } } $output = array( '#type' => 'fieldset', '#title' => t('Controlled fields') . ' (' . count($rows) . ')', '#collapsible' => TRUE, '#collapsed' => TRUE, '#description' => '

' . t('Below are listed all fields and groups controlled by this field.') . '

', 'controlled_fields_table' => array('#value' => theme('table', array(t('Name'), t('Trigger values'), t('Type')), $rows)), ); return $output; } /** * Controllable field settings form */ function conditional_fields_content_admin_field_controllable($type, $default_values, $available_fields, $allowed_values, $context) { $output = array( '#type' => 'fieldset', '#title' => t('Controlling fields'), '#collapsible' => TRUE, '#collapsed' => FALSE, '#description' => '

' . t('Choose which allowed values of available controlling fields will trigger this @context, making it visible both in node editing and view. If no value is set, the @context will be always visible. Only fields with Allowed values set are available for control. If a field is inside a controlled group, it can control or be controlled only by fields in the same group.', array('@context' => $context)) . '

', ); // Create selection lists foreach ($available_fields as $field) { $allowed_values[$field['field_name']] = array('conditional_field_no_value' => t('- Not controlling -')) + $allowed_values[$field['field_name']]; if (isset($default_values[$field['field_name']]) && $default_values[$field['field_name']] != FALSE) { $default_value = $default_values[$field['field_name']]; } else { $default_value = 'conditional_field_no_value'; } $output[$field['field_name']] = array( '#type' => 'select', '#multiple' => TRUE, '#title' => $field['widget']['label'] . ' (' . $field['field_name'] . ')', // To do: set right url for groups '#description' => t('Edit the allowed values of %field-name.', array('@edit-field' => url('admin/content/node-type/' . $type['url_str'] . '/fields/' . $field['field_name'], array('query' => 'destination=admin/content/node-type/' . arg(3) . '/' . arg(4) . '/' . arg(5))), '%field-name' => $field['field_name']) ), '#options' => $allowed_values[$field['field_name']], '#default_value' => $default_value, ); } return $output; } /** * Alteration of the fieldgroup editing form */ function conditional_fields_fieldgroup_group_edit_form(&$form) { if (!user_access('administer conditional fields')) { return; } // A group that contains fields controlling or controlled by fields in other groups or no groups at all, // cannot be a controlled group. if ($data = conditional_fields_load_data($form['#content_type']['type'])) { $groups = fieldgroup_groups($form['#content_type']['type']); $group = $groups[$form['group_name']['#default_value']]; foreach ($data as $row) { if (isset($group['fields'][$row['field_name']]) XOR isset($group['fields'][$row['control_field_name']])) { return; } } } // Find fields with allowed values which are not inside a group foreach ($form['#content_type']['fields'] as $field) { $in_group = fieldgroup_get_group($form['#content_type']['type'], $field['field_name']); if (!$in_group) { if ($allowed_values[$field['field_name']] = conditional_fields_allowed_values($field)) { $available_fields[$field['field_name']] = $field; } } } if (isset($available_fields)) { $default_values = conditional_fields_available_fields_default_values($form['group_name']['#default_value'], $available_fields); // Add the fieldset to the form $form['widget']['conditional_fields'] = array( '#type' => 'fieldset', '#title' => t('Conditional fields settings'), '#tree' => TRUE, '#collapsible' => TRUE, '#collapsed' => is_null($default_values), '#weight' => 8, '#attributes' => array('id' => 'conditional-fields-settings'), ); $form['widget']['conditional_fields']['available_fields'] = conditional_fields_content_admin_field_controllable($form['#content_type'], $default_values, $available_fields, $allowed_values, 'group'); // Add validation function $form['#validate'][] = 'conditional_fields_content_admin_field_validate'; // Add submission function $form['#submit'] = array_merge(array('conditional_fields_forms_submit'), $form['#submit']); } return; } /** * Check selection of values */ function conditional_fields_content_admin_field_validate($form, &$form_state) { if ($form_state['values']['conditional_fields']['available_fields']) { $count_selected = 0; foreach ($form_state['values']['conditional_fields']['available_fields'] as $available_field => $trigger_values) { // Disallow selecting "Not controlling" and values at the same time if ($trigger_values['conditional_field_no_value'] && count($trigger_values) > 1) { form_set_error('conditional_fields][' . $available_field, t('You cannot select "Not controlling" and other values at the same time.')); } } } else { // Warn user on allowed values change if (!$GLOBALS['content_copy'] && !empty($form['#controlled_fields']) && $form_state['values']['allowed_values'] != $form_state['#field_info'][$form['values']['field_name']]['allowed_values']) { drupal_set_message(t('If you changed or removed any allowed value from the field, you might have to review its controlled fields and fieldgroups settings.'), 'warning'); } } } /** * Handle saving of conditional field settings. * The controlled field can be either a field or a group */ function conditional_fields_forms_submit($form, &$form_state) { $controlled_field = (isset($form_state['values']['field_name']) ? $form_state['values']['field_name'] : $form_state['values']['group_name']); $type = (isset($form['#field']['type_name']) ? $form['#field']['type_name'] : $form['#content_type']['type']); conditional_fields_save_field($type, $controlled_field, $form_state['values']['conditional_fields']['available_fields']); // Fields already controlled should share the same settings of their controlling fields if ($form_state['values']['controlled_fields']) { foreach ($form_state['values']['controlled_fields'] as $field_name => $trigger_values) { conditional_fields_save_field($type, $field_name, $form_state['values']['conditional_fields']['available_fields']); } } } /** * Find the allowed values for a field */ function conditional_fields_allowed_values($field) { static $options; if ($options[$field['field_name']]) { return $options[$field['field_name']]; } $function = $field['module'] .'_allowed_values'; $options[$field['field_name']] = function_exists($function) ? $function($field) : content_allowed_values($field); // Strip tags, since we use a select, not the real widget. foreach ($options[$field['field_name']] as $key => $label) { $options[$field['field_name']][$key] = strip_tags($label); } return $options[$field['field_name']]; } /** * Alter node form. We do it in after_build for compatibility * with non-core CCK widgets */ function conditional_fields_node_editing_form(&$form, $form_state) { $form['#after_build'][] = 'conditional_fields_node_after_build'; } /** * Main tasks: * - Create javascript settings * - Prepare custom validation for required controlled fields * - Assign a theme function to conditional fields * - Apply orphaned fields settings if applicable */ function conditional_fields_node_after_build($form, &$form_state) { // Avoid running twice when the form is rebuilt with AHAH if ($form_state['clicked_button']['#ahah']) { return $form; } $type_name = $form['type']['#value']; // Do nothing if there are no conditional fields if (!$data = conditional_fields_load_data($type_name)) { return $form; } $controlling_fields = array(); $missing_controlling_fields = array(); $controlled_fields = array(); $required_fields = array(); $js_settings = array(); foreach ($data as $row) { $controlling_fields[$row['control_field_name']][$row['field_name']] = $row['trigger_values']; $controlled_fields[$row['field_name']][$row['control_field_name']] = $row['trigger_values']; } /* Handle controlling fields */ foreach ($controlling_fields as $controlling_field_name => $controlling_field_descendants) { // Check if the controlling field is in the form, user has access to it, and is editable. $group_of_controlling_field = conditional_fields_get_group($type_name, $controlling_field_name); $controlling_field = conditional_fields_item_in_form($form, $controlling_field_name, $group_of_controlling_field); if (!$controlling_field || $controlling_field['#access'] === FALSE || $controlling_field['#type'] == 'markup') { $missing_controlling_fields[] = $controlling_field_name; continue; } // Set values on form for themeing. if ($group_of_controlling_field) { $form[$group_of_controlling_field][$controlling_field_name]['#controlling_fields'] = TRUE; conditional_fields_item_apply_theme($form[$group_of_controlling_field][$controlling_field_name]); } else { $form[$controlling_field_name]['#controlling_fields'] = TRUE; conditional_fields_item_apply_theme($form[$controlling_field_name]); } } /* Handle controlled fields */ foreach ($controlled_fields as $controlled_field_name => $controlled_field_parents) { // Check if the controlled field is in the form and user has access to it. $group_of_controlled_field = conditional_fields_get_group($type_name, $controlled_field_name); $controlled_field = conditional_fields_item_in_form($form, $controlled_field_name, $group_of_controlled_field); if (!$controlled_field || $controlled_field['#access'] === FALSE) { continue; } // Handle orphaned fields. foreach ($missing_controlling_fields as $missing_controlling_field) { unset($controlled_field_parents[$missing_controlling_field]); } if (count($controlled_field_parents) == 0) { $orphaned_settings = variable_get('c_fields_edit_' . $type_name, C_FIELDS_ORPHANED_SHOW_TRIGGERED); switch ($orphaned_settings) { case C_FIELDS_ORPHANED_SHOW_TRIGGERED: // Show only triggered fields. E.g.: fields whose controlling // fields have triggering values set by default, or set by // another user with permissions. $triggered = TRUE; foreach ($controlling_fields as $controlling_field_name => $controlling_field_descendants) { if ($controlling_field_descendants[$controlled_field_name]) { if (!conditional_fields_is_triggered($form_state['values'][$controlling_field_name], $controlling_field_descendants[$controlled_field_name])) { $triggered = FALSE; break; } } } if ($triggered) { break; } case C_FIELDS_ORPHANED_HIDE: // Unset controlled field. if ($group_of_controlled_field) { unset($form[$group_of_controlled_field][$controlled_field_name]); } else { unset($form[$controlled_field_name]); } break; case C_FIELDS_ORPHANED_SHOW_ALL: // Do nothing: the default behavior is ok. } continue; } if ($controlled_field['#required']) { $required_fields[$controlled_field_name] = array('field' => $controlled_field_name, 'in_group' => $group_of_controlled_field); } $is_group = (strpos($controlled_field_name , 'group_') === 0 ? TRUE : FALSE); // Required fields inside a controlled fieldgroup // must be handled by conditional fields. if ($is_group) { foreach (element_children($controlled_field) as $group_element) { if ($controlled_field[$group_element]['#required'] && !$controlled_fields[$group_element]) { $required_fields[$group_element] = array('field' => $group_element, 'in_group' => $controlled_field_name); } } } // Set values on form for themeing. if ($group_of_controlled_field) { $form[$group_of_controlled_field][$controlled_field_name]['#controlled_fields'] = TRUE; conditional_fields_item_apply_theme($form[$group_of_controlled_field][$controlled_field_name]); } else { $form[$controlled_field_name]['#controlled_fields'] = TRUE; $is_group ? conditional_fields_item_apply_theme($form[$controlled_field_name], $controlled_field_name) : conditional_fields_item_apply_theme($form[$controlled_field_name]); } // Add fields to javascript settings // TODO: Use unique ids (requires per-widget settings) $js_controlled_field_id = '#conditional-' . conditional_fields_form_clean_id($controlled_field_name); foreach ($controlled_field_parents as $controlling_field_name => $trigger_values) { $js_controlling_field_id = '#conditional-' . conditional_fields_form_clean_id($controlling_field_name); $js_settings['controlling_fields'][$js_controlling_field_id][$js_controlled_field_id] = array('field_id' => $js_controlled_field_id, 'trigger_values' => $trigger_values); } } // Controlled fields should only be required when triggered. // Since required fields validation is hardcoded in _form_validate, // we need to unset the #required property and perform a custom validation. foreach ($required_fields as $field) { if ($field['in_group']) { conditional_fields_custom_required_field($form[$field['in_group']][$field['field']]); conditional_fields_item_apply_theme($form[$field['in_group']][$field['field']]); } else { conditional_fields_custom_required_field($form[$field['field']]); conditional_fields_item_apply_theme($form[$field['field']]); } } // Apply user interface settings $ui_settings = variable_get('c_fields_js_' . $type_name, C_FIELDS_JS_HIDE); switch ($ui_settings) { case C_FIELDS_JS_DISABLE: $js_settings['ui_settings'] = 'disable'; break; case C_FIELDS_JS_HIDE: $js_settings['ui_settings']['animation'] = (int)variable_get('c_fields_animation_' . $type_name, C_FIELDS_ANIMATION_NO); $js_settings['ui_settings']['anim_speed'] = variable_get('c_fields_anim_speed_' . $type_name, "normal"); break; } if ($ui_settings != C_FIELDS_JS_NO) { conditional_fields_add_js($js_settings); } // Pass variables for validation $form['#conditional_fields']['data'] = $data; $form['#conditional_fields']['required_fields'] = $required_fields; $form['#conditional_fields']['settings'] = $js_settings; // Add validation function $form['#validate'] = array_merge(array('conditional_fields_node_editing_form_validate'), (array)$form['#validate']); return $form; } /** * Insert appropriate themeing functions in a conditional field form element. */ function conditional_fields_item_apply_theme(&$element, $group_name = '') { if (!empty($group_name)) { $element['#group_id'] = 'conditional-' . conditional_fields_form_clean_id($group_name); // We add themeing in post_render so the wrapping is outside the fieldset. $element['#post_render'] = array_merge(array('conditional_fields_fieldgroup_post_render'), (array)$element['#post_render']); } else { // Save a previously set function so it can be called before rendering the field if ($element['#theme'] && $element['#theme'] != 'conditional_fields_form_item') { $element['#conditional_fields_theme'] = $element['#theme']; } $element['#theme'] = 'conditional_fields_form_item'; } } /** * Find an item in a form by key. If it's a CCK field, the function * will find it using field_info. */ function conditional_fields_item_in_form($form, $item_name, $group = FALSE) { static $items; if ($items[$item_name]) { return $items[$item_name]; } if ($group) { if ($form[$group][$item_name]) { $items[$item_name] = $form[$group][$item_name]; } elseif ($form[$group][$form['#field_info'][$item_name]['display_settings']['parent']][$item_name]) { $items[$item_name] = $form[$group][$form['#field_info'][$item_name]['display_settings']['parent']][$item_name]; } } else { if ($form[$item_name]) { $items[$item_name] = $form[$item_name]; } elseif ($form[$form['#field_info'][$item_name]['display_settings']['parent']][$item_name]) { $items[$item_name] = $form[$form['#field_info'][$item_name]['display_settings']['parent']][$item_name]; } } if ($items[$item_name]) { return $items[$item_name]; } return FALSE; } /** * Get the fieldgroup of a field */ function conditional_fields_get_group($type_name, $field_name) { if (!module_exists('fieldgroup')) { return FALSE; } return fieldgroup_get_group($type_name, $field_name); } /** * Validation for node editing form. */ function conditional_fields_node_editing_form_validate($form, &$form_state) { // When form fails validation, hook_form_alter is not called, so we add js here too if ($form['#conditional_fields']['settings']['ui_settings']) { conditional_fields_add_js($form['#conditional_fields']['settings']); } // Rebuild the array grouping the data by controlled fields // Needed to handle multiple controlling field per controlled field $controlled_fields = array(); foreach ($form['#conditional_fields']['data'] as $row) { $controlled_fields[$row['field_name']][$row['control_field_name']] = $row['trigger_values']; } foreach ($controlled_fields as $controlled_field_name => $controlling_fields) { // Check if all controlling field were triggered $triggered = FALSE; foreach ($controlling_fields as $controlling_field_name => $trigger_values) { $triggered = conditional_fields_is_triggered($form_state['values'][$controlling_field_name], $trigger_values); if ($triggered == FALSE) { break; } } $required_fields = $form['#conditional_fields']['required_fields']; if ($required_fields[$controlled_field_name]['in_group']) { $controlled_field = &$form[$required_fields[$controlled_field_name]['in_group']][$controlled_field_name]; } else { $controlled_field = &$form[$controlled_field_name]; } // Controlled field if (strpos($controlled_field_name , 'field_') === 0) { if ($triggered) { // Check required if (!empty($required_fields) && $required_fields[$controlled_field_name]) { $in_group = $required_fields[$controlled_field_name]['in_group']; // Check if the controlled field is empty if (conditional_fields_check_empty($form_state['values'][$controlled_field_name])) { // Check whether the controlled field is in a group or not and set error accordingly if (!$in_group) { form_error($controlled_field, t('!name field is required.', array('!name' => $controlled_field['#title']))); } else { // Set error only if the containing group is not controlled or is controlled and triggered $set_error = TRUE; foreach ($form['#conditional_fields']['data'] as $row_check_containing_group) { if ($row_check_containing_group['field_name'] == $in_group) { if (!conditional_fields_is_triggered($form_state['values'][$row_check_containing_group['control_field_name']], $row_check_containing_group['trigger_values'])) { $set_error = FALSE; } break; } } if ($set_error) { form_error($controlled_field, t('!name field is required.', array('!name' => $controlled_field['#title']))); } } } } } else { // Do not submit values of controlled fields which were not triggered (except on preview) if (variable_get('c_fields_reset_default_' . $form['type']['#value'], 1) && !in_array('node_form_build_preview', (array)$form_state['submit_handlers'])) { // Load default values like in content_field_form() in content.node_form.inc $controlled_field_info = $form['#field_info'][$controlled_field_name]; if (content_callback('widget', 'default value', $controlled_field_info) != CONTENT_CALLBACK_NONE) { $callback = content_callback('widget', 'default value', $controlled_field_info) == CONTENT_CALLBACK_CUSTOM ? $field['widget']['module'] .'_default_value' : 'content_default_value'; if (function_exists($callback)) { $items = $callback($form, $form_state, $controlled_field_info, 0); } } $form_state['values'][$controlled_field_name] = $items; } } } // Controlled group elseif (strpos($controlled_field_name , 'group_') === 0) { foreach (element_children($controlled_field) as $field_in_group) { // Check if the controlling field was triggered if ($triggered) { // Check required if (!empty($required_fields) && $required_fields[$field_in_group] && !$controlled_field[$field_in_group]['#controlled_fields'] && conditional_fields_check_empty($form_state['values'][$field_in_group])) { form_error($controlled_field[$field_in_group], t('!name field is required.', array('!name' => $controlled_field[$field_in_group]['#title']))); } } else { // Do not submit values of controlled fields which were not triggered (except on preview) if (variable_get('c_fields_reset_default_' . $form['type']['#value'], 1) && !in_array('node_form_build_preview', $form_state['submit_handlers'])) { // Load default values like in content_field_form() in content.node_form.inc $field_in_group_info = $form['#field_info'][$field_in_group]; if (content_callback('widget', 'default value', $field_in_group_info) != CONTENT_CALLBACK_NONE) { $callback = content_callback('widget', 'default value', $field_in_group_info) == CONTENT_CALLBACK_CUSTOM ? $field['widget']['module'] .'_default_value' : 'content_default_value'; if (function_exists($callback)) { $items = $callback($form, $form_state, $field_in_group_info, 0); } } $form_state['values'][$field_in_group] = $items; } } } } } } /** * Checks if a submitted field value is empty */ function conditional_fields_check_empty($field) { // Normal fields if (isset($field[0]['value'])) { $value = $field[0]['value']; } // Node reference elseif (isset($field[0]['nid'])) { $value = $field[0]['nid']; } if (!count($value) || (is_string($value) && drupal_strlen(trim($value)) == 0)) { return TRUE; } return FALSE; } /** * Returns true if the field was triggered * $selected_values The values of the controlling field selected by the user when creating the node * $trigger_values An array containing the information we need to select the trigger values */ function conditional_fields_is_triggered($selected_values, $trigger_values) { foreach ((array)$selected_values as $values) { foreach ((array)$values as $value) { if (isset($value) && in_array($value, $trigger_values)) { return TRUE; } } } return FALSE; } /** * Returns an array of conditional fields settings for a given node type. * $structure can be either 'flat' or 'row' . 'row' data is data per row, * while 'flat' data is a list of both controlling and controlled fields. */ function conditional_fields_load_data($type, $structure = 'row', $reset = FALSE) { static $data; if ($reset) { unset($data); } if (!isset($data[$structure][$type])) { $data[$structure][$type] = array(); if ($structure == 'row') { $query = db_query("SELECT control_field_name, field_name, trigger_values FROM {conditional_fields} WHERE type = '%s'", $type); while ($result = db_fetch_array($query)) { $result['trigger_values'] = unserialize($result['trigger_values']); $data['row'][$type][] = $result; } } elseif ($structure == 'flat') { $query = db_query("SELECT control_field_name, field_name FROM {conditional_fields} WHERE type = '%s'", $type); while ($result = db_fetch_array($query)) { $data['flat'][$type][$result['control_field_name']] = $result['control_field_name']; $data['flat'][$type][$result['field_name']] = $result['field_name']; } $data['flat'][$type] = array_unique($data['flat'][$type]); } } return $data[$structure][$type]; } /** * Find conditional fields and mark them. */ function conditional_fields_field_overview_form(&$form) { // Check if we have conditional data if (!$data = conditional_fields_load_data($form['#type_name'])) { return; } foreach ($data as $field) { if (in_array($field['control_field_name'], $form['#fields'])) { $form[$field['control_field_name']]['field_name']['#value'] .= theme('conditional_fields_manage_marker', NULL, $field['field_name']); } if (in_array($field['field_name'], $form['#fields'])) { $form[$field['field_name']]['field_name']['#value'] .= theme('conditional_fields_manage_marker', $field['control_field_name'], NULL); } // Mark groups if (in_array($field['field_name'], $form['#groups'])) { $form[$field['field_name']]['group_name']['#value'] .= theme('conditional_fields_manage_marker', $field['control_field_name'], NULL); } } // Add validation function $form['#validate'] = array_merge(array('conditional_fields_field_overview_form_validate'), $form['#validate']); } /** * Conditional fields can't change group. */ function conditional_fields_field_overview_form_validate($form, $form_state) { $data = conditional_fields_load_data($form['#type_name'], 'flat'); foreach ($form['#fields'] as $field_name) { if ($data[$field_name] && $form_state['values'][$field_name]['parent'] != $form_state['values'][$field_name]['prev_parent']) { form_set_error('', t("You can't change the parent group of a conditional field.")); } } } /** * Load default values from conditional_fields table. */ function conditional_fields_available_fields_default_values($control_field, $conditional_fields) { foreach ($conditional_fields as $field) { $query = db_query("SELECT trigger_values FROM {conditional_fields} WHERE control_field_name = '%s' AND field_name = '%s' AND type = '%s'", $field['field_name'], $control_field, $field['type_name']); $result = unserialize(db_result($query)); if ($result) { $default_values[$field['field_name']] = $result; } } return $default_values; } /** * Adds javascript to the node editing form */ function conditional_fields_add_js($settings) { // Avoid adding js twice when the node form has failed validation static $js; if (!$js) { drupal_add_js(array('ConditionalFields' => $settings), 'setting'); drupal_add_js(drupal_get_path('module', 'conditional_fields') . '/conditional_fields.js'); $js = TRUE; } } /** * post_render function to handle controlled fields after "add more" ahah request */ function conditional_fields_add_more_post_render($content, $item) { return theme('conditional_fields_wrapper', $content, 'conditional-' . conditional_fields_form_clean_id($item['#field_name']), array('conditional-field', ' controlled-field')); } /** * post_render function to make fieldgroup wrapping themeable */ function conditional_fields_fieldgroup_post_render($content, $item) { return theme('conditional_fields_wrapper', $content, $item['#group_id'], array('conditional-field', ' controlled-field')); } /* * Clean conditional fields settings pertaining to this removed field */ function _conditional_fields_content_admin_field_remove_submit($form, $form_state) { conditional_fields_remove_field_settings($form_state['field_name']); } /* * Clean conditional fields settings pertaining to this removed group */ function conditional_fields_fieldgroup_remove_group_submit($form, $form_state) { conditional_fields_remove_field_settings($form['#group_name']); } /* * Remove all settings for a field. * Since our field names are really the field instance name, it should be safe to remove without checking. */ function conditional_fields_remove_field_settings($field_name) { db_query("DELETE FROM {conditional_fields} WHERE control_field_name = '%s' OR field_name = '%s'", $field_name, $field_name); } /** * Implementation of hook_node_type(). */ function conditional_fields_node_type($op, $info) { switch ($op) { case 'update': conditional_fields_node_type_update($info); break; case 'delete': conditional_fields_node_type_delete($info->type); break; } } /** * Update conditional fields to a new type name */ function conditional_fields_node_type_update($info) { if (isset($info->old_type) && $info->type != $info->old_type) { db_query("UPDATE {conditional_fields} SET type = '%s' WHERE type ='%s'", $info->type, $info->old_type); // Update variables db_query("UPDATE {variable} SET name = 'c_fields_js_%s' WHERE name ='c_fields_js_%s'", $info->type, $info->old_type); db_query("UPDATE {variable} SET name = 'c_fields_animation_%s' WHERE name ='c_fields_animation_%s'", $info->type, $info->old_type); db_query("UPDATE {variable} SET name = 'c_fields_anim_speed_%s' WHERE name ='c_fields_anim_speed_%s'", $info->type, $info->old_type); db_query("UPDATE {variable} SET name = 'c_fields_reset_default_%s' WHERE name ='c_fields_reset_default_%s'", $info->type, $info->old_type); db_query("UPDATE {variable} SET name = 'c_fields_show_all_%s' WHERE name ='c_fields_show_all_%s'", $info->type, $info->old_type); db_query("UPDATE {variable} SET name = 'c_fields_view_%s' WHERE name ='c_fields_view_%s'", $info->type, $info->old_type); db_query("UPDATE {variable} SET name = 'c_fields_edit_%s' WHERE name ='c_fields_edit_%s'", $info->type, $info->old_type); cache_clear_all('variables', 'cache'); } } /** * Remove conditional fields of a node type */ function conditional_fields_node_type_delete($type) { db_query("DELETE FROM {conditional_fields} WHERE type = '%s'", $type); // Delete variables variable_del('c_fields_js_' . $type); variable_del('c_fields_animation_' . $type); variable_del('c_fields_anim_speed_' . $type); variable_del('c_fields_reset_default_' . $type); variable_del('c_fields_show_all_' . $type); variable_del('c_fields_view_' . $type); variable_del('c_fields_edit_' . $type); } /** * Implementation of hook_content_fieldapi(). */ function conditional_fields_content_fieldapi($ops, $field) { // Handle deletion of fields if ($ops == 'delete instance') { db_query("DELETE FROM {conditional_fields} WHERE type = '%s' AND (control_field_name = '%s' OR field_name = '%s')", $field['type_name'], $field['field_name'], $field['field_name']); } // Import conditional fields definitions with content_copy module elseif ($ops == 'create instance') { if ($field['conditional_fields']['available_fields']) { conditional_fields_save_field($field['type_name'], $field['field_name'], $field['conditional_fields']['available_fields']); } } } /** * Handle saving of individual controlled field settings * $controlling_fields is a keyed array of controlling fields names and trigger values * * Use this function with caution, as it doesn't check if the content type and the fields actually exist. */ function conditional_fields_save_field($type_name, $controlled_field, $controlling_fields) { foreach ($controlling_fields as $controlling_field => $trigger_values) { // If the row already exists if (db_result(db_query("SELECT COUNT(*) FROM {conditional_fields} WHERE control_field_name = '%s' AND field_name = '%s' AND type = '%s'", $controlling_field, $controlled_field, $type_name))) { // If no value is set, delete the entry, else update it if (empty($trigger_values) || $trigger_values['conditional_field_no_value']) { conditional_fields_delete_field($type_name, $controlled_field, $controlling_field); } else { conditional_fields_update_field($type_name, $controlled_field, $controlling_field, $trigger_values); } } else { // If values are set, create new entry if (!empty($trigger_values) && !$trigger_values['conditional_field_no_value']) { conditional_fields_insert_field($type_name, $controlled_field, $controlling_field, $trigger_values); } } } } /** * Insert into database the data of a new conditional field */ function conditional_fields_insert_field($type_name, $controlled_field, $controlling_field, $trigger_values) { db_query("INSERT INTO {conditional_fields} (control_field_name, field_name, type, trigger_values) VALUES ('%s', '%s', '%s', '%s')", $controlling_field, $controlled_field, $type_name, serialize($trigger_values)); } /** * Update an existing conditonal field's database data */ function conditional_fields_update_field($type_name, $controlled_field, $controlling_field, $trigger_values) { db_query("UPDATE {conditional_fields} SET trigger_values = '%s' WHERE control_field_name = '%s' AND field_name = '%s' AND type = '%s'", serialize($trigger_values), $controlling_field, $controlled_field, $type_name); } /** * Delete a conditonal field's database data */ function conditional_fields_delete_field($type_name, $controlled_field, $controlling_field) { db_query("DELETE FROM {conditional_fields} WHERE control_field_name = '%s' AND field_name = '%s' AND type = '%s'", $controlling_field, $controlled_field, $type_name); } /** * Wrapper for conditional_fields_set_required_field_recurse */ function conditional_fields_set_required_field($item) { conditional_fields_set_required_field_recurse($item); return $item; }; /** * Recursive function to set required for all conditionally required fields. * This causes Drupal to render conditionally required fields in a way that * indicates they are required when visible. */ function conditional_fields_set_required_field_recurse(&$item) { $item['#required'] = TRUE; foreach (element_children($item) as $child) { conditional_fields_set_required_field_recurse($item[$child]); } } /** * Unset the #required property and set a #conditional_fields_required property for custom validation. */ function conditional_fields_custom_required_field(&$field) { if ($field['#required']) { unset($field['#required']); $field['#conditional_fields_required'] = TRUE; } foreach (element_children($field) as $child) { conditional_fields_custom_required_field($field[$child]); } } /** * Implementation of hook_features_api() (features module). */ function conditional_fields_features_api() { return array( 'conditional_fields' => array( 'name' => t('Conditional Fields'), 'default_hook' => 'conditional_fields_default_fields', 'default_file' => FEATURES_DEFAULTS_INCLUDED, 'features_source' => TRUE, 'file' => drupal_get_path('module', 'conditional_fields') .'/includes/conditional_fields.features.inc', ), ); } /** * Implementation of hook_theme(). */ function conditional_fields_theme() { return array( 'conditional_fields_form_item' => array( 'arguments' => array(), ), 'conditional_fields_wrapper' => array( 'arguments' => array(), ), 'conditional_fields_manage_marker' => array( 'arguments' => array('controlling' => NULL, 'controlled' => NULL), ), ); } /** * Prepares conditional fields for rendering and handles * special cases. * Note that if you modify the id and classes of these fields, * you have to modify conditional_fields.js accordingly. */ function theme_conditional_fields_form_item($item) { if (isset($item['#conditional_fields_required']) && $item['#conditional_fields_required']) { $item = conditional_fields_set_required_field($item); } if ($item['#conditional_fields_theme']) { $rendered_item = theme($item['#conditional_fields_theme'], $item); } else { $rendered_item = drupal_render($item); } if (!$item['#controlling_fields'] && !$item['#controlled_fields']) { return $rendered_item; } $id = 'conditional-' . conditional_fields_form_clean_id($item['#field_name'] ? $item['#field_name'] : $item['#array_parents'][0]); $classes = array('conditional-field'); if ($item['#controlled_fields']) { $classes[] = 'controlled-field'; } if ($item['#controlling_fields']) { $classes[] = 'controlling-field'; } return theme('conditional_fields_wrapper', $rendered_item, $id, $classes); } /** * Themes a wrapper around conditional fields. * Note that if you modify the id and classes of these fields, * you have to modify conditional_fields.js accordingly. */ function theme_conditional_fields_wrapper($content, $id, $classes) { return '
' . $content . '
'; } /** * Themes a conditional field marker in 'Manage fields'. */ function theme_conditional_fields_manage_marker($controlling, $controlled) { $output = ''; if ($controlling) { $output .= '
' . t('Controlled by %controlling', array('%controlling' => $controlling)) . '
'; } if ($controlled) { $output .= '
' . t('Controlling %controlled', array('%controlled' => $controlled)) . '
'; } return $output; } /** * form_clean_id in Drupal 6 adds a unique ID check, which messes things up here * Since fields have unique names anyway, we can safely use this wrapper (I hope). */ function conditional_fields_form_clean_id($id) { $id = str_replace(array('][', '_', ' '), '-', $id); return $id; }