' . t('About') . ''; $output .= '

' . t('The field collection module provides a field, to which any number of fields can be attached. See the Field module help page for more information about fields.', array('@field-help' => url('admin/help/field'))) . '

'; return $output; } } /** * Implements hook_form_alter(). * * Checks for a value set by the embedded widget so fields are not displayed * with the 'all languages' hint incorrectly. */ function field_collection_form_alter(&$form, &$form_state) { if (!empty($form['#field_collection_translation_fields'])) { foreach ($form['#field_collection_translation_fields'] as $address) { drupal_array_set_nested_value($form, array_merge($address, array('#multilingual')), TRUE); } } } /** * Implements hook_form_FORM_ID_alter() for field_ui_field_overview_form(). * * Make the names of the field collection fields into links to edit the fields * for that field collection on the host's field edit page. */ function field_collection_form_field_ui_field_overview_form_alter(&$form, &$form_state) { if (count($form['#fields'])) { foreach($form['fields'] as $fieldname => $field) { if (!isset($field['type']['#title'])) { continue; } if ($field['type']['#title'] == 'Field collection') { $form['fields'][$fieldname]['field_name']['#markup'] = l($form['fields'][$fieldname]['field_name']['#markup'], 'admin/structure/field-collections/' . str_replace('_', '-', $fieldname) . '/fields'); } } } } /** * Implements hook_ctools_plugin_directory(). */ function field_collection_ctools_plugin_directory($module, $plugin) { if ($module == 'ctools') { return 'ctools/' . $plugin; } } /** * Implements hook_entity_info(). */ function field_collection_entity_info() { $return['field_collection_item'] = array( 'label' => t('Field collection item'), 'label callback' => 'entity_class_label', 'uri callback' => 'entity_class_uri', 'entity class' => 'FieldCollectionItemEntity', 'controller class' => 'EntityAPIController', 'base table' => 'field_collection_item', 'revision table' => 'field_collection_item_revision', 'fieldable' => TRUE, // For integration with Redirect module. // @see http://drupal.org/node/1263884 'redirect' => FALSE, 'entity keys' => array( 'id' => 'item_id', 'revision' => 'revision_id', 'bundle' => 'field_name', ), 'module' => 'field_collection', 'view modes' => array( 'full' => array( 'label' => t('Full content'), 'custom settings' => FALSE, ), ), 'access callback' => 'field_collection_item_access', 'deletion callback' => 'field_collection_item_delete', 'metadata controller class' => 'FieldCollectionItemMetadataController', 'translation' => array( 'entity_translation' => array( 'class' => 'EntityTranslationFieldCollectionItemHandler', ), ), ); // Add info about the bundles. We do not use field_info_fields() but directly // use field_read_fields() as field_info_fields() requires built entity info // to work. foreach (field_read_fields(array('type' => 'field_collection')) as $field_name => $field) { $return['field_collection_item']['bundles'][$field_name] = array( 'label' => t('Field collection @field', array('@field' => $field_name)), 'admin' => array( 'path' => 'admin/structure/field-collections/%field_collection_field_name', 'real path' => 'admin/structure/field-collections/' . strtr($field_name, array('_' => '-')), 'bundle argument' => 3, 'access arguments' => array('administer field collections'), ), ); $path = field_collection_field_get_path($field) . '/%field_collection_item'; // Enable the first available path scheme as default one. if (!isset($return['field_collection_item']['translation']['entity_translation']['base path'])) { $return['field_collection_item']['translation']['entity_translation']['base path'] = $path; $return['field_collection_item']['translation']['entity_translation']['path wildcard'] = '%field_collection_item'; $return['field_collection_item']['translation']['entity_translation']['default_scheme'] = $field_name; } else { $return['field_collection_item']['translation']['entity_translation']['path schemes'][$field_name] = array( 'base path' => $path, ); } } if (module_exists('entitycache')) { $return['field_collection_item']['field cache'] = FALSE; $return['field_collection_item']['entity cache'] = TRUE; } return $return; } /** * Provide the original entity language. * * If a language property is defined for the current entity we synchronize the * field value using the entity language, otherwise we fall back to * LANGUAGE_NONE. * * @param $entity_type * @param $entity * * @return * A language code */ function field_collection_entity_language($entity_type, $entity) { if (module_exists('entity_translation') && entity_translation_enabled($entity_type)) { $handler = entity_translation_get_handler($entity_type, $entity); $langcode = $handler->getLanguage(); } else { $langcode = entity_language($entity_type, $entity); } return !empty($langcode) ? $langcode : LANGUAGE_NONE; } /** * Menu callback for loading the bundle names. */ function field_collection_field_name_load($arg) { $field_name = strtr($arg, array('-' => '_')); if (($field = field_info_field($field_name)) && $field['type'] == 'field_collection') { return $field_name; } } /** * Loads a field collection item. * * @return field_collection_item * The field collection item entity or FALSE. */ function field_collection_item_load($item_id, $reset = FALSE) { $result = field_collection_item_load_multiple(array($item_id), array(), $reset); return $result ? reset($result) : FALSE; } /** * Loads a field collection revision. * * @param $revision_id * The field collection revision ID. */ function field_collection_item_revision_load($revision_id) { return entity_revision_load('field_collection_item', $revision_id); } /** * Loads field collection items. * * @return * An array of field collection item entities. */ function field_collection_item_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) { return entity_load('field_collection_item', $ids, $conditions, $reset); } /** * Implements hook_menu(). */ function field_collection_menu() { $items = array(); if (module_exists('field_ui')) { $items['admin/structure/field-collections'] = array( 'title' => 'Field collections', 'description' => 'Manage fields on field collections.', 'page callback' => 'field_collections_overview', 'access arguments' => array('administer field collections'), 'type' => MENU_NORMAL_ITEM, 'file' => 'field_collection.admin.inc', ); } // Add menu paths for viewing/editing/deleting field collection items. foreach (field_info_fields() as $field) { if ($field['type'] == 'field_collection') { $path = field_collection_field_get_path($field); $count = count(explode('/', $path)); $items[$path . '/%field_collection_item'] = array( 'page callback' => 'field_collection_item_page_view', 'page arguments' => array($count), 'access callback' => 'entity_access', 'access arguments' => array('view', 'field_collection_item', $count), 'file' => 'field_collection.pages.inc', ); $items[$path . '/%field_collection_item/view'] = array( 'title' => 'View', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items[$path . '/%field_collection_item/edit'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('field_collection_item_form', $count), 'access callback' => 'entity_access', 'access arguments' => array('update', 'field_collection_item', $count), 'title' => 'Edit', 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, 'file' => 'field_collection.pages.inc', ); $items[$path . '/%field_collection_item/delete'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('field_collection_item_delete_confirm', $count), 'access callback' => 'entity_access', 'access arguments' => array('delete', 'field_collection_item', $count), 'title' => 'Delete', 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_INLINE, 'file' => 'field_collection.pages.inc', ); // Add entity type and the entity id as additional arguments. $items[$path . '/add/%/%'] = array( 'page callback' => 'field_collection_item_add', 'page arguments' => array($field['field_name'], $count + 1, $count + 2), // The pace callback takes care of checking access itself. 'access callback' => TRUE, 'file' => 'field_collection.pages.inc', ); // Add menu items for dealing with revisions. $items[$path . '/%field_collection_item/revisions/%field_collection_item_revision'] = array( 'page callback' => 'field_collection_item_page_view', 'page arguments' => array($count + 2), 'access callback' => 'entity_access', 'access arguments' => array('view', 'field_collection_item', $count + 2), 'file' => 'field_collection.pages.inc', ); } } return $items; } /** * Implements hook_menu_alter() to fix the field collections admin UI tabs. */ function field_collection_menu_alter(&$items) { if (module_exists('field_ui') && isset($items['admin/structure/field-collections/%field_collection_field_name/fields'])) { // Make the fields task the default local task. $items['admin/structure/field-collections/%field_collection_field_name'] = $items['admin/structure/field-collections/%field_collection_field_name/fields']; $item = &$items['admin/structure/field-collections/%field_collection_field_name']; $item['type'] = MENU_NORMAL_ITEM; $item['title'] = 'Manage fields'; $item['title callback'] = 'field_collection_admin_page_title'; $item['title arguments'] = array(3); $items['admin/structure/field-collections/%field_collection_field_name/fields'] = array( 'title' => 'Manage fields', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => 1, ); } } /** * Menu title callback. */ function field_collection_admin_page_title($field_name) { return t('Field collection @field_name', array('@field_name' => $field_name)); } /** * Implements hook_admin_paths(). */ function field_collection_admin_paths() { if (variable_get('node_admin_theme')) { return array( 'field-collection/*/*/edit' => TRUE, 'field-collection/*/*/delete' => TRUE, 'field-collection/*/add/*/*' => TRUE, ); } } /** * Implements hook_permission(). */ function field_collection_permission() { return array( 'administer field collections' => array( 'title' => t('Administer field collections'), 'description' => t('Create and delete fields on field collections.'), ), ); } /** * Determines whether the given user has access to a field collection. * * @param $op * The operation being performed. One of 'view', 'update', 'create', 'delete'. * @param $item * Optionally a field collection item. If nothing is given, access for all * items is determined. * @param $account * The user to check for. Leave it to NULL to check for the global user. * @return boolean * Whether access is allowed or not. */ function field_collection_item_access($op, FieldCollectionItemEntity $item = NULL, $account = NULL) { // We do not support editing field collection revisions that are not used at // the hosts default revision as saving the host might result in a new default // revision. if (isset($item) && !$item->isInUse() && $op != 'view') { return FALSE; } if (user_access('administer field collections', $account)) { return TRUE; } if (!isset($item)) { return FALSE; } $op = $op == 'view' ? 'view' : 'edit'; // Access is determined by the entity and field containing the reference. $field = field_info_field($item->field_name); $entity_access = entity_access($op == 'view' ? 'view' : 'update', $item->hostEntityType(), $item->hostEntity(), $account); return $entity_access && field_access($op, $field, $item->hostEntityType(), $item->hostEntity(), $account); } /** * Deletion callback */ function field_collection_item_delete($id) { $fci = field_collection_item_load($id); if (!empty($fci)) { $fci->delete(); } } /** * Implements hook_theme(). */ function field_collection_theme() { return array( 'field_collection_item' => array( 'render element' => 'elements', 'template' => 'field-collection-item', ), 'field_collection_view' => array( 'render element' => 'element', ), ); } /** * Implements hook_field_info(). */ function field_collection_field_info() { return array( 'field_collection' => array( 'label' => t('Field collection'), 'description' => t('This field stores references to embedded entities, which itself may contain any number of fields.'), 'instance_settings' => array(), 'default_widget' => 'field_collection_hidden', 'default_formatter' => 'field_collection_view', // As of now there is no UI for setting the path. 'settings' => array( 'path' => '', 'hide_blank_items' => TRUE, 'hide_initial_item' => FALSE, ), // Add entity property info. 'property_type' => 'field_collection_item', 'property_callbacks' => array('field_collection_entity_metadata_property_callback'), ), ); } /** * Implements hook_field_instance_settings_form(). */ function field_collection_field_instance_settings_form($field, $instance) { $element['fieldset'] = array( '#type' => 'fieldset', '#title' => t('Default value'), '#collapsible' => FALSE, // As field_ui_default_value_widget() does, we change the #parents so that // the value below is writing to $instance in the right location. '#parents' => array('instance'), ); // Be sure to set the default value to NULL, e.g. to repair old fields // that still have one. $element['fieldset']['default_value'] = array( '#type' => 'value', '#value' => NULL, ); $element['fieldset']['content'] = array( '#pre' => '

