You are here

entityreference_view_widget.module in Entity Reference View Widget 7

Same filename and directory in other branches
  1. 7.2 entityreference_view_widget.module

File

entityreference_view_widget.module
View source
<?php

/**
 * Implements hook_entity_info_alter().
 */
function entityreference_view_widget_entity_info_alter(&$info) {
  foreach ($info as $entity_type => $data) {
    $info[$entity_type]['view modes']['entityreference_view_widget'] = array(
      'label' => t('Entity Reference View Widget'),
      'custom settings' => TRUE,
    );
  }
}

/**
 * Implements hook_entity_view().
 */
function entityreference_view_widget_entity_view($entity, $type, $view_mode, $langcode) {
  if ($view_mode == 'entityreference_view_widget') {
    $entity_id = entity_id($type, $entity);

    // Add just a placeholder, filled in when the widget gets themed.
    $entity->content['entityreference_view_widget_action'] = array(
      '#markup' => '<!--entityreference-view-widget-action-' . $entity_id . '-->',
      '#weight' => -10,
    );
  }
}

/**
 * Implements hook_menu().
 */
function entityreference_view_widget_menu() {
  $items = array();
  $items['entityreference_view_widget/ajax'] = array(
    'title' => 'AJAX callback',
    'page callback' => 'entityreference_view_widget_ajax',
    'delivery callback' => 'ajax_deliver',
    'access callback' => TRUE,
    'theme callback' => 'ajax_base_page_theme',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_theme().
 */
function entityreference_view_widget_theme() {
  return array(
    'widget' => array(
      'render element' => 'form',
      'template' => 'entityreference-view-widget',
    ),
    'entityreference_view_widget_selected_items' => array(
      'render element' => 'element',
    ),
  );
}

/**
 * Implements hook_views_api().
 */
function entityreference_view_widget_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'entityreference_view_widget') . '/views',
  );
}

/**
 * Custom #ajax callback.
 */
function entityreference_view_widget_ajax() {

  // Reset the cache of IDs. Drupal rather aggressively prevents id duplication
  // but this causes it to remember IDs that are no longer even being used.
  // Taken from Views.
  if (isset($_POST['ajax_html_ids'])) {
    unset($_POST['ajax_html_ids']);
  }

  // Include the node.pages when processing the form
  form_load_include($form_state, 'inc', 'node', 'node.pages');
  list($form, $form_state) = ajax_get_form();
  drupal_process_form($form['#form_id'], $form, $form_state);
  $form_parents = func_get_args();

  // Retrieve the element to be rendered.
  $form = drupal_array_get_nested_value($form, $form_parents);
  $output = drupal_render($form);
  $js = drupal_add_js();
  $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']);
  $commands = array();
  $commands[] = ajax_command_replace(NULL, $output, $settings);
  return array(
    '#type' => 'ajax',
    '#commands' => $commands,
  );
}

/**
 * Implements hook_field_widget_info().
 */
function entityreference_view_widget_field_widget_info() {
  $widgets['entityreference_view_widget'] = array(
    'label' => t('View'),
    'description' => t('An advanced, view-based widget.'),
    'field types' => array(
      'entityreference',
    ),
    'settings' => array(
      'view' => '',
      'pass_argument' => FALSE,
    ),
    'behaviors' => array(
      'multiple values' => FIELD_BEHAVIOR_CUSTOM,
      'default value' => FIELD_BEHAVIOR_NONE,
    ),
  );
  return $widgets;
}

/**
 * The module's exposed form plugin returns a form array instead of a string
 * so that the form can be added to the widget.
 * So $view->exposed_widgets is an array, and $vars['exposed'] is "Array".
 */
function entityreference_view_widget_preprocess_views_view(&$vars) {
  if (is_array($vars['exposed'])) {
    $vars['exposed'] = '';
  }
}

/**
 * Implements hook_field_widget_settings_form().
 */
