You are here

text_noderef.module in Text or Nodereference 7

Same filename and directory in other branches
  1. 6 text_noderef.module

Text or nodereference field formatter for a text field and autocomplete widget.

File

text_noderef.module
View source
<?php

/**
 * @file
 * Text or nodereference field formatter for a text field and autocomplete widget.
 */

/**
 * Implements hook_menu().
 */
function text_noderef_menu() {
  $items = array();
  $items['text_noderef/%/%'] = array(
    'title' => 'Text or Nodereference',
    'page callback' => 'text_noderef_json',
    'page arguments' => array(
      1,
      2,
    ),
    'access callback' => 'text_noderef_access',
    'access arguments' => array(
      1,
      2,
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Check access to the menu callback of the text_noderef widget.
 *
 * @param $bundle_name
 *   Bundle name.
 * @param $field_name
 *   Field name.
 */
function text_noderef_access($bundle_name, $field_name) {
  return user_access('access content') && ($field = field_info_instance('node', $field_name, $bundle_name)) && field_access('view', $field, 'node') && field_access('edit', $field, 'node');
}

/**
 * Helper function for text_noderef_json().
 *
 * @param $string
 *   The string used as needle.
 * @param $field
 *   Field definition whose widget is being used.
 * @param $result
 *   EFQ result array.
 *
 * @return
 *   Array of matches found.
 */
function _text_noderef_check_result($string, $field, $result) {
  $matches = array();
  if (isset($result['node'])) {
    $cnt = 0;
    reset($result['node']);
    while ($cnt <= 10 && ($node = each($result['node']))) {
      $node = node_load($node['value']->nid);
      if (!$field['widget']['settings']['case_sensitive']) {
        $cnt++;
        $matches[$node->title] = $node->title;
      }
      else {
        if ($field['widget']['settings']['autocomplete_match'] == 'contains' && strpos($node->title, $string) !== FALSE) {
          $cnt++;
          $matches[$node->title] = $node->title;
        }
        if ($field['widget']['settings']['autocomplete_match'] == 'starts_with' && drupal_substr($node->title, 0, drupal_strlen($string)) == $string) {
          $cnt++;
          $matches[$node->title] = $node->title;
        }
      }
    }
  }
  return $matches;
}

/**
 * Menu callback; Retrieve a pipe delimited string of autocomplete suggestions.
 *
 * @param $bundle_name
 *   Bundle name whose field widget is being used.
 * @param $field_name
 *   Field name whose widget is being used.
 * @param $string
 *   The string used as needle.
 */
function text_noderef_json($bundle_name, $field_name, $string = '') {
  $field = field_info_instance('node', $field_name, $bundle_name);
  $matches = array();

  // Do not act upon empty search string nor on unrelated fields.
  if ($field['widget']['type'] == 'text_noderef_textfield' && $string != '') {
    $needle = db_like($string) . '%';
    if ($field['widget']['settings']['autocomplete_match'] == 'contains') {
      $needle = '%' . $needle;
    }

    // Step 1/3: Check the available field data.
    $query = db_select('node', 'n');
    $query
      ->innerJoin('field_data_' . $field_name, 'fdf', 'n.nid = %alias.entity_id');
    $query
      ->condition('n.status', 1)
      ->fields('fdf', array(
      $field_name . '_value',
    ))
      ->condition($field_name . '_value', $needle, 'LIKE')
      ->groupBy($field_name . '_value')
      ->orderBy($field_name . '_value')
      ->range(0, 10)
      ->addTag('node_access');
    $result = $query
      ->execute();
    foreach ($result as $value) {
      $value = $value->{$field_name . '_value'};
      $matches[$value] = $value;
    }

    // Step 2/3: Check the node titles.
    $query = new EntityFieldQuery();
    $query
      ->entityCondition('entity_type', 'node');
    $bundles = array_filter($field['widget']['settings']['bundles']);
    if ($bundles) {
      $query
        ->entityCondition('bundle', $bundles, 'IN');
    }
    $query
      ->propertyCondition('status', 1)
      ->propertyCondition('title', $needle, 'LIKE')
      ->propertyOrderBy('title');
    $result = $query
      ->execute();
    $matches += _text_noderef_check_result($string, $field, $result);

    // Step 3/3: Merge and sanitize output.
    asort($matches, SORT_LOCALE_STRING);
    foreach ($matches as &$value) {

      // Add a class wrapper for a few required CSS overrides.
      $value = '<div class="reference-autocomplete">' . check_plain($value) . '</div>';
    }
  }
  drupal_json_output($matches);
}

/**
 * Implements hook_field_widget_info().
 */
function text_noderef_field_widget_info() {
  return array(
    'text_noderef_textfield' => array(
      'label' => t('Text or node reference field'),
      'description' => t('Autocomplete for existing text field data and/or given node titles.'),
      'field types' => array(
        'text',
      ),
      'settings' => array(
        'size' => 60,
        'bundles' => array(),
        'autocomplete_match' => 'starts_with',
        'case_sensitive' => 0,
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function text_noderef_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  if ($instance['widget']['type'] == 'text_noderef_textfield') {

    // Reusing text_textfield widget type from text.module, then adding the
    // autocomplete stuff.
    $instance['widget']['type'] = 'text_textfield';
    $element = text_field_widget_form($form, $form_state, $field, $instance, $langcode, $items, $delta, $element);
    $element['value']['#autocomplete_path'] = 'text_noderef/' . $element['#bundle'] . '/' . $element['#field_name'];
  }
  return $element;
}

/**
 * Implements hook_field_widget_settings_form().
 */
function text_noderef_field_widget_settings_form($field, $instance) {
  $form = array();
  $widget = $instance['widget'];
  $settings = $widget['settings'];
  if ($widget['type'] == 'text_noderef_textfield') {

    // Reusing text_textfield widget type from text.module, then adding the
    // autocomplete-related stuff.
    $instance['widget']['type'] = 'text_textfield';
    $form = text_field_widget_settings_form($field, $instance);
    $bundles = array();
    foreach (node_type_get_types() as $bundle) {
      $bundles[$bundle->type] = $bundle->name;
    }
    $form['bundles'] = array(
      '#type' => 'checkboxes',
      '#title' => t('Content types for autocompleting on titles'),
      '#options' => $bundles,
      '#default_value' => $settings['bundles'],
      '#description' => t('Autocompleting is done on all the content types if none of them is selected.'),
    );
    $form['autocomplete_match'] = array(
      '#type' => 'select',
      '#title' => t('Autocomplete matching'),
      '#default_value' => $settings['autocomplete_match'],
      '#options' => array(
        'starts_with' => t('Starts with'),
        'contains' => t('Contains'),
      ),
      '#description' => t('Select the method used to collect autocomplete suggestions. Note that <em>Contains</em> can cause performance issues on sites with thousands of records.'),
    );
    $form['case_sensitive'] = array(
      '#type' => 'radios',
      '#title' => t('Case sensitive'),
      '#default_value' => $settings['case_sensitive'],
      '#options' => array(
        0 => t('Disabled'),
        1 => t('Enabled'),
      ),
    );
    $form['case_sensitive']['#description'] = theme('item_list', array(
      'items' => array(
        t('Case-insensitive queries are implemented using the <a href="!function-lower-url">LOWER()</a> function in combination with the <a href="!operator-like-url">LIKE</a> operator.', array(
          '!function-lower-url' => 'http://dev.mysql.com/doc/refman/5.1/en/string-functions.html#function_lower',
          '!operator-like-url' => 'http://dev.mysql.com/doc/refman/5.1/en/string-comparison-functions.html#operator_like',
        )),
        t('Note that MySQL might ignore case sensitivity depending on the collation used in your database definition (see <a href="!mysql-i18n-l10n-url">Internationalization and Localization</a> chapter in the MySQL manual). If you need case insensitive checks, it is recommended (for performance reasons) to use a case insensitive collation as well (such as utf8_general_ci), rather than disabling the case sensitive option here.', array(
          '!mysql-i18n-l10n-url' => 'http://dev.mysql.com/doc/refman/5.1/en/internationalization-localization.html',
        )),
        t('You may want to create an expression index using the LOWER() function to speed up this kind of queries in PostgreSQL (See <a href="!indexes-expressional-url">Indexes on Expressions</a>).', array(
          '!indexes-expressional-url' => 'http://www.postgresql.org/docs/8.4/static/indexes-expressional.html',
        )),
      ),
    ));
  }
  return $form;
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * 'text_noderef_textfield' field instances should not have any type of text
 * processing beyond 'Plain text'.
 */
function text_noderef_form_field_ui_field_edit_form_alter(&$form, $form_state) {
  if ($form['#instance']['widget']['type'] == 'text_noderef_textfield') {
    $form['instance']['settings']['text_processing'] = array(
      '#type' => 'value',
      '#value' => 0,
    );
  }
}

/**
 * Implements hook_field_formatter_info().
 */
function text_noderef_field_formatter_info() {
  return array(
    'text_noderef_default' => array(
      'label' => t('Text or nodereference'),
      'field types' => array(
        'text',
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function text_noderef_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {

  // This function may be called several times on a single page, so cache
  // the already seen texts to a local static variable to speed up non-first
  // calls.
  $element = array();
  $settings = $display['settings'];
  if ($display['type'] == 'text_noderef_default') {
    $cache =& drupal_static(__FUNCTION__);
    $key = $instance['field_name'] . '@' . $instance['bundle'];
    foreach ($items as $delta => $item) {
      if (!isset($cache[$key]) || !isset($cache[$key][$item['value']])) {
        $query = new EntityFieldQuery();
        $query
          ->entityCondition('entity_type', 'node');
        $bundles = array_filter($instance['widget']['settings']['bundles']);
        if ($bundles) {
          $query
            ->entityCondition('bundle', $bundles, 'IN');
        }
        $query
          ->propertyCondition('status', 1)
          ->propertyCondition('title', $item['value'], 'LIKE')
          ->propertyOrderBy('title');
        $result = $query
          ->execute();
        if (isset($result['node'])) {
          reset($result['node']);
          $node = each($result['node']);
          $cache[$key][$item['value']] = l($item['value'], 'node/' . $node['value']->nid);
        }
        else {
          $cache[$key][$item['value']] = $item['safe_value'];
        }
      }
      $element[$delta] = array(
        '#markup' => $cache[$key][$item['value']],
      );
    }
  }
  return $element;
}

/**
 * Implements hook_content_migrate_instance_alter().
 */
function text_noderef_content_migrate_instance_alter(&$instance_value, $field_value) {
  if ($instance_value['widget']['module'] == 'text_noderef') {

    // The formatter name changed.
    foreach ($instance_value['display'] as $context => $settings) {
      if ($settings['type'] == 'text_text_noderef') {
        $instance_value['display'][$context]['type'] = 'text_noderef_default';
      }
    }
  }
}

/**
 * Implements hook_content_migrate_field_alter().
 *
 * @see http://drupal.org/node/1417626
 */
function text_noderef_content_migrate_field_alter(&$field_value, $instance_value) {
  if ($field_value['type'] == 'text' && $instance_value['widget']['type'] == 'text_noderef_textfield' && empty($field_value['settings']['max_length'])) {

    // $field_value['type'] must not be changed, because our widget and field
    // formatter are kicking in only for these. OTOH, a max_length must be
    // defined for 'simple' text fields, so we must follow this way: let's
    // define a 'high enough' (TM) value for this.
    $field_value['settings']['max_length'] = 999;
  }
}