', '#markup' => t('To specify a default value, configure it via the regular default value setting of each field that is part of the field collection. To do so, go to the Manage fields screen of the field collection.', array('!url' => url('admin/structure/field-collections/' . strtr($field['field_name'], array('_' => '-')) . '/fields'))), '#suffix' => '

', ); return $element; } /** * Returns the base path to use for field collection items. */ function field_collection_field_get_path($field) { if (empty($field['settings']['path'])) { return 'field-collection/' . strtr($field['field_name'], array('_' => '-')); } return $field['settings']['path']; } /** * Implements hook_field_settings_form(). */ function field_collection_field_settings_form($field, $instance) { $form['hide_blank_items'] = array( '#type' => 'checkbox', '#title' => t('Hide blank items'), '#default_value' => $field['settings']['hide_blank_items'], '#description' => t('Ordinarily a new blank item will be added to unlimited cardinality fields whenever they appear in a form. Checking this will prevent the blank item from appearing if the field already contains data.'), '#weight' => 10, '#states' => array( // Show the setting if the cardinality is -1. 'visible' => array( ':input[name="field[cardinality]"]' => array('value' => '-1'), ), ), ); $form['hide_initial_item'] = array( '#type' => 'checkbox', '#title' => t('Hide initial item'), '#default_value' => $field['settings']['hide_initial_item'], '#description' => t('Prevent the default blank item from appearing even if the field has no data yet. If checked, the user must explicitly add the field collection.'), '#weight' => 11, '#states' => array( // Show the setting if the cardinality is -1 and hide_blank_items is checked. 'visible' => array( ':input[name="field[cardinality]"]' => array('value' => '-1'), ':input[name="field[settings][hide_blank_items]"]' => array('checked' => TRUE), ), ), ); return $form; } /** * Implements hook_field_insert(). */ function field_collection_field_insert($host_entity_type, $host_entity, $field, $instance, $langcode, &$items) { foreach ($items as &$item) { if ($entity = field_collection_field_get_entity($item)) { if (!empty($host_entity->is_new) && empty($entity->is_new)) { // If the host entity is new but we have a field_collection that is not // new, it means that its host is being cloned. Thus we need to clone // the field collection entity as well. $new_entity = clone $entity; $new_entity->item_id = NULL; $new_entity->revision_id = NULL; $new_entity->is_new = TRUE; $entity = $new_entity; } if (!empty($entity->is_new)) { $entity->setHostEntity($host_entity_type, $host_entity, field_collection_entity_language($host_entity_type, $host_entity), FALSE); } $entity->save(TRUE); $item = array( 'value' => $entity->item_id, 'revision_id' => $entity->revision_id, ); } } } /** * Implements hook_field_update(). * * Care about removed field collection items. * * Support saving field collection items in @code $item['entity'] @endcode. This * may be used to seamlessly create field collection items during host-entity * creation or to save changes to the host entity and its collections at once. */ function field_collection_field_update($host_entity_type, $host_entity, $field, $instance, $langcode, &$items) { // When entity language is changed field values are moved to the new language // and old values are marked as removed. We need to avoid processing them in // this case. $entity_langcode = field_collection_entity_language($host_entity_type, $host_entity); $original = isset($host_entity->original) ? $host_entity->original : $host_entity; $original_langcode = field_collection_entity_language($host_entity_type, $original); $langcode = $langcode == $original_langcode ? $entity_langcode : $langcode; $top_host = $host_entity; while (method_exists($top_host, 'hostEntity')) { $top_host = $top_host->hostEntity(); } // Prevent workbench moderation from deleting field collections or paragraphs // on node_save() during workbench_moderation_store(), when // $host_entity->revision == 0. if (!empty($top_host->workbench_moderation['updating_live_revision'])) { return; } // Load items from the original entity. $items_original = !empty($original->{$field['field_name']}[$langcode]) ? $original->{$field['field_name']}[$langcode] : array(); $original_by_id = array_flip(field_collection_field_item_to_ids($items_original)); foreach ($items as $delta => &$item) { // In case the entity has been changed / created, save it and set the id. // If the host entity creates a new revision, save new item-revisions as // well. if (isset($item['entity']) || !empty($host_entity->revision)) { if ($entity = field_collection_field_get_entity($item)) { // If the host entity is saved as new revision, do the same for the item. if (!empty($host_entity->revision) || !empty($host_entity->is_new_revision)) { $entity->revision = TRUE; // Without this cache clear entity_revision_is_default will // incorrectly return false here when creating a new published revision if (!isset($cleared_host_entity_cache)) { list($entity_id) = entity_extract_ids($host_entity_type, $host_entity); entity_get_controller($host_entity_type)->resetCache(array($entity_id)); $cleared_host_entity_cache = true; } $is_default = entity_revision_is_default($host_entity_type, $host_entity); // If an entity type does not support saving non-default entities, // assume it will be saved as default. if (!isset($is_default) || $is_default) { $entity->default_revision = TRUE; $entity->archived = FALSE; } else { $entity->default_revision = FALSE; } } if (!empty($entity->is_new)) { $entity->setHostEntity($host_entity_type, $host_entity, $langcode, FALSE); } else { $entity->updateHostEntity($host_entity, $host_entity_type); } $entity->save(TRUE); $item = array( 'value' => $entity->item_id, 'revision_id' => $entity->revision_id, ); } } unset($original_by_id[$item['value']]); } // If there are removed items, care about deleting the item entities. if ($original_by_id) { $ids = array_flip($original_by_id); // If we are creating a new revision, the old-items should be kept but get // marked as archived now. if (!empty($host_entity->revision)) { db_update('field_collection_item') ->fields(array('archived' => 1)) ->condition('item_id', $ids, 'IN') ->execute(); } else { // Load items from the original entity from all languages checking which // are the unused items. $current_items = array(); $languages = language_list(); foreach ($languages as $langcode_value) { $current_items += !empty($host_entity->{$field['field_name']}[$langcode_value->language]) ? $host_entity->{$field['field_name']}[$langcode_value->language] : array(); $current_by_id = field_collection_field_item_to_ids($current_items); } $items_to_remove = array_diff($ids, $current_by_id); // Delete unused field collection items now. foreach (field_collection_item_load_multiple($items_to_remove) as $un_item) { $un_item->updateHostEntity($host_entity); $un_item->deleteRevision(TRUE); } } } } /** * Implements hook_field_delete(). */ function field_collection_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) { // Also delete all embedded entities. if ($ids = field_collection_field_item_to_ids($items)) { // We filter out entities that are still being referenced by other // host-entities. This should never be the case, but it might happened e.g. // when modules cloned a node without knowing about field-collection. $entity_info = entity_get_info($entity_type); $entity_id_name = $entity_info['entity keys']['id']; $field_column = key($field['columns']); foreach ($ids as $id_key => $id) { $query = new EntityFieldQuery(); $entities = $query ->fieldCondition($field['field_name'], $field_column, $id) ->execute(); unset($entities[$entity_type][$entity->$entity_id_name]); if (!empty($entities[$entity_type])) { // Filter this $id out. unset($ids[$id_key]); } // Set a flag to remember that the host entity is being deleted. See // FieldCollectionItemEntity::deleteHostEntityReference(). // Doing this on $entity is not sufficient because the cache for $entity // may have been reset since it was loaded. That would cause // hostEntity() to load it from the database later without the flag. $field_collection_item = field_collection_item_load($id); if ($field_collection_item) { $hostEntity = $field_collection_item->hostEntity(); if (!empty($hostEntity)) { $hostEntity->field_collection_deleting = TRUE; } } } entity_delete_multiple('field_collection_item', $ids); } } /** * Implements hook_field_delete_revision(). */ function field_collection_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) { foreach ($items as $item) { if (!empty($item['revision_id'])) { if ($entity = field_collection_item_revision_load($item['revision_id'])) { $entity->deleteRevision(TRUE); } } } } /** * Get an array of field collection item IDs stored in the given field items. */ function field_collection_field_item_to_ids($items) { $ids = array(); foreach ($items as $item) { if (!empty($item['value'])) { $ids[] = $item['value']; } } return $ids; } /** * Implements hook_field_is_empty(). */ function field_collection_field_is_empty($item, $field) { if (!empty($item['value'])) { return FALSE; } elseif (isset($item['entity'])) { return field_collection_item_is_empty($item['entity']); } return TRUE; } /** * Determines whether a field collection item entity is empty based on the collection-fields. */ function field_collection_item_is_empty(FieldCollectionItemEntity $item) { $instances = field_info_instances('field_collection_item', $item->field_name); $is_empty = TRUE; // Check whether all fields are booleans. $all_boolean = $instances && !(bool) array_filter($instances, '_field_collection_field_is_not_boolean'); foreach ($instances as $instance) { $field_name = $instance['field_name']; $field = field_info_field($field_name); // Boolean fields as those are always considered non-empty, thus their // information is not useful and can be skipped by default. if (!$all_boolean && $field['type'] == 'list_boolean') { continue; } // Determine the list of languages to iterate on. $languages = field_available_languages('field_collection_item', $field); foreach ($languages as $langcode) { if (!empty($item->{$field_name}[$langcode])) { // If at least one collection-field is not empty; the // field collection item is not empty. foreach ($item->{$field_name}[$langcode] as $field_item) { if (!module_invoke($field['module'], 'field_is_empty', $field_item, $field)) { $is_empty = FALSE; } } } } } // Allow other modules a chance to alter the value before returning. drupal_alter('field_collection_is_empty', $is_empty, $item); return $is_empty; } /** * Callback used by array_filter in field_collection_is_empty. */ function _field_collection_field_is_not_boolean($instance) { $field = field_info_field($instance['field_name']); return $field['type'] != 'list_boolean'; } /** * Implements hook_field_formatter_info(). */ function field_collection_field_formatter_info() { return array( 'field_collection_list' => array( 'label' => t('Links to field collection items'), 'field types' => array('field_collection'), 'settings' => array( 'edit' => t('Edit'), 'translate' => t('Translate'), 'delete' => t('Delete'), 'add' => t('Add'), 'description' => TRUE, ), ), 'field_collection_view' => array( 'label' => t('Field collection items'), 'field types' => array('field_collection'), 'settings' => array( 'edit' => t('Edit'), 'delete' => t('Delete'), 'add' => t('Add'), 'description' => TRUE, 'view_mode' => 'full', ), ), 'field_collection_fields' => array( 'label' => t('Fields only'), 'field types' => array('field_collection'), 'settings' => array( 'view_mode' => 'full', ), ), ); } /** * Implements hook_field_formatter_settings_form(). */ function field_collection_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) { $display = $instance['display'][$view_mode]; $settings = $display['settings']; $elements = array(); if ($display['type'] != 'field_collection_fields') { $elements['add'] = array( '#type' => 'textfield', '#title' => t('Add link title'), '#default_value' => $settings['add'], '#description' => t('Leave the title empty, to hide the link.'), ); $elements['edit'] = array( '#type' => 'textfield', '#title' => t('Edit link title'), '#default_value' => $settings['edit'], '#description' => t('Leave the title empty, to hide the link.'), ); $elements['translate'] = array( '#type' => 'textfield', '#title' => t('Translate link title'), '#default_value' => $settings['translate'], '#description' => t('Leave the title empty, to hide the link.'), '#access' => field_collection_item_is_translatable(), ); $elements['delete'] = array( '#type' => 'textfield', '#title' => t('Delete link title'), '#default_value' => $settings['delete'], '#description' => t('Leave the title empty, to hide the link.'), ); $elements['description'] = array( '#type' => 'checkbox', '#title' => t('Show the field description beside the add link.'), '#default_value' => $settings['description'], '#description' => t('If enabled and the add link is shown, the field description is shown in front of the add link.'), ); } // Add a select form element for view_mode if viewing the rendered field_collection. if ($display['type'] !== 'field_collection_list') { $entity_type = entity_get_info('field_collection_item'); $options = array(); foreach ($entity_type['view modes'] as $mode => $info) { $options[$mode] = $info['label']; } $elements['view_mode'] = array( '#type' => 'select', '#title' => t('View mode'), '#options' => $options, '#default_value' => $settings['view_mode'], '#description' => t('Select the view mode'), ); } return $elements; } /** * Implements hook_field_formatter_settings_summary(). */ function field_collection_field_formatter_settings_summary($field, $instance, $view_mode) { $display = $instance['display'][$view_mode]; $settings = $display['settings']; $output = array(); if ($display['type'] !== 'field_collection_fields') { $links = field_collection_get_operations($settings, TRUE); if ($links) { $output[] = t('Links: @links', array('@links' => check_plain(implode(', ', $links)))); } else { $output[] = t('Links: none'); } } if ($display['type'] !== 'field_collection_list') { $entity_type = entity_get_info('field_collection_item'); if (!empty($entity_type['view modes'][$settings['view_mode']]['label'])) { $output[] = t('View mode: @mode', array('@mode' => $entity_type['view modes'][$settings['view_mode']]['label'])); } } return implode('
', $output); } /** * Implements hook_field_formatter_view(). */ function field_collection_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { $element = array(); $settings = $display['settings']; switch ($display['type']) { case 'field_collection_list': foreach ($items as $delta => $item) { if ($field_collection = field_collection_field_get_entity($item)) { $output = l($field_collection->label(), $field_collection->path()); $links = array(); foreach (field_collection_get_operations($settings) as $op => $label) { if ($settings[$op] && entity_access($op == 'edit' ? 'update' : $op, 'field_collection_item', $field_collection)) { $title = entity_i18n_string("field:{$field['field_name']}:{$instance['bundle']}:setting_$op", $settings[$op]); $links[] = l($title, $field_collection->path() . '/' . $op, array('query' => drupal_get_destination())); } } if ($links) { $output .= ' (' . implode('|', $links) . ')'; } $element[$delta] = array('#markup' => $output); } } field_collection_field_formatter_links($element, $entity_type, $entity, $field, $instance, $langcode, $items, $display); break; case 'field_collection_view': $view_mode = !empty($display['settings']['view_mode']) ? $display['settings']['view_mode'] : 'full'; foreach ($items as $delta => $item) { if ($field_collection = field_collection_field_get_entity($item)) { $element[$delta]['entity'] = $field_collection->view($view_mode); $element[$delta]['#theme_wrappers'] = array('field_collection_view'); $element[$delta]['#attributes']['class'][] = 'field-collection-view'; $element[$delta]['#attributes']['class'][] = 'clearfix'; $element[$delta]['#attributes']['class'][] = drupal_clean_css_identifier('view-mode-' . $view_mode); $links = array( '#theme' => 'links__field_collection_view', ); $links['#attributes']['class'][] = 'field-collection-view-links'; foreach (field_collection_get_operations($settings) as $op => $label) { if ($settings[$op] && entity_access($op == 'edit' ? 'update' : $op, 'field_collection_item', $field_collection)) { $links['#links'][$op] = array( 'title' => entity_i18n_string("field:{$field['field_name']}:{$instance['bundle']}:setting_$op", $settings[$op]), 'href' => $field_collection->path() . '/' . $op, 'query' => drupal_get_destination(), ); } } $element[$delta]['links'] = $links; } } field_collection_field_formatter_links($element, $entity_type, $entity, $field, $instance, $langcode, $items, $display); if (!empty($items) || !empty($element['#suffix'])) { $element['#attached']['css'][] = drupal_get_path('module', 'field_collection') . '/field_collection.theme.css'; } break; case 'field_collection_fields': $view_mode = !empty($display['settings']['view_mode']) ? $display['settings']['view_mode'] : 'full'; foreach ($items as $delta => $item) { if ($field_collection = field_collection_field_get_entity($item)) { $element[$delta]['entity'] = $field_collection->view($view_mode); } } break; } return $element; } /** * Returns an array of enabled operations. */ function field_collection_get_operations($settings, $add = FALSE) { $operations = array(); if ($add) { $operations[] = 'add'; } $operations[] = 'edit'; if (field_collection_item_is_translatable()) { $operations[] = 'translate'; } $operations[] = 'delete'; global $field_collection_operation_keys; $field_collection_operation_keys = array_flip($operations); $operations = array_filter(array_intersect_key($settings, $field_collection_operation_keys)); asort($operations); return $operations; } /** * Helper function to add links to a field collection field. */ function field_collection_field_formatter_links(&$element, $entity_type, $entity, $field, $instance, $langcode, $items, $display) { $settings = $display['settings']; $allow_create_item = FALSE; if ($settings['add'] && ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || count($items) < $field['cardinality'])) { // Check whether the current is allowed to create a new item. $field_collection_item = entity_create('field_collection_item', array('field_name' => $field['field_name'])); $field_collection_item->setHostEntity($entity_type, $entity, $langcode, FALSE); if (entity_access('create', 'field_collection_item', $field_collection_item)) { $allow_create_item = TRUE; $path = field_collection_field_get_path($field); list($id) = entity_extract_ids($entity_type, $entity); $element['#suffix'] = ''; if (!empty($settings['description'])) { $element['#suffix'] .= '
' . field_filter_xss($instance['description']) . '
'; } $title = entity_i18n_string("field:{$field['field_name']}:{$instance['bundle']}:setting_add", $settings['add']); $add_path = $path . '/add/' . $entity_type . '/' . $id; $element['#suffix'] .= ''; } } // If there is no add link, add a special class to the last item. if (!empty($items) || $allow_create_item) { if (empty($element['#suffix'])) { $index = count(element_children($element)) - 1; $element[$index]['#attributes']['class'][] = 'field-collection-view-final'; } $element += array('#prefix' => '', '#suffix' => ''); $element['#prefix'] .= '
'; $element['#suffix'] .= '
'; } return $element; } /** * Themes field collection items printed using the field_collection_view formatter. */ function theme_field_collection_view($variables) { $element = $variables['element']; return '' . $element['#children'] . ''; } /** * Implements hook_field_widget_info(). */ function field_collection_field_widget_info() { return array( 'field_collection_hidden' => array( 'label' => t('Hidden'), 'field types' => array('field_collection'), 'behaviors' => array( 'multiple values' => FIELD_BEHAVIOR_CUSTOM, 'default value' => FIELD_BEHAVIOR_NONE, ), ), 'field_collection_embed' => array( 'label' => t('Embedded'), 'field types' => array('field_collection'), 'behaviors' => array( 'multiple values' => FIELD_BEHAVIOR_DEFAULT, 'default value' => FIELD_BEHAVIOR_NONE, ), ), ); } /** * Implements hook_field_widget_form(). */ function field_collection_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { static $recursion = 0; switch ($instance['widget']['type']) { case 'field_collection_hidden': return $element; case 'field_collection_embed': // If the field collection item form contains another field collection, // we might ran into a recursive loop. Prevent that. if ($recursion++ > 3) { drupal_set_message(t('The field collection item form has not been embedded to avoid recursive loops.'), 'error'); return $element; } $field_parents = $element['#field_parents']; $field_name = $element['#field_name']; $language = $element['#language']; // Nest the field collection item entity form in a dedicated parent space, // by appending [field_name, langcode, delta] to the current parent space. // That way the form values of the field collection item are separated. $parents = array_merge($field_parents, array($field_name, $language, $delta)); $element += array( '#element_validate' => array('field_collection_field_widget_embed_validate'), '#parents' => $parents, ); if ($field['cardinality'] == 1) { $element['#type'] = 'fieldset'; } $field_state = field_form_get_state($field_parents, $field_name, $language, $form_state); if (field_collection_hide_blank_items($field) && $delta == $field_state['items_count'] && $delta > 0) { // Do not add a blank item. Also see // field_collection_field_attach_form() for correcting #max_delta. $recursion--; return FALSE; } elseif (field_collection_hide_blank_items($field) && $field_state['items_count'] == 0) { // We show one item, so also specify that as item count. So when the // add button is pressed the item count will be 2 and we show two items. $field_state['items_count'] = 1; } if (isset($field_state['entity'][$delta])) { $field_collection_item = $field_state['entity'][$delta]; } else { if (isset($items[$delta])) { $field_collection_item = field_collection_field_get_entity($items[$delta], $field_name); } // Show an empty collection if we have no existing one or it does not // load. if (empty($field_collection_item)) { $field_collection_item = entity_create('field_collection_item', array('field_name' => $field_name)); $field_collection_item->setHostEntity($element['#entity_type'], $element['#entity'], $langcode); } // Put our entity in the form state, so FAPI callbacks can access it. $field_state['entity'][$delta] = $field_collection_item; } // Register a child entity translation handler to properly deal with the // entity form language. if (field_collection_item_is_translatable()) { $element['#host_entity_type'] = $element['#entity_type']; $element['#host_entity'] = $element['#entity']; // Give each field collection item a unique entity translation handler // ID, otherwise an infinite loop occurs when adding values to nested // field collection items. if (!isset($field_collection_item->entity_translation_handler_id)) { list($id, $revision_id) = entity_extract_ids('field_collection_item', $field_collection_item); $revision_id = isset($revision_id) ? $revision_id : 0; $field_collection_item->entity_translation_handler_id = 'field_collection_item' . '-' . (!empty($id) ? 'eid-' . $id . '-' . $revision_id : 'new-' . rand()); } $element['#field_collection_item'] = $field_collection_item; field_collection_add_child_translation_handler($element); // Ensure this is executed even with cached forms. This is mainly useful // when dealing with AJAX calls. $element['#process'][] = 'field_collection_add_child_translation_handler'; // Flag the field to be processed in field_collection_form_alter to // avoid adding incorrect translation hints. $address = array_slice($element['#parents'], 0, -2); if (empty($form['#field_collection_translation_fields']) || !in_array($address, $form['#field_collection_translation_fields'])) { $form['#field_collection_translation_fields'][] = $address; } } // Add the subform field_form_set_state($field_parents, $field_name, $language, $form_state, $field_state); // Set the language to to parent entity language, because // field_content_languages() will always set $language to LANGUAGE_NONE. if (field_collection_item_is_translatable()) { field_attach_form('field_collection_item', $field_collection_item, $element, $form_state, entity_language($element['#host_entity_type'], $element['#host_entity'])); } else { field_attach_form('field_collection_item', $field_collection_item, $element, $form_state, $language); } // Make sure subfields get translatable clues (like 'all languages') if (field_collection_item_is_translatable() && variable_get('entity_translation_shared_labels', TRUE)) { foreach (element_children($element) as $key) { $element[$key]['#process'][] = 'entity_translation_element_translatability_clue'; } } if (empty($element['#required'])) { $element['#after_build'][] = 'field_collection_field_widget_embed_delay_required_validation'; } if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED && empty($form_state['programmed'])) { $element['remove_button'] = array( '#delta' => $delta, '#name' => implode('_', $parents) . '_remove_button', '#type' => 'submit', '#value' => t('Remove'), '#validate' => array(), '#submit' => array('field_collection_remove_submit'), '#limit_validation_errors' => array(), '#ajax' => array( // 'wrapper' is filled in field_collection_field_attach_form(). 'callback' => 'field_collection_remove_js', 'effect' => 'fade', ), '#weight' => 1000, ); } $recursion--; return $element; } } /** * Implements hook_entity_translation_source_field_state_alter() */ function field_collection_entity_translation_source_field_state_alter(&$field_state) { if (isset($field_state['entity'])) { module_load_include('inc', 'entity', 'includes/entity.ui'); foreach ($field_state['entity'] as $delta => $entity) { if ($entity instanceof FieldCollectionItemEntity) { $field_state['entity'][$delta] = entity_ui_clone_entity('field_collection_item', $entity); } } } } /** * Registers a child entity translation handler for the given element. */ function field_collection_add_child_translation_handler($element) { $handler = entity_translation_get_handler($element['#host_entity_type'], $element['#host_entity']); $handler->addChild('field_collection_item', $element['#field_collection_item']); return $element; } /** * Implements hook_field_attach_form(). * * Corrects #max_delta when we hide the blank field collection item. * * @see field_add_more_js() * @see field_collection_field_widget_form() */ function field_collection_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) { foreach (field_info_instances($entity_type, $form['#bundle']) as $field_name => $instance) { $field = field_info_field($field_name); if ($field['type'] == 'field_collection' && field_collection_hide_blank_items($field) && field_access('edit', $field, $entity_type) && $instance['widget']['type'] == 'field_collection_embed') { $element_langcode = $form[$field_name]['#language']; if ($form[$field_name][$element_langcode]['#max_delta'] > 0) { $form[$field_name][$element_langcode]['#max_delta']--; } // Remove blank form elements and force user to explicitly add a field // collection if both 'hide_initial_item' and 'hide_blank_items' are TRUE. if ($field['settings']['hide_initial_item'] && $field['settings']['hide_blank_items'] && field_collection_item_is_empty($form[$field_name][$element_langcode][0]['#entity'])) { _field_collection_process_children_attached($form[$field_name][$element_langcode][0]); unset($form[$field_name][$element_langcode][0]); unset($form_state['field']['#parents'][$field_name][$element_langcode][0]); } } if ($field['type'] == 'field_collection' && $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED && empty($form_state['programmed']) && field_access('edit', $field, $entity_type) && $instance['widget']['type'] == 'field_collection_embed') { $element_langcode = $form[$field_name]['#language']; $element_wrapper = $form[$field_name][$element_langcode]['add_more']['#ajax']['wrapper']; for ($i = 0; $i <= $form[$field_name][$element_langcode]['#max_delta']; $i++) { if (isset($form[$field_name][$element_langcode][$i]['remove_button'])) { $form[$field_name][$element_langcode][$i]['remove_button']['#ajax']['wrapper'] = $element_wrapper; } } } } // If FCs are translatable, make sure we mark any necessary sub-fields in the // FC widget as translatable as well. if ($entity_type == 'field_collection_item' && field_collection_item_is_translatable() ) { foreach (field_info_instances($entity_type, $form['#bundle']) as $field_name => $instance) { $field = field_info_field($field_name); if (isset($field['translatable'])) { $form[$field_name]['#multilingual'] = (boolean) $field['translatable']; } } } } /** * Recurses through field children and processes thier attachments. */ function _field_collection_process_children_attached($elements) { if (empty($elements)) { return; } if (isset($elements['#attached'])) { drupal_process_attached($elements); } foreach (element_children($elements) as $key) { _field_collection_process_children_attached($elements[$key]); } } /** * AJAX callback for removing a field collection item. * * This returns the new page content to replace the page content made obsolete * by the form submission. * * @see field_collection_remove_submit() */ function field_collection_remove_js($form, $form_state) { $button = $form_state['triggering_element']; // Go one level up in the form, to the widgets container. $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -2)); $field_name = $element['#field_name']; $langcode = $element['#language']; $parents = $element['#field_parents']; $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state); $field = $field_state['field']; if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) { return; } return $element; } /** * Submit callback to remove an item from the field UI multiple wrapper. * * When a remove button is submitted, we need to find the item that it * referenced and delete it. Since field UI has the deltas as a straight * unbroken array key, we have to renumber everything down. Since we do this * we *also* need to move all the deltas around in the $form_state['values'], * $form_state['input'], and $form_state['field'] so that user changed values * follow. This is a bit of a complicated process. */ function field_collection_remove_submit($form, &$form_state) { $button = $form_state['triggering_element']; $delta = $button['#delta']; // Where in the form we'll find the parent element. $address = array_slice($button['#array_parents'], 0, -2); $values_address = array_slice($button['#parents'], 0, -2); // Go one level up in the form, to the widgets container. $parent_element = drupal_array_get_nested_value($form, $address); $field_name = $parent_element['#field_name']; $langcode = $parent_element['#language']; $parents = $parent_element['#field_parents']; $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state); // Use the actual array of field collection items as the upper limit of this // for loop rather than 'item_count'. This is because it will be creating extra // dummy items here and the two measures go out of sync after the fist delete. $field_collection_item_count = count($field_state['entity']) - 1; // Go ahead and renumber everything from our delta to the last // item down one. This will overwrite the item being removed. for ($i = $delta; $i <= $field_collection_item_count; $i++) { $old_element_address = array_merge($address, array($i + 1)); $old_element_values_address = array_merge($values_address, array($i + 1)); $new_element_values_address = array_merge($values_address, array($i)); $moving_element = drupal_array_get_nested_value($form, $old_element_address); $moving_element_value = drupal_array_get_nested_value($form_state['values'], $old_element_values_address); $moving_element_input = drupal_array_get_nested_value($form_state['input'], $old_element_values_address); $moving_element_field = drupal_array_get_nested_value($form_state['field']['#parents'], $old_element_address); // Tell the element where it's being moved to. $moving_element['#parents'] = $new_element_values_address; // Move the element around. form_set_value($moving_element, $moving_element_value, $form_state); drupal_array_set_nested_value($form_state['input'], $moving_element['#parents'], $moving_element_input); drupal_array_set_nested_value($form_state['field']['#parents'], $moving_element['#parents'], $moving_element_field); // Move the entity in our saved state. if (isset($field_state['entity'][$i + 1])) { $field_state['entity'][$i] = $field_state['entity'][$i + 1]; } else { unset($field_state['entity'][$i]); } } // Replace the deleted entity with an empty one. This helps to ensure that // trying to add a new entity won't ressurect a deleted entity from the // trash bin. $count = count($field_state['entity']); $field_state['entity'][$count] = entity_create('field_collection_item', array('field_name' => $field_name)); // Then remove the last item. But we must not go negative. if ($field_state['items_count'] > 0) { $field_state['items_count']--; } // Fix the weights. Field UI lets the weights be in a range of // (-1 * item_count) to (item_count). This means that when we remove one, // the range shrinks; weights outside of that range then get set to // the first item in the select by the browser, floating them to the top. // We use a brute force method because we lost weights on both ends // and if the user has moved things around, we have to cascade because // if I have items weight weights 3 and 4, and I change 4 to 3 but leave // the 3, the order of the two 3s now is undefined and may not match what // the user had selected. $input = drupal_array_get_nested_value($form_state['input'], $values_address); // Sort by weight uasort($input, '_field_sort_items_helper'); // Reweight everything in the correct order. $weight = -1 * $field_state['items_count']; foreach ($input as $key => $item) { if ($item) { $input[$key]['_weight'] = $weight++; } } drupal_array_set_nested_value($form_state['input'], $values_address, $input); field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state); $form_state['rebuild'] = TRUE; } /** * Gets a field collection item entity for a given field item. * * @param $field_name * (optional) If given and there is no entity yet, a new entity object is * created for the given item. * * @return * The entity object or FALSE. */ function field_collection_field_get_entity(&$item, $field_name = NULL) { if (isset($item['entity'])) { return $item['entity']; } elseif (isset($item['value'])) { // By default always load the default revision, so caches get used. $entity = field_collection_item_load($item['value']); if ($entity && $entity->revision_id != $item['revision_id']) { // A non-default revision is a referenced, so load this one. $entity = field_collection_item_revision_load($item['revision_id']); } return $entity; } elseif (!isset($item['entity']) && isset($field_name)) { $item['entity'] = entity_create('field_collection_item', array('field_name' => $field_name)); return $item['entity']; } return FALSE; } /** * FAPI #after_build of an individual field collection element to delay the validation of #required. */ function field_collection_field_widget_embed_delay_required_validation(&$element, &$form_state) { // If the process_input flag is set, the form and its input is going to be // validated. Prevent #required (sub)fields from throwing errors while // their non-#required field collection item is empty. if ($form_state['process_input']) { _field_collection_collect_required_elements($element, $element['#field_collection_required_elements']); } return $element; } function _field_collection_collect_required_elements(&$element, &$required_elements) { // Recurse through all children. foreach (element_children($element) as $key) { if (isset($element[$key]) && $element[$key]) { _field_collection_collect_required_elements($element[$key], $required_elements); } } if (!empty($element['#required'])) { $element['#required'] = FALSE; $required_elements[] = &$element; $element += array('#pre_render' => array()); array_unshift($element['#pre_render'], 'field_collection_field_widget_render_required'); } } /** * #pre_render callback that ensures the element is rendered as being required. */ function field_collection_field_widget_render_required($element) { $element['#required'] = TRUE; return $element; } /** * FAPI validation of an individual field collection element. */ function field_collection_field_widget_embed_validate($element, &$form_state, $complete_form) { $field = field_widget_field($element, $form_state); $field_parents = $element['#field_parents']; $field_name = $element['#field_name']; $language = $element['#language']; $field_state = field_form_get_state($field_parents, $field_name, $language, $form_state); // We have to populate the field_collection_item before we can attach it to // the form. if (isset($field_state['entity'][$element['#delta']])) { $field_collection_item = $field_state['entity'][$element['#delta']]; } else { $field_values = drupal_array_get_nested_value($form_state['values'], $field_state['array_parents']); if ($field_values[$element['#delta']]) { $field_collection_item = entity_create('field_collection_item', array('field_name' => $field['field_name'])); foreach ($field_values[$element['#delta']] as $key => $value) { if (property_exists($field_collection_item, $key)) { $field_collection_item->{$key} = $value; } } } } // Attach field API validation of the embedded form. field_attach_form_validate('field_collection_item', $field_collection_item, $element, $form_state); // Handle a possible language change. if (field_collection_item_is_translatable()) { $handler = entity_translation_get_handler('field_collection_item', $field_collection_item); $element_values = &drupal_array_get_nested_value($form_state['values'], $field_state['array_parents']); $element_form_state = array('values' => &$element_values[$element['#delta']]); $handler->entityFormLanguageWidgetSubmit($element, $element_form_state); } // Now validate required elements if the entity is not empty. if (!field_collection_item_is_empty($field_collection_item) && !empty($element['#field_collection_required_elements'])) { foreach ($element['#field_collection_required_elements'] as &$elements) { // Copied from _form_validate(). if (isset($elements['#needs_validation'])) { $is_empty_multiple = (!count($elements['#value'])); $is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0); $is_empty_value = ($elements['#value'] === 0); $is_empty_option = (isset($elements['#options']['_none']) && $elements['#value'] == '_none'); // Validate fields with hook_field_is_empty. This will handle cases when // file entity passes validation when it shouldn't. $is_empty_field = FALSE; if (isset($elements['#field_name'])) { $field = field_info_field($elements['#field_name']); // Extract field values array with all columns from form_state. // The field we're looking at is always 3 levels deeper than field // collection field. $field_depth = count($elements['#field_parents']) + 3; $field_values = drupal_array_get_nested_value($form_state['values'], array_slice($elements['#array_parents'], 0, $field_depth)); // Special case lists since we don't get the correct array_parents. if (count($elements['#array_parents']) < $field_depth && is_array($field_values)) { $field_values = reset($field_values); } $is_empty_field = module_invoke($field['module'], 'field_is_empty', $field_values, $field); } if ($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_option || $is_empty_field) { if (isset($elements['#title'])) { form_error($elements, t('@name field is required in the @collection collection.', array( '@name' => $elements['#title'], '@collection' => $field_state['instance']['label'], ))); } else { form_error($elements); } } } } } // Only if the form is being submitted, finish the collection entity and // prepare it for saving. if ($form_state['submitted'] && !form_get_errors()) { field_attach_submit('field_collection_item', $field_collection_item, $element, $form_state); // Load initial form values into $item, so any other form values below the // same parents are kept. $item = drupal_array_get_nested_value($form_state['values'], $element['#parents']); // Set the _weight if it is a multiple field. if (isset($element['_weight']) && ($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED)) { $item['_weight'] = $element['_weight']['#value']; } // Ensure field columns are poroperly populated. $item['value'] = $field_collection_item->item_id; $item['revision_id'] = $field_collection_item->revision_id; // Put the field collection item in $item['entity'], so it is saved with // the host entity via hook_field_presave() / field API if it is not empty. // @see field_collection_field_presave() $item['entity'] = $field_collection_item; form_set_value($element, $item, $form_state); } } /** * Implements hook_field_create_field(). */ function field_collection_field_create_field($field) { if ($field['type'] == 'field_collection') { field_attach_create_bundle('field_collection_item', $field['field_name']); // Clear caches. entity_info_cache_clear(); // Do not directly issue menu rebuilds here to avoid potentially multiple // rebuilds. Instead, let menu_get_item() issue the rebuild on the next // request. variable_set('menu_rebuild_needed', TRUE); } } /** * Implements hook_field_delete_field(). */ function field_collection_field_delete_field($field) { if ($field['type'] == 'field_collection') { // Notify field.module that field collection was deleted. field_attach_delete_bundle('field_collection_item', $field['field_name']); // Clear caches. entity_info_cache_clear(); // Do not directly issue menu rebuilds here to avoid potentially multiple // rebuilds. Instead, let menu_get_item() issue the rebuild on the next // request. variable_set('menu_rebuild_needed', TRUE); } } /** * Implements hook_i18n_string_list_{textgroup}_alter(). */ function field_collection_i18n_string_list_field_alter(&$properties, $type, $instance) { if ($type == 'field_instance') { $field = field_info_field($instance['field_name']); if ($field['type'] == 'field_collection' && !empty($instance['display'])) { foreach ($instance['display'] as $view_mode => $display) { if ($display['type'] != 'field_collection_fields') { $display['settings'] += array('edit' => 'edit', 'translate' => 'translate', 'delete' => 'delete', 'add' => 'add'); $properties['field'][$instance['field_name']][$instance['bundle']]['setting_edit'] = array( 'title' => t('Edit link title'), 'string' => $display['settings']['edit'], ); $properties['field'][$instance['field_name']][$instance['bundle']]['setting_translate'] = array( 'title' => t('Edit translate title'), 'string' => $display['settings']['translate'], ); $properties['field'][$instance['field_name']][$instance['bundle']]['setting_delete'] = array( 'title' => t('Delete link title'), 'string' => $display['settings']['delete'], ); $properties['field'][$instance['field_name']][$instance['bundle']]['setting_add'] = array( 'title' => t('Add link title'), 'string' => $display['settings']['add'], ); } } } } } /** * Implements hook_views_api(). */ function field_collection_views_api() { return array( 'api' => '3.0-alpha1', 'path' => drupal_get_path('module', 'field_collection') . '/views', ); } /** * Implements hook_features_pipe_COMPONENT_alter() for field objects. * * This is used with Features v1.0 and v2.0 prior to beta2, newer releases * separated the field_base from the field_instance so this won't be used. * * @see field_collection_features_pipe_field_instance_alter(). */ function field_collection_features_pipe_field_alter(&$pipe, $data, $export) { // Skip this if Features has been updated to v2.0-beta2 or newer as it will // use the separate field_instance integration instead. if (!function_exists('field_instance_features_export_options')) { // Add the fields of the field collection entity to the pipe. foreach ($data as $identifier) { if (($field = features_field_load($identifier)) && $field['field_config']['type'] == 'field_collection') { $fields = field_info_instances('field_collection_item', $field['field_config']['field_name']); foreach ($fields as $name => $field) { $pipe['field'][] = "{$field['entity_type']}-{$field['bundle']}-{$field['field_name']}"; } } } } } /** * Implements hook_features_pipe_COMPONENT_alter() for field_instance objects. * * This is used with Features v2.0-beta2 and newer. */ function field_collection_features_pipe_field_instance_alter(&$pipe, $data, $export) { // Add the fields of the field collection entity to the pipe. foreach ($data as $identifier) { if (($field = features_field_load($identifier)) && $field['field_config']['type'] == 'field_collection') { $fields = field_info_instances('field_collection_item', $field['field_config']['field_name']); foreach ($fields as $name => $field) { $pipe['field_instance'][] = "{$field['entity_type']}-{$field['bundle']}-{$field['field_name']}"; } } } } /** * Callback for generating entity metadata property info for our field instances. * * @see field_collection_field_info() */ function field_collection_entity_metadata_property_callback(&$info, $entity_type, $field, $instance, $field_type) { $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']]; // Set the bundle as we know it is the name of the field. $property['bundle'] = $field['field_name']; $property['getter callback'] = 'field_collection_field_property_get'; $property['setter callback'] = 'field_collection_field_property_set'; } /** * Entity property info setter callback for the host entity property. * * As the property is of type entity, the value will be passed as a wrapped * entity. */ function field_collection_item_set_host_entity($item, $property_name, $wrapper) { if (empty($item->is_new)) { throw new EntityMetadataWrapperException('The host entity may be set only during creation of a field collection item.'); } if (!isset($wrapper->{$item->field_name})) { throw new EntityMetadataWrapperException('The specified entity has no such field collection field.'); } $entity_type = $wrapper->type(); $field = field_info_field($item->field_name); $langcode = field_is_translatable($entity_type, $field) ? field_collection_entity_language($entity_type, $wrapper->value()) : LANGUAGE_NONE; $item->setHostEntity($wrapper->type(), $wrapper->value(), $langcode); } /** * Entity property info getter callback for the host entity property. */ function field_collection_item_get_host_entity($item) { // As the property is defined as 'entity', we have to return a wrapped entity. return entity_metadata_wrapper($item->hostEntityType(), $item->hostEntity()); } /** * Entity property info getter callback for the field collection items. * * Like entity_metadata_field_property_get(), but additionally supports getting * not-yet saved collection items from @code $item['entity'] @endcode. */ function field_collection_field_property_get($entity, array $options, $name, $entity_type, $info) { $field = field_info_field($name); $langcode = field_language($entity_type, $entity, $name, isset($options['language']) ? $options['language']->language : NULL); $values = array(); if (isset($entity->{$name}[$langcode])) { foreach ($entity->{$name}[$langcode] as $delta => $data) { // Wrappers do not support multiple entity references being revisions or // not yet saved entities. In the case of a single reference we can return // the entity object though. if ($field['cardinality'] == 1) { $values[$delta] = field_collection_field_get_entity($data); } elseif (isset($data['value'])) { $values[$delta] = $data['value']; } } } // For an empty single-valued field, we have to return NULL. return $field['cardinality'] == 1 ? ($values ? reset($values) : NULL) : $values; } /** * Entity property info setter callback for the field collection items. * * Like entity_metadata_field_property_set(), but additionally supports * saving the revision id. */ function field_collection_field_property_set($entity, $name, $value, $langcode, $entity_type) { $field = field_info_field($name); $columns = array_keys($field['columns']); $langcode = entity_metadata_field_get_language($entity_type, $entity, $field, $langcode); $values = $field['cardinality'] == 1 ? array($value) : (array) $value; $items = array(); foreach ($values as $delta => $value) { if (isset($value)) { if ($value instanceof FieldCollectionItemEntity) { $items[$delta][$columns[0]] = $value->item_id; $items[$delta][$columns[1]] = $value->revision_id; } elseif (is_array($value) && isset($value['value']) && isset($value['revision_id'])) { $items[$delta][$columns[0]] = $value['value']; $items[$delta][$columns[1]] = $value['revision_id']; } else { $item = field_collection_item_load($value); $items[$delta][$columns[0]] = $item->item_id; $items[$delta][$columns[1]] = $item->revision_id; } } } $entity->{$name}[$langcode] = $items; // Empty the static field language cache, so the field system picks up any // possible new languages. drupal_static_reset('field_language'); } /** * Implements hook_devel_generate(). */ function field_collection_devel_generate($object, $field, $instance, $bundle) { // Create a new field collection object and add fake data to its fields. $field_collection = entity_create('field_collection_item', array('field_name' => $field['field_name'])); $field_collection->language = $object->language; $field_collection->setHostEntity($instance['entity_type'], $object, $object->language, FALSE); devel_generate_fields($field_collection, 'field_collection_item', $field['field_name']); $field_collection->save(TRUE); return array( 'value' => $field_collection->item_id, 'revision_id' => $field_collection->revision_id, ); } /** * Determine if field collection items can be translated. * * @return * Boolean indicating whether field collection items can be translated. */ function field_collection_item_is_translatable() { return (bool) module_invoke('entity_translation', 'enabled', 'field_collection_item'); } /** * Implements hook_entity_translation_delete(). */ function field_collection_entity_translation_delete($entity_type, $entity, $langcode) { if (field_collection_item_is_translatable()) { list(, , $bundle) = entity_extract_ids($entity_type, $entity); foreach (field_info_instances($entity_type, $bundle) as $instance) { $field_name = $instance['field_name']; $field = field_info_field($field_name); if ($field['type'] == 'field_collection') { $field_langcode = field_is_translatable($entity_type, $field) ? $langcode : LANGUAGE_NONE; if (!empty($entity->{$field_name}[$field_langcode])) { foreach ($entity->{$field_name}[$field_langcode] as $delta => $item) { $field_collection_item = field_collection_field_get_entity($item); $handler = entity_translation_get_handler('field_collection_item', $field_collection_item); $translations = $handler->getTranslations(); if (isset($translations->data[$langcode])) { $handler->removeTranslation($langcode); $field_collection_item->save(TRUE); } } } } } } } /** * Determines if the additional blank items should be displayed or not. * * @param array $field * The field info array. * * @return bool * TRUE if the additional blank items should be hidden, and FALSE if not. */ function field_collection_hide_blank_items($field) { return !empty($field['settings']['hide_blank_items']) && $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED; } /** * Implements hook_admin_menu_map(). */ function field_collection_admin_menu_map() { if (user_access('administer field collections')) { $map['admin/structure/field-collections/%field_collection_field_name'] = array( 'parent' => 'admin/structure/field-collections', 'arguments' => array( array('%field_collection_field_name' => array_keys(field_read_fields(array('type' => 'field_collection')))), ), ); return $map; } } /** * implements hook_entity_translation_insert */ function field_collection_entity_translation_insert($entity_type, $entity, $translation, $values = array()) { // Check if some of the values inserted are of a field_collection field if (!empty($values)) { foreach ($values as $field_name => $value) { $field = field_info_field($field_name); if ($field['type'] == 'field_collection') { // We have found a field collection $language = $translation['language']; $source_language = $translation['source']; if (!empty($value[$language])) { $source_items = !empty($entity->{$field_name}[$source_language]) ? field_collection_field_item_to_ids($entity->{$field_name}[$source_language]) : array(); foreach ($value[$language] as $delta => $field_value) { if (!isset($field_value['entity'])) { if ($fc_entity = field_collection_field_get_entity($field_value)) { // Check if this field collection item belongs to the source language if (in_array($fc_entity->item_id, $source_items)) { // Clone the field collection item $new_fc_entity = clone $fc_entity; $new_fc_entity->item_id = NULL; $new_fc_entity->revision_id = NULL; $new_fc_entity->is_new = TRUE; // Set the new entity for saving it later $entity->{$field_name}[$language][$delta]['entity'] = $new_fc_entity; } } } } } } } } }