function entityreference_view_widget_field_widget_settings_form($field, $instance) {

  // Only fields with unlimited cardinality are supported at the moment.
  if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
    drupal_set_message(t("The selected widget only supports fields with 'Number of values' set to 'Unlimited'. Otherwise it won't get displayed on the entity form."), 'warning');
  }
  $widget = $instance['widget'];
  $settings = $widget['settings'];
  $target_type = $field['settings']['target_type'];
  $info = entity_get_info($target_type);
  $target_base_table = $info['base table'];
  if ($widget['type'] == 'entityreference_view_widget') {

    // Get a list of all views that contain a "Entityreference View Widget" display.
    $options = array();
    $displays = views_get_applicable_views('entityreference view display');
    foreach ($displays as $data) {
      list($view, $display_id) = $data;
      $view_name = !empty($view->human_name) ? $view->human_name : $view->name;
      $options[$view->name . '|' . $display_id] = check_plain($view_name . ' | ' . $view->display_handler->display->display_title);
    }
    $element['view'] = array(
      '#type' => 'select',
      '#title' => t('View'),
      '#description' => t('Specify the View to use for selecting items. Only views that have an "Entityreference View Widget" display are shown.'),
      '#options' => $options,
      '#default_value' => $settings['view'],
      '#required' => TRUE,
    );
    $element['pass_argument'] = array(
      '#type' => 'checkbox',
      '#title' => t('Pass selected entity ids to View'),
      '#description' => t('If enabled, the View will get all selected entity ids as the first argument. Useful for excluding already selected items.'),
      '#default_value' => $settings['pass_argument'],
    );
  }
  return $element;
}

/**
 * Implements hook_field_widget_form().
 */
