diff --git a/MultifieldViewsHandler.php b/MultifieldViewsHandler.php
new file mode 100644
index 0000000..cdcb4d0
--- /dev/null
+++ b/MultifieldViewsHandler.php
@@ -0,0 +1,909 @@
+base_table.
+ *
+ * @var string
+ */
+ public $base_table;
+
+ /**
+ * Store the field instance.
+ *
+ * @var array
+ */
+ public $instance;
+
+ public $fieldHandler;
+
+ private $init_options;
+
+ public function construct() {
+ parent::construct();
+ $definition = $this->definition;
+ $definition['handler'] = $definition['subfield handler'];
+ unset($definition['subfield handler']);
+ $definition['field_name'] = $definition['subfield_name'];
+ unset($definition['subfield_name']);
+ $this->fieldHandler = _views_create_handler($definition, 'handler', 'field');
+ if ($this->view && $this->init_options) {
+ $this->fieldHandler->init($this->view, $this->init_options);
+ }
+ }
+
+ function init(&$view, &$options) {
+ $this->init_options = $options;
+ parent::init($view, $options);
+ $this->multifield_info = $field = field_info_field($this->definition['field_name']);
+ $this->field_info = $subfield = field_info_field($this->definition['subfield_name']);
+ $this->multiple = FALSE;
+ $this->limit_values = FALSE;
+
+ if ($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) {
+ $this->multiple = TRUE;
+
+ // If "Display all values in the same row" is FALSE, then we always limit
+ // in order to show a single unique value per row.
+ if (!$this->options['group_rows']) {
+ $this->limit_values = TRUE;
+ }
+
+ // If "First and last only" is chosen, limit the values
+ if (!empty($this->options['delta_first_last'])) {
+ $this->limit_values = TRUE;
+ }
+
+ // Otherwise, we only limit values if the user hasn't selected "all", 0, or
+ // the value matching field cardinality.
+ if ((intval($this->options['delta_limit']) && ($this->options['delta_limit'] != $field['cardinality'])) || intval($this->options['delta_offset'])) {
+ $this->limit_values = TRUE;
+ }
+ }
+
+ // Convert old style entity id group column to new format.
+ // @todo Remove for next major version.
+ if ($this->options['group_column'] == 'entity id') {
+ $this->options['group_column'] = 'entity_id';
+ }
+ if ($this->fieldHandler) {
+ $this->fieldHandler->init($view, $options);
+ }
+ }
+
+ /**
+ * Check whether current user has access to this handler.
+ *
+ * @return bool
+ * Return TRUE if the user has access to view this field.
+ */
+ /*function access() {
+ $base_table = $this->get_base_table();
+ return field_access('view', $this->field_info, $this->definition['entity_tables'][$base_table]);
+ }*/
+
+ /**
+ * Set the base_table and base_table_alias.
+ *
+ * @return string
+ * The base table which is used in the current view "context".
+ */
+ function get_base_table() {
+ if (!isset($this->base_table)) {
+ // This base_table is coming from the entity not the field.
+ $this->base_table = $this->view->base_table;
+
+ // If the current field is under a relationship you can't be sure that the
+ // base table of the view is the base table of the current field.
+ // For example a field from a node author on a node view does have users as base table.
+ if (!empty($this->options['relationship']) && $this->options['relationship'] != 'none') {
+ $relationships = $this->view->display_handler->get_option('relationships');
+ if (!empty($relationships[$this->options['relationship']])) {
+ $options = $relationships[$this->options['relationship']];
+ $data = views_fetch_data($options['table']);
+ $this->base_table = $data[$options['field']]['relationship']['base'];
+ }
+ }
+ }
+
+ return $this->base_table;
+ }
+
+ /**
+ * Called to add the field to a query.
+ *
+ * By default, the only columns added to the query are entity_id and
+ * entity_type. This is because other needed data is fetched by entity_load().
+ * Other columns are added only if they are used in groupings, or if
+ * 'add fields to query' is specifically set to TRUE in the field definition.
+ *
+ * The 'add fields to query' switch is used by modules which need all data
+ * present in the query itself (such as "sphinx").
+ */
+ function query($use_groupby = FALSE) {
+ $this->get_base_table();
+
+ $params = array();
+ if ($use_groupby) {
+ // When grouping on a "field API" field (whose "real_field" is set to
+ // entity_id), retrieve the minimum entity_id to have a valid entity_id to
+ // pass to field_view_field().
+ $params = array(
+ 'function' => 'min',
+ );
+
+ $this->ensure_my_table();
+ }
+
+ // Get the entity type according to the base table of the field.
+ // Then add it to the query as a formula. That way we can avoid joining
+ // the field table if all we need is entity_id and entity_type.
+ $entity_type = $this->definition['entity_tables'][$this->base_table];
+ $entity_info = entity_get_info($entity_type);
+
+ if (isset($this->relationship)) {
+ $this->base_table_alias = $this->relationship;
+ }
+ else {
+ $this->base_table_alias = $this->base_table;
+ }
+
+ // We always need the base field (entity_id / revision_id).
+ if (empty($this->definition['is revision'])) {
+ $this->field_alias = $this->query->add_field($this->base_table_alias, $entity_info['entity keys']['id'], '', $params);
+ }
+ else {
+ $this->field_alias = $this->query->add_field($this->base_table_alias, $entity_info['entity keys']['revision'], '', $params);
+ $this->aliases['entity_id'] = $this->query->add_field($this->base_table_alias, $entity_info['entity keys']['id'], '', $params);
+ }
+
+
+ // The alias needs to be unique, so we use both the field table and the entity type.
+ $entity_type_alias = $this->definition['table'] . '_' . $entity_type . '_entity_type';
+ $this->aliases['entity_type'] = $this->query->add_field(NULL, "'$entity_type'", $entity_type_alias);
+
+ $fields = $this->additional_fields;
+ // We've already added entity_type, so we can remove it from the list.
+ $entity_type_key = array_search('entity_type', $fields);
+ if ($entity_type_key !== FALSE) {
+ unset($fields[$entity_type_key]);
+ }
+
+ if ($use_groupby) {
+ // Add the fields that we're actually grouping on.
+ $options = array();
+
+ if ($this->options['group_column'] != 'entity_id') {
+ $options = array($this->options['group_column'] => $this->options['group_column']);
+ }
+
+ $options += is_array($this->options['group_columns']) ? $this->options['group_columns'] : array();
+
+
+ $fields = array();
+ $rkey = $this->definition['is revision'] ? 'FIELD_LOAD_REVISION' : 'FIELD_LOAD_CURRENT';
+ // Go through the list and determine the actual column name from field api.
+ foreach ($options as $column) {
+ $name = $column;
+ if (isset($this->multifield_info['storage']['details']['sql'][$rkey][$this->table][$column])) {
+ $name = $this->multifield_info['storage']['details']['sql'][$rkey][$this->table][$column];
+ }
+
+ $fields[$column] = $name;
+ }
+
+ $this->group_fields = $fields;
+ }
+
+ // Add additional fields (and the table join itself) if needed.
+ if ($this->add_field_table($use_groupby)) {
+ $this->ensure_my_table();
+ $this->add_additional_fields($fields);
+
+ // Filter by language, if field translation is enabled.
+ $field = $this->multifield_info;
+ if (field_is_translatable($entity_type, $field) && !empty($this->view->display_handler->options['field_language_add_to_query'])) {
+ $column = $this->table_alias . '.language';
+ // By the same reason as field_language the field might be LANGUAGE_NONE in reality so allow it as well.
+ // @see this::field_language()
+ global $language_content;
+ $default_language = language_default('language');
+ $language = str_replace(array('***CURRENT_LANGUAGE***', '***DEFAULT_LANGUAGE***'), array($language_content->language, $default_language), $this->view->display_handler->options['field_language']);
+ $placeholder = $this->placeholder();
+ $language_fallback_candidates = array($language);
+ if (variable_get('locale_field_language_fallback', TRUE)) {
+ require_once DRUPAL_ROOT . '/includes/language.inc';
+ $language_fallback_candidates = array_merge($language_fallback_candidates, language_fallback_get_candidates());
+ }
+ else {
+ $language_fallback_candidates[] = LANGUAGE_NONE;
+ }
+ $this->query->add_where_expression(0, "$column IN($placeholder) OR $column IS NULL", array($placeholder => $language_fallback_candidates));
+ }
+ }
+
+ // The revision id inhibits grouping.
+ // So, stop here if we're using grouping, or if aren't adding all columns to
+ // the query.
+ if ($use_groupby || empty($this->definition['add fields to query'])) {
+ return;
+ }
+
+ $this->add_additional_fields(array('revision_id'));
+ }
+
+ /**
+ * Determine if the field table should be added to the query.
+ */
+ function add_field_table($use_groupby) {
+ // Grouping is enabled, or we are explicitly required to do this.
+ if ($use_groupby || !empty($this->definition['add fields to query'])) {
+ return TRUE;
+ }
+ // This a multiple value field, but "group multiple values" is not checked.
+ if ($this->multiple && !$this->options['group_rows']) {
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Determine if this field is click sortable.
+ */
+ function click_sortable() {
+ // Not click sortable in any case.
+ if (empty($this->definition['click sortable'])) {
+ return FALSE;
+ }
+ // A field is not click sortable if it's a multiple field with
+ // "group multiple values" checked, since a click sort in that case would
+ // add a join to the field table, which would produce unwanted duplicates.
+ if ($this->multiple && $this->options['group_rows']) {
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /**
+ * Called to determine what to tell the clicksorter.
+ */
+ function click_sort($order) {
+ // No column selected, can't continue.
+ if (empty($this->options['click_sort_column'])) {
+ return;
+ }
+
+ $this->ensure_my_table();
+ $column = _field_sql_storage_columnname($this->definition['field_name'], _field_sql_storage_columnname($this->definition['subfield_name'], $this->options['click_sort_column']));
+ if (!isset($this->aliases[$column])) {
+ // Column is not in query; add a sort on it (without adding the column).
+ $this->aliases[$column] = $this->table_alias . '.' . $column;
+ }
+ $this->query->add_orderby(NULL, NULL, $order, $this->aliases[$column]);
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ // option_definition runs before init/construct, so no $this->field_info
+ $field = field_info_field($this->definition['subfield_name']);
+ $field_type = field_info_field_types($field['type']);
+ $column_names = array_keys($field['columns']);
+ $default_column = '';
+ // Try to determine a sensible default.
+ if (count($column_names) == 1) {
+ $default_column = $column_names[0];
+ }
+ elseif (in_array('value', $column_names)) {
+ $default_column = 'value';
+ }
+
+ // If the field has a "value" column, we probably need that one.
+ $options['click_sort_column'] = array(
+ 'default' => $default_column,
+ );
+ $options['type'] = array(
+ 'default' => $field_type['default_formatter'],
+ );
+ $options['settings'] = array(
+ 'default' => array(),
+ );
+ $options['group_column'] = array(
+ 'default' => $default_column,
+ );
+ $options['group_columns'] = array(
+ 'default' => array(),
+ );
+
+ // Options used for multiple value fields.
+ $options['group_rows'] = array(
+ 'default' => TRUE,
+ 'bool' => TRUE,
+ );
+ // If we know the exact number of allowed values, then that can be
+ // the default. Otherwise, default to 'all'.
+ $options['delta_limit'] = array(
+ 'default' => ($field['cardinality'] > 1) ? $field['cardinality'] : 'all',
+ );
+ $options['delta_offset'] = array(
+ 'default' => 0,
+ );
+ $options['delta_reversed'] = array(
+ 'default' => FALSE,
+ 'bool' => TRUE,
+ );
+ $options['delta_first_last'] = array(
+ 'default' => FALSE,
+ 'bool' => TRUE,
+ );
+
+ $options['multi_type'] = array(
+ 'default' => 'separator',
+ );
+ $options['separator'] = array(
+ 'default' => ', ',
+ );
+
+ $options['field_api_classes'] = array(
+ 'default' => FALSE,
+ 'bool' => TRUE,
+ );
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+ module_load_include('inc', 'views', 'modules/field/views_handler_field_field');
+ $subfield = $this->field_info;
+ $formatters = _field_view_formatter_options($subfield['type']);
+ $column_names = array_keys($subfield['columns']);
+
+ // If this is a multiple value field, add its options.
+ if ($this->multiple) {
+ $this->multiple_options_form($form, $form_state);
+ }
+
+ // No need to ask the user anything if the field has only one column.
+ if (count($subfield['columns']) == 1) {
+ $form['click_sort_column'] = array(
+ '#type' => 'value',
+ '#value' => isset($column_names[0]) ? $column_names[0] : '',
+ );
+ }
+ else {
+ $form['click_sort_column'] = array(
+ '#type' => 'select',
+ '#title' => t('Column used for click sorting'),
+ '#options' => drupal_map_assoc($column_names),
+ '#default_value' => $this->options['click_sort_column'],
+ '#description' => t('Used by Style: Table to determine the actual column to click sort the field on. The default is usually fine.'),
+ '#fieldset' => 'more',
+ );
+ }
+
+ $form['type'] = array(
+ '#type' => 'select',
+ '#title' => t('Formatter'),
+ '#options' => $formatters,
+ '#default_value' => $this->options['type'],
+ '#ajax' => array(
+ 'path' => views_ui_build_form_url($form_state),
+ ),
+ '#submit' => array('views_ui_config_item_form_submit_temporary'),
+ '#executes_submit_callback' => TRUE,
+ );
+
+ $form['field_api_classes'] = array(
+ '#title' => t('Use field template'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->options['field_api_classes'],
+ '#description' => t('If checked, field api classes will be added using field.tpl.php (or equivalent). This is not recommended unless your CSS depends upon these classes. If not checked, template will not be used.'),
+ '#fieldset' => 'style_settings',
+ '#weight' => 20,
+ );
+
+ if ($this->multiple) {
+ $form['field_api_classes']['#description'] .= ' ' . t('Checking this option will cause the group Display Type and Separator values to be ignored.');
+ }
+
+ // Get the currently selected formatter.
+ $format = $this->options['type'];
+
+ $formatter = field_info_formatter_types($format);
+ $settings = $this->options['settings'] + field_info_formatter_settings($format);
+
+ // Provide an instance array for hook_field_formatter_settings_form().
+ ctools_include('fields');
+ $this->instance = ctools_fields_fake_field_instance($this->definition['subfield_name'], '_custom', $formatter, $settings);
+
+ // Store the settings in a '_custom' view mode.
+ $this->instance['display']['_custom'] = array(
+ 'type' => $format,
+ 'settings' => $settings,
+ );
+
+ // Get the settings form.
+ $settings_form = array('#value' => array());
+ $function = $formatter['module'] . '_field_formatter_settings_form';
+ if (function_exists($function)) {
+ $settings_form = $function($subfield, $this->instance, '_custom', $form, $form_state);
+ }
+ $form['settings'] = $settings_form;
+ }
+
+ /**
+ * Provide options for multiple value fields.
+ */
+ function multiple_options_form(&$form, &$form_state) {
+ $field = $this->multifield_info;
+
+ $form['multiple_field_settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Multiple field settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#weight' => 5,
+ );
+
+ $form['group_rows'] = array(
+ '#title' => t('Display all values in the same row'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->options['group_rows'],
+ '#description' => t('If checked, multiple values for this field will be shown in the same row. If not checked, each value in this field will create a new row. If using group by, please make sure to group by "Entity ID" for this setting to have any effect.'),
+ '#fieldset' => 'multiple_field_settings',
+ );
+
+ // Make the string translatable by keeping it as a whole rather than
+ // translating prefix and suffix separately.
+ list($prefix, $suffix) = explode('@count', t('Display @count value(s)'));
+
+ if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) {
+ $type = 'textfield';
+ $options = NULL;
+ $size = 5;
+ }
+ else {
+ $type = 'select';
+ $options = drupal_map_assoc(range(1, $field['cardinality']));
+ $size = 1;
+ }
+ $form['multi_type'] = array(
+ '#type' => 'radios',
+ '#title' => t('Display type'),
+ '#options' => array(
+ 'ul' => t('Unordered list'),
+ 'ol' => t('Ordered list'),
+ 'separator' => t('Simple separator'),
+ ),
+ '#dependency' => array('edit-options-group-rows' => array(TRUE)),
+ '#default_value' => $this->options['multi_type'],
+ '#fieldset' => 'multiple_field_settings',
+ );
+
+ $form['separator'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Separator'),
+ '#default_value' => $this->options['separator'],
+ '#dependency' => array(
+ 'radio:options[multi_type]' => array('separator'),
+ 'edit-options-group-rows' => array(TRUE),
+ ),
+ '#dependency_count' => 2,
+ '#fieldset' => 'multiple_field_settings',
+ );
+
+ $form['delta_limit'] = array(
+ '#type' => $type,
+ '#size' => $size,
+ '#field_prefix' => $prefix,
+ '#field_suffix' => $suffix,
+ '#options' => $options,
+ '#default_value' => $this->options['delta_limit'],
+ '#prefix' => '
',
+ '#dependency' => array('edit-options-group-rows' => array(TRUE)),
+ '#fieldset' => 'multiple_field_settings',
+ );
+
+ list($prefix, $suffix) = explode('@count', t('starting from @count'));
+ $form['delta_offset'] = array(
+ '#type' => 'textfield',
+ '#size' => 5,
+ '#field_prefix' => $prefix,
+ '#field_suffix' => $suffix,
+ '#default_value' => $this->options['delta_offset'],
+ '#dependency' => array('edit-options-group-rows' => array(TRUE)),
+ '#description' => t('(first item is 0)'),
+ '#fieldset' => 'multiple_field_settings',
+ );
+ $form['delta_reversed'] = array(
+ '#title' => t('Reversed'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->options['delta_reversed'],
+ '#suffix' => $suffix,
+ '#dependency' => array('edit-options-group-rows' => array(TRUE)),
+ '#description' => t('(start from last values)'),
+ '#fieldset' => 'multiple_field_settings',
+ );
+ $form['delta_first_last'] = array(
+ '#title' => t('First and last only'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->options['delta_first_last'],
+ '#suffix' => '
',
+ '#dependency' => array('edit-options-group-rows' => array(TRUE)),
+ '#fieldset' => 'multiple_field_settings',
+ );
+ }
+
+ /**
+ * Extend the groupby form with group columns.
+ */
+ function groupby_form(&$form, &$form_state) {
+ parent::groupby_form($form, $form_state);
+ // With "field API" fields, the column target of the grouping function
+ // and any additional grouping columns must be specified.
+ $group_columns = array(
+ 'entity_id' => t('Entity ID'),
+ ) + drupal_map_assoc(array_keys($this->multifield_info['columns']), 'ucfirst');
+
+ $form['group_column'] = array(
+ '#type' => 'select',
+ '#title' => t('Group column'),
+ '#default_value' => $this->options['group_column'],
+ '#description' => t('Select the column of this field to apply the grouping function selected above.'),
+ '#options' => $group_columns,
+ );
+
+ $options = drupal_map_assoc(array('bundle', 'language', 'entity_type'), 'ucfirst');
+
+ // Add on defined fields, noting that they're prefixed with the field name.
+ $form['group_columns'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Group columns (additional)'),
+ '#default_value' => $this->options['group_columns'],
+ '#description' => t('Select any additional columns of this field to include in the query and to group on.'),
+ '#options' => $options + $group_columns,
+ );
+ }
+
+ function groupby_form_submit(&$form, &$form_state) {
+ parent::groupby_form_submit($form, $form_state);
+ $item = &$form_state['handler']->options;
+
+ // Add settings for "field API" fields.
+ $item['group_column'] = $form_state['values']['options']['group_column'];
+ $item['group_columns'] = array_filter($form_state['values']['options']['group_columns']);
+ }
+
+ /**
+ * Load the entities for all fields that are about to be displayed.
+ */
+ function post_execute(&$values) {
+ if (!empty($values)) {
+ // Divide the entity ids by entity type, so they can be loaded in bulk.
+ $entities_by_type = array();
+ $revisions_by_type = array();
+ foreach ($values as $key => $object) {
+ if (isset($this->aliases['entity_type']) && isset($object->{$this->aliases['entity_type']}) && isset($object->{$this->field_alias}) && !isset($values[$key]->_field_data[$this->field_alias])) {
+ $entity_type = $object->{$this->aliases['entity_type']};
+ if (empty($this->definition['is revision'])) {
+ $entity_id = $object->{$this->field_alias};
+ $entities_by_type[$entity_type][$key] = $entity_id;
+ }
+ else {
+ $revision_id = $object->{$this->field_alias};
+ $entity_id = $object->{$this->aliases['entity_id']};
+ $entities_by_type[$entity_type][$key] = array($entity_id, $revision_id);
+ }
+ }
+ }
+
+ // Load the entities.
+ foreach ($entities_by_type as $entity_type => $entity_ids) {
+ $entity_info = entity_get_info($entity_type);
+ if (empty($this->definition['is revision'])) {
+ $entities = entity_load($entity_type, $entity_ids);
+ $keys = $entity_ids;
+ }
+ else {
+ // Revisions can't be loaded multiple, so we have to load them
+ // one by one.
+ $entities = array();
+ $keys = array();
+ foreach ($entity_ids as $key => $combined) {
+ list($entity_id, $revision_id) = $combined;
+ $entity = entity_load($entity_type, array($entity_id), array($entity_info['entity keys']['revision'] => $revision_id));
+ if ($entity) {
+ $entities[$revision_id] = array_shift($entity);
+ $keys[$key] = $revision_id;
+ }
+ }
+ }
+
+ foreach ($keys as $key => $entity_id) {
+ // If this is a revision, load the revision instead.
+ if (isset($entities[$entity_id])) {
+ $values[$key]->_field_data[$this->field_alias] = array(
+ 'entity_type' => $entity_type,
+ 'entity' => $entities[$entity_id],
+ );
+ }
+ }
+ }
+
+ // Now, transfer the data back into the resultset so it can be easily used.
+ foreach ($values as $row_id => &$value) {
+ $value->{'field_' . $this->options['id']} = $this->set_items($value, $row_id);
+ }
+ }
+ }
+
+ /**
+ * Render all items in this field together.
+ *
+ * When using advanced render, each possible item in the list is rendered
+ * individually. Then the items are all pasted together.
+ */
+ function render_items($items) {
+ if (!empty($items)) {
+ if (!$this->options['group_rows']) {
+ return implode('', $items);
+ }
+
+ if ($this->options['multi_type'] == 'separator') {
+ return implode(filter_xss_admin($this->options['separator']), $items);
+ }
+ else {
+ return theme('item_list', array(
+ 'items' => $items,
+ 'title' => NULL,
+ 'type' => $this->options['multi_type'],
+ ));
+ }
+ }
+ }
+
+ function get_items($values) {
+ return $values->{'field_' . $this->options['id']};
+ }
+
+ function get_value($values, $field = NULL) {
+ // Go ahead and render and store in $this->items.
+
+ // Deep clone needed, otherwise over written by $new_values.
+ $entity = $values->_field_data[$this->field_alias]['entity'];
+ $entity = unserialize(serialize($entity));
+
+ $entity_type = $values->_field_data[$this->field_alias]['entity_type'];
+ $langcode = $this->field_language($entity_type, $entity);
+ if (empty($langcode)) {
+ $langcode = LANGUAGE_NONE;
+ }
+
+ // If we are grouping, copy our group fields into the cloned entity.
+ // It's possible this will cause some weirdness, but there's only
+ // so much we can hope to do.
+ if (!empty($this->group_fields)) {
+ // first, test to see if we have a base value.
+ $base_value = array();
+ // Note: We would copy original values here, but it can cause problems.
+ // For example, text fields store cached filtered values as
+ // 'safe_value' which doesn't appear anywhere in the field definition
+ // so we can't affect it. Other side effects could happen similarly.
+ $data = FALSE;
+ foreach ($this->group_fields as $field_name => $column) {
+ if (property_exists($values, $this->aliases[$column])) {
+ $base_value[$field_name] = $values->{$this->aliases[$column]};
+ if (isset($base_value[$field_name])) {
+ $data = TRUE;
+ }
+ }
+ }
+
+ // If any of our aggregated fields have data, fake it:
+ if ($data) {
+ // Now, overwrite the original value with our aggregated value.
+ // This overwrites it so there is always just one entry.
+ $entity->{$this->definition['field_name']}[$langcode] = array($base_value);
+ }
+ else {
+ $entity->{$this->definition['field_name']}[$langcode] = array();
+ }
+ }
+
+ // The field we are trying to display doesn't exist on this entity.
+ if (!isset($entity->{$this->definition['field_name']})) {
+ return array();
+ }
+
+ // We are supposed to show only certain deltas.
+ if ($this->limit_values && !empty($entity->{$this->definition['field_name']})) {
+ $all_values = !empty($entity->{$this->definition['field_name']}[$langcode]) ? $entity->{$this->definition['field_name']}[$langcode] : array();
+ if ($this->options['delta_reversed']) {
+ $all_values = array_reverse($all_values);
+ }
+
+ // Offset is calculated differently when row grouping for a field is
+ // not enabled. Since there are multiple rows, the delta needs to be
+ // taken into account, so that different values are shown per row.
+ if (!$this->options['group_rows'] && isset($this->aliases['delta']) && isset($values->{$this->aliases['delta']})) {
+ $delta_limit = 1;
+ $offset = $values->{$this->aliases['delta']};
+ }
+ // Single fields don't have a delta available so choose 0.
+ elseif (!$this->options['group_rows'] && !$this->multiple) {
+ $delta_limit = 1;
+ $offset = 0;
+ }
+ else {
+ $delta_limit = $this->options['delta_limit'];
+ $offset = intval($this->options['delta_offset']);
+
+ // We should only get here in this case if there's an offset, and
+ // in that case we're limiting to all values after the offset.
+ if ($delta_limit == 'all') {
+ $delta_limit = count($all_values) - $offset;
+ }
+ }
+
+ // Determine if only the first and last values should be shown
+ $delta_first_last = $this->options['delta_first_last'];
+
+ $new_values = array();
+ for ($i = 0; $i < $delta_limit; $i++) {
+ $new_delta = $offset + $i;
+
+ if (isset($all_values[$new_delta])) {
+ // If first-last option was selected, only use the first and last values
+ if (!$delta_first_last
+ // Use the first value.
+ || $new_delta == $offset
+ // Use the last value.
+ || $new_delta == ($delta_limit + $offset - 1)) {
+ $new_values[] = $all_values[$new_delta];
+ }
+ }
+ }
+ $entity->{$this->definition['field_name']}[$langcode] = $new_values;
+ }
+
+ if ($field == 'entity') {
+ return $entity;
+ }
+ else {
+ return !empty($entity->{$this->definition['field_name']}[$langcode]) ? $entity->{$this->definition['field_name']}[$langcode] : array();
+ }
+ }
+
+ /**
+ * Return an array of items for the field.
+ */
+ function set_items($values, $row_id) {
+ // In some cases the instance on the entity might be easy, see
+ // https://drupal.org/node/1161708 and https://drupal.org/node/1461536 for
+ // more information.
+ if (empty($values->_field_data[$this->field_alias]) || empty($values->_field_data[$this->field_alias]['entity']) || !isset($values->_field_data[$this->field_alias]['entity']->{$this->definition['field_name']})) {
+ return array();
+ }
+
+ $display = array(
+ 'type' => $this->options['type'],
+ 'settings' => $this->options['settings'],
+ 'label' => 'hidden',
+
+ // Pass the View object in the display so that fields can act on it.
+ 'views_view' => $this->view,
+ 'views_field' => $this,
+ 'views_row_id' => $row_id,
+ );
+
+
+ $entity_type = $values->_field_data[$this->field_alias]['entity_type'];
+ $entity = $this->get_value($values, 'entity');
+ if (!$entity) {
+ return array();
+ }
+
+ $langcode = $this->field_language($entity_type, $entity);
+ if (empty($langcode)) {
+ $langcode = LANGUAGE_NONE;
+ }
+
+ $multifield_items = field_get_items($entity_type, $entity, $this->definition['field_name'], $langcode);
+ array_walk($multifield_items, 'multifield_item_unserialize', multifield_extract_multifield_machine_name($this->multifield_info));
+ $render_array = array();
+ foreach ($multifield_items as $multifield_item) {
+ $multifield = _multifield_field_item_to_entity(multifield_extract_multifield_machine_name($this->multifield_info), $multifield_item);
+ $subfield_langcode = $this->field_language('multifield', $multifield);
+ if (empty($render_array)) {
+ $render_array = field_view_field('multifield', $multifield, $this->definition['subfield_name'], $display, $subfield_langcode);
+ }
+ else {
+ $subfield_render_array = field_view_field('multifield', $multifield, $this->definition['subfield_name'], $display, $subfield_langcode);
+ // Multifield subfields are always single value.
+ $render_array[] = $subfield_render_array[0];
+ }
+ }
+
+ $items = array();
+ if ($this->options['field_api_classes']) {
+ // Make a copy.
+ $array = $render_array;
+ return array(array('rendered' => drupal_render($render_array)));
+ }
+
+ foreach (element_children($render_array) as $count) {
+ $items[$count]['rendered'] = $render_array[$count];
+ // field_view_field() adds an #access property to the render array that
+ // determines whether or not the current user is allowed to view the
+ // field in the context of the current entity. We need to respect this
+ // parameter when we pull out the children of the field array for
+ // rendering.
+ if (isset($render_array['#access'])) {
+ $items[$count]['rendered']['#access'] = $render_array['#access'];
+ }
+ // Only add the raw field items (for use in tokens) if the current user
+ // has access to view the field content.
+ if ((!isset($items[$count]['rendered']['#access']) || $items[$count]['rendered']['#access']) && !empty($render_array['#items'][$count])) {
+ $items[$count]['raw'] = $render_array['#items'][$count];
+ }
+ }
+ return $items;
+ }
+
+ function render_item($count, $item) {
+ return render($item['rendered']);
+ }
+
+ function document_self_tokens(&$tokens) {
+ if ($this->fieldHandler) {
+ $this->fieldHandler->document_self_tokens($tokens);
+ }
+ }
+
+ function add_self_tokens(&$tokens, $item) {
+ if ($this->fieldHandler) {
+ $this->fieldHandler->add_self_tokens($tokens, $item);
+ }
+ }
+
+ public function __call($name, $arguments) {
+ return call_user_func_array(array($this->fieldHandler, $name), $arguments);
+ }
+}
diff --git a/multifield.info b/multifield.info
index 4e50a68..09d2dac 100644
--- a/multifield.info
+++ b/multifield.info
@@ -6,6 +6,7 @@ dependencies[] = ctools
dependencies[] = field
configure = admin/structure/multifield
files[] = MultifieldEntityController.php
+files[] = MultifieldViewsHandler.php
files[] = tests/MultifieldAdministrationTestCase.test
files[] = tests/MultifieldDevelGenerateTestCase.test
files[] = tests/MultifieldEntityTranslationTestCase.test
diff --git a/multifield.module b/multifield.module
index fa3a1c6..f906bed 100644
--- a/multifield.module
+++ b/multifield.module
@@ -4,6 +4,17 @@ require_once dirname(__FILE__) . '/multifield.field.inc';
require_once dirname(__FILE__) . '/multifield.features.inc';
/**
+ * Implements hook_hook_info_alter().
+ */
+function multifield_hook_info_alter(&$info) {
+ // @todo Remove when https://www.drupal.org/node/2309543 is fixed.
+ $info += array_fill_keys(array(
+ 'field_views_data',
+ 'field_views_data_alter',
+ ), array('group' => 'views'));
+}
+
+/**
* Implements hook_permission().
*/
function multifield_permission() {
@@ -101,12 +112,14 @@ function multifield_entity_info() {
'label' => t('Multifield'),
'controller class' => 'MultifieldEntityController',
'base table' => 'multifield',
+ 'revision table' => 'multifield',
'fieldable' => TRUE,
// Mark this as a configuratoin entity type to prevent other modules from
// assuming they can do stuff with this entity type.
'configuration' => TRUE,
'bundle keys' => array(
'bundle' => 'machine_name',
+ 'revision' => 'revision_id',
),
'entity keys' => array(
'id' => 'id',
@@ -605,20 +618,6 @@ function multifield_form_field_ui_field_delete_form_alter(&$form, &$form_state)
}
/**
- * Implements hook_views_data_alter().
- */
-function multifield_views_data_alter(array &$data) {
- // Remove any references to the fake multifield table.
- unset($data['multifield']);
- unset($data['entity_multifield']);
- unset($data['views_entity_multifield']);
- foreach ($data as &$table) {
- unset($table['table']['join']['multifield']);
- unset($table['table']['default_relationship']['multifield']);
- }
-}
-
-/**
* Implements hook_admin_menu_map().
*/
function multifield_admin_menu_map() {
diff --git a/multifield.views.inc b/multifield.views.inc
new file mode 100644
index 0000000..2888953
--- /dev/null
+++ b/multifield.views.inc
@@ -0,0 +1,101 @@
+ &$_additional) {
+ if (!in_array($_additional, array('delta', 'language', 'bundle'))) {
+ $_additional = $field_name . '_' . $_additional;
+ }
+ }
+ foreach ($f_data['field']['additional fields'] as $index => $additional) {
+ if (!in_array($additional, array('delta', 'language', 'bundle'))) {
+ $source_field = substr($f_data['field']['additional fields'][$index], strlen($field_name . '_'));
+ if (($key = array_search($f_data['field']['additional fields'][$index], $table[$field_name]['field']['additional fields'])) !== false) {
+ unset($table[$field_name]['field']['additional fields'][$f_data['field']['additional fields'][$index]]);
+ }
+ $table[$f_data['field']['additional fields'][$index]]['field'] = $f_data['field'];
+ $table[$f_data['field']['additional fields'][$index]]['field']['handler'] = 'MultifieldViewsHandler';
+ $table[$f_data['field']['additional fields'][$index]]['field']['subfield_name'] = $f_data['field']['field_name'];
+ $table[$f_data['field']['additional fields'][$index]]['field']['field_name'] = $field_name;
+ if (isset($subfield_data['field_data_' . $subfield_name][$source_field]['field'])) {
+ $table[$f_data['field']['additional fields'][$index]]['field']['subfield handler'] = $subfield_data['field_data_' . $subfield_name][$source_field]['field']['handler'];
+ }
+ else {
+ $table[$f_data['field']['additional fields'][$index]]['field']['subfield handler'] = $f_data['field']['handler'];
+ }
+ foreach (array('argument', 'filter', 'sort') as $handler) {
+ if (isset($subfield_data['field_data_' . $subfield_name][$source_field][$handler]) && isset($table[$f_data['field']['additional fields'][$index]][$handler])) {
+ // Overwrite handler.
+ $table[$f_data['field']['additional fields'][$index]][$handler]['handler'] = $subfield_data['field_data_' . $subfield_name][$source_field][$handler]['handler'];
+ // Add additional options without overwriting table, field_name etc.
+ $table[$f_data['field']['additional fields'][$index]][$handler] += $subfield_data['field_data_' . $subfield_name][$source_field][$handler];
+ }
+ }
+ if (isset($subfield_data['field_data_' . $subfield_name][$source_field]['relationship'])) {
+ $table[$f_data['field']['additional fields'][$index]]['relationship'] = $subfield_data['field_data_' . $subfield_name][$source_field]['relationship'];
+ }
+ }
+ }
+ $table[$new_name] = $f_data;
+ $table[$new_name]['field']['handler'] = 'MultifieldViewsHandler';
+ $table[$new_name]['field']['subfield_name'] = $f_data['field']['field_name'];
+ $table[$new_name]['field']['field_name'] = $field_name;
+ $table[$new_name]['field']['subfield handler'] = $f_data['field']['handler'];
+ }
+ }
+ }
+ return $data;
+}
+
+function _multifield_subfield_views_data($field) {
+ $data = array();
+ $machine_name = multifield_extract_multifield_machine_name($field);
+ foreach (multifield_type_get_subfields($machine_name) as $subfield_name) {
+ $subfield = field_info_field($subfield_name);
+ if ($subfield['storage']['type'] != 'field_sql_storage') {
+ continue;
+ }
+
+ $result = (array) module_invoke($subfield['module'], 'field_views_data', $subfield);
+
+ if (empty($result)) {
+ $result = field_views_field_default_views_data($subfield);
+ }
+ drupal_alter('field_views_data', $result, $subfield, $subfield['module']);
+ $data[$subfield_name] = $result;
+ }
+
+ return $data;
+}