function entityreference_view_widget_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $field_name = $field['field_name'];
  $field_language = $element['#language'];
  $entity_type = $field['settings']['target_type'];
  $widget = $instance['widget'];

  // The view hasn't been selected yet, abort.
  if (empty($widget['settings']['view'])) {
    return;
  }

  // Only fields with unlimited cardinality are supported at the moment.
  if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
    drupal_set_message(t("The widget for field %field won't be displayed because it only supports fields with unlimited cardinality.", array(
      '%field' => $instance['label'],
    )), 'error');
    return;
  }

  // Load the items for form rebuilds from the field state as they might not be
  // in $form_state['values'] because of validation limitations. Also, they are
  // only passed in as $items when editing existing entities.
  $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state);
  if (isset($field_state['items'])) {
    $items = $field_state['items'];
  }
  $selected_entity_ids = array();
  foreach ($items as $item) {
    $selected_entity_ids[] = $item['target_id'];
  }

  // First build the view used for selecting items.
  $target_view = explode('|', $widget['settings']['view']);
  $view = views_get_view($target_view[0]);
  $view
    ->set_display($target_view[1]);

  // If the widget has been rebuilt via #ajax, change the exposed filter and
  // pager values, if needed.
  $ajax = isset($form_state['values'][$field_name][$field_language]);
  if ($ajax) {
    $values = $form_state['values'][$field_name];

    // Set exposed_raw_input and exposed_data just like views_exposed_form_submit() does.
    if (!empty($values['exposed_filters'])) {
      $exposed_input = $values['exposed_filters'];
      $view->exposed_raw_input = $exposed_input;
      $view->exposed_data = $exposed_input;

      // Also set exposed_input, to prevent get_exposed_input() from using the
      // previous outdated values
      unset($exposed_input['submit']);
      $view->exposed_input = $exposed_input;
    }

    // Only change the page if the rebuild wasn't triggered by an exposed filter,
    // and the page param is properly formatted.
    if (isset($values['page']) && is_numeric($values['page']) && empty($form_state['triggering_element']['#exposed_filter'])) {
      $view
        ->set_current_page($values['page']);
    }
  }

  // Pass the selected entity ids as the first argument, if needed.
  if (!empty($selected_entity_ids) && !empty($widget['settings']['pass_argument'])) {
    $arguments = array();
    $arguments[0] = implode(',', $selected_entity_ids);
    $view
      ->set_arguments($arguments);
  }

  // Display errors here if there is no full page build.
  $output = $ajax ? theme('status_messages') : '';
  $output .= $view
    ->preview($target_view[1]);
  $element += array(
    '#element_validate' => array(
      'entityreference_view_widget_validate',
    ),
    '#theme' => 'widget',
    '#theme_wrappers' => array(
      'fieldset',
    ),
    '#attributes' => array(
      'class' => array(
        'entityreference-view-widget',
      ),
    ),
  );
  $element['#attached']['css'][] = drupal_get_path('module', 'entityreference_view_widget') . '/entityreference_view_widget.css';
  $element['#attached']['js'][] = drupal_get_path('module', 'entityreference_view_widget') . '/entityreference_view_widget.js';

  // Add the rendered view and the exposed filters form.
  $element['view'] = array(
    '#markup' => $output,
  );
  if (!empty($view->exposed_widgets)) {
    $element['filters'] = entityreference_view_widget_prepare_filters($view->exposed_widgets, $field_name);
  }

  // Add a hidden form element to hold the current pager value.
  $element['page'] = array(
    // Set the parents above $field_language, so that the value doesn't get wiped
    // in entityreference_view_widget_validate.
    '#parents' => array(
      $field_name,
      'page',
    ),
    '#type' => 'hidden',
    '#attributes' => array(
      'class' => array(
        'entityreference-view-widget-page',
      ),
    ),
    '#default_value' => 0,
  );

  // In order to have #ajax-ified pagers, we do a horrible thing here: a hidden
  // #ajax button is added, then when a pager link is clicked, a piece of JS code
  // sets the hidden page field (defined above) and simulates a click of the
  // button (defined below).
  $element['pager_submit'] = array(
    '#type' => 'button',
    '#name' => 'pager_submit',
    '#attributes' => array(
      'class' => array(
        'pager-submit',
      ),
    ),
    '#ajax' => array(
      'path' => 'entityreference_view_widget/ajax/' . $field_name,
      'wrapper' => 'edit-' . str_replace('_', '-', $field_name),
      'method' => 'replace',
      'progress' => array(
        'type' => 'throbber',
        'message' => '',
      ),
    ),
    '#limit_validation_errors' => array(
      array(
        $field_name,
      ),
    ),
    '#weight' => 100,
    '#submit' => array(
      'entityreference_view_widget_exposed_filters_pager_submit',
    ),
  );
  if (!$view->display_handler->options['hide_left']) {

    // Add a list of selected items (each item is a full rendered entity).
    if (!empty($selected_entity_ids)) {
      $selected_entities = entity_load($entity_type, $selected_entity_ids);
      $entity_view = entity_view($entity_type, $selected_entities, 'entityreference_view_widget');
      $entity_view = reset($entity_view);
      $i = 0;
      foreach ($selected_entities as $entity_id => $entity) {

        // Remove the checkbox placeholder, it's not needed in this context.
        unset($entity_view[$entity_id]['entityreference_view_widget_action']);
        $entity_view[$entity_id]['_weight'] = array(
          '#parents' => array(
            $field_name,
            $field_language,
            'items',
            $entity_id,
            '_weight',
          ),
          '#type' => 'weight',
          '#title' => t('Weight for @title', array(
            '@title' => entity_label($entity_type, $entity),
          )),
          '#title_display' => 'invisible',
          '#delta' => count($selected_entities),
          '#default_value' => $i,
        );
        $entity_view[$entity_id]['remove'] = array(
          '#parents' => array(
            $field_name,
            $field_language,
            'items',
            $entity_id,
            'remove',
          ),
          '#type' => 'checkbox',
          '#title' => t('Remove'),
          '#weight' => -10,
          '#refresh_items' => TRUE,
          '#ajax' => array(
            'path' => 'entityreference_view_widget/ajax/' . $field_name,
            'wrapper' => 'edit-' . str_replace('_', '-', $field_name),
            'method' => 'replace',
            'progress' => array(
              'type' => 'throbber',
              'message' => '',
            ),
          ),
        );
        $i++;
      }
      $element['selected_items'] = $entity_view;
      $element['selected_items']['#theme'] = 'entityreference_view_widget_selected_items';
    }
    else {

      // Add empty text.
      $element['selected_items'] = array(
        '#markup' => t('No items have been selected.'),
      );
    }
  }

  // Create the checkboxes that will replace the placeholders in entities
  // displayed through the view.
  $substitutions = array();
  $result_entities = $view->query
    ->get_result_entities($view->result);
  foreach ($result_entities[1] as $row_id => $entity) {
    $entity_id = entity_id($entity_type, $entity);
    if (!$view->display_handler->options['hide_left']) {
      $element['add'][$entity_id] = array(
        '#type' => 'checkbox',
        '#refresh_items' => TRUE,
        '#ajax' => array(
          'path' => 'entityreference_view_widget/ajax/' . $field_name,
          'wrapper' => 'edit-' . str_replace('_', '-', $field_name),
          'method' => 'replace',
          'progress' => array(
            'type' => 'throbber',
            'message' => '',
          ),
        ),
      );
    }
    else {
      $element['add'][$entity_id] = array(
        '#type' => 'checkbox',
        '#refresh_items' => TRUE,
      );
    }
    $substitutions[] = array(
      'placeholder' => '<!--entityreference-view-widget-action-' . $entity_id . '-->',
      'entity_id' => $entity_id,
    );
  }
  $element['#substitutions'] = array(
    '#type' => 'value',
    '#value' => $substitutions,
  );
  return $element;
}

/**
 * Implements hook_preprocess_HOOK().
 */
function entityreference_view_widget_preprocess_widget(&$variables) {
  $form = $variables['form'];

  // Replace the placeholders in the view with actual checkboxes.
  $search = array();
  $replace = array();
  foreach ($form['#substitutions']['#value'] as $substitution) {
    $entity_id = $substitution['entity_id'];
    $search[] = $substitution['placeholder'];
    $replace[] = isset($form['add'][$entity_id]) ? drupal_render($form['add'][$entity_id]) : '';
  }
  $form['view']['#markup'] = str_replace($search, $replace, $form['view']['#markup']);
  $variables['selected_items'] = drupal_render($form['selected_items']);
  $variables['filters'] = drupal_render($form['filters']);
  $variables['pager_submit'] = drupal_render($form['pager_submit']);
  $variables['view'] = drupal_render($form['view']);
  unset($form['filters']);
  unset($form['selected_items']);
  unset($form['view']);
  unset($form['pager_submit']);
  $variables['extra'] = drupal_render_children($form);
}

/**
 * Returns HTML for the selected entities (usually displaed in the left sidebar).
 * By default it adds tabledrag functionality.
 *
 * @param $variables
 *   An associative array containing:
 *   - element: A render element representing the widgets.
 *
 * @ingroup themeable
 */
function theme_entityreference_view_widget_selected_items($variables) {
  $element = $variables['element'];

  // Special ID and classes for draggable tables.
  $weight_class = $element['#id'] . '-weight';
  $table_id = $element['#id'] . '-table';

  // Get our list of widgets in order (needed when the form comes back after
  // preview or failed validation).
  $widgets = array();
  foreach (element_children($element) as $key) {
    $widgets[] =& $element[$key];
  }
  usort($widgets, '_field_sort_items_value_helper');
  $rows = array();
  foreach ($widgets as $key => &$widget) {

    // Render the weight element.
    $widget['_weight']['#attributes']['class'] = array(
      $weight_class,
    );
    $weight = render($widget['_weight']);

    // Render everything else (the renderable array returned by entity_view()).
    $widget['#theme_wrappers'] = array();
    $information = drupal_render($widget);
    $rows[] = array(
      'data' => array(
        $information,
        $weight,
      ),
      'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array(
        'draggable',
      )) : array(
        'draggable',
      ),
      'no_striping' => TRUE,
    );
  }
  drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class);
  $output = empty($rows) ? '' : theme('table', array(
    'header' => array(),
    'rows' => $rows,
    'attributes' => array(
      'id' => $table_id,
    ),
  ));
  $output .= drupal_render_children($element);
  return $output;
}

/**
 * Validation callback for the widget form.
 * Adjusts the internal ids taking into account the add / remove checkboxes.
 */
function entityreference_view_widget_validate($element, &$form_state) {
  $field_name = $element['#field_name'];
  $language = $element['#language'];
  $target_type = $form_state['field'][$field_name][$language]['field']['settings']['target_type'];
  $element_values = $form_state['values'][$field_name][$language];
  $value = array();

  // First add the existing elements, if they are not marked for deletion.
  if (!empty($element_values['items'])) {

    // Sort selected elements by their weight.
    uasort($element_values['items'], '_field_sort_items_helper');
    foreach ($element_values['items'] as $entity_id => $data) {
      if (!$data['remove']) {
        $value[] = array(
          'target_type' => $target_type,
          'target_id' => $entity_id,
        );
      }
    }
  }

  // Then new items (marked for addition).
  if (!empty($element_values['add'])) {
    $add = array_filter($element_values['add']);
    foreach ($add as $entity_id => $status) {
      if (!isset($element_values['items'][$entity_id])) {
        $value[] = array(
          'target_type' => $target_type,
          'target_id' => $entity_id,
        );
      }
    }
  }
  form_set_value($element, $value, $form_state);

  // Update items.
  $parents = array_slice($element['#parents'], 0, -2);
  $field_state = field_form_get_state($parents, $field_name, $language, $form_state);
  $field_state['items'] = $value;
  field_form_set_state($parents, $field_name, $language, $form_state, $field_state);
}
function entityreference_view_widget_exposed_filters_validate($element, &$form_state) {

  // @todo Do here what views_exposed_form_validate() does.
  // We have both the exposed plugin and the view in $element['#exposed_form_plugin'].
  // Get a list of all errors, run the handler & plugin validators, get a new
  // list of errors. If the list grew, pop the new elements and add them to the
  // exposed_filters fieldset. Right now the validators do almost nothing
  // so there's no big rush to do this.
}

/**
 * Prepare the form containing exposed views filters for functioning
 * as a part of the widget form (validation, #ajax, #parents...).
 */
function entityreference_view_widget_prepare_filters($form, $field_name) {

  // $form will be a string if the user failed to select our exposed plugin style plugin.
  if (!is_array($form)) {
    return;
  }
  $form['#element_validate'] = array(
    'entityreference_view_widget_exposed_filters_validate',
  );
  $form['submit']['#submit'] = array(
    'entityreference_view_widget_exposed_filters_submit',
  );
  $form['submit']['#limit_validation_errors'] = array(
    array(
      $field_name,
    ),
  );
  $form['submit']['#name'] = 'apply-' . $field_name;
  foreach (element_get_visible_children($form) as $key) {
    $form[$key]['#parents'] = array(
      $field_name,
      'exposed_filters',
      $key,
    );
    $form[$key]['#exposed_filter'] = TRUE;
    if (!empty($form[$key]['#ajax']) && empty($form[$key]['#ajax']['path']) && empty($form[$key]['#ajax']['callback'])) {
      $form[$key]['#ajax']['path'] = 'entityreference_view_widget/ajax/' . $field_name;
      $form[$key]['#ajax']['wrapper'] = 'edit-' . str_replace('_', '-', $field_name);
    }
  }
  return $form;
}

/**
 * Submit handler added to the "Apply" button of the exposed filters form.
 * Rerenders the form so that it works even if JS is disabled.
 */
function entityreference_view_widget_exposed_filters_submit($form, &$form_state) {
  $form_state['rebuild'] = TRUE;
}

/**
 * Submit handler added to the pager submit hidden button of the exposed filters
 * form.
 * Rerenders the form so that it works even if JS is disabled.
 */
function entityreference_view_widget_exposed_filters_pager_submit($form, &$form_state) {
  $form_state['rebuild'] = TRUE;
}

Functions

Namesort descending Description
entityreference_view_widget_ajax Custom #ajax callback.
entityreference_view_widget_entity_info_alter Implements hook_entity_info_alter().
entityreference_view_widget_entity_view Implements hook_entity_view().
entityreference_view_widget_exposed_filters_pager_submit Submit handler added to the pager submit hidden button of the exposed filters form. Rerenders the form so that it works even if JS is disabled.
entityreference_view_widget_exposed_filters_submit Submit handler added to the "Apply" button of the exposed filters form. Rerenders the form so that it works even if JS is disabled.
entityreference_view_widget_exposed_filters_validate
entityreference_view_widget_field_widget_form Implements hook_field_widget_form().
entityreference_view_widget_field_widget_info Implements hook_field_widget_info().
entityreference_view_widget_field_widget_settings_form Implements hook_field_widget_settings_form().
entityreference_view_widget_menu Implements hook_menu().
entityreference_view_widget_prepare_filters Prepare the form containing exposed views filters for functioning as a part of the widget form (validation, #ajax, #parents...).
entityreference_view_widget_preprocess_views_view The module's exposed form plugin returns a form array instead of a string so that the form can be added to the widget. So $view->exposed_widgets is an array, and $vars['exposed'] is "Array".
entityreference_view_widget_preprocess_widget Implements hook_preprocess_HOOK().
entityreference_view_widget_theme Implements hook_theme().
entityreference_view_widget_validate Validation callback for the widget form. Adjusts the internal ids taking into account the add / remove checkboxes.
entityreference_view_widget_views_api Implements hook_views_api().
theme_entityreference_view_widget_selected_items Returns HTML for the selected entities (usually displaed in the left sidebar). By default it adds tabledrag functionality.