You are here

nodereference.module in Content Construction Kit (CCK) 5

Defines a field type for referencing one node from another.

File

nodereference.module
View source
<?php

/**
 * @file
 * Defines a field type for referencing one node from another.
 */

/**
 * Implementation of hook_menu().
 */
function nodereference_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'nodereference/autocomplete',
      'title' => t('node reference autocomplete'),
      'callback' => 'nodereference_autocomplete',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK,
    );
  }
  return $items;
}

/**
 * Implementation of hook_field_info().
 */
function nodereference_field_info() {
  return array(
    'nodereference' => array(
      'label' => t('Node Reference'),
    ),
  );
}

/**
 * Implementation of hook_field_settings().
 */
function nodereference_field_settings($op, $field) {
  switch ($op) {
    case 'form':
      $form = array();
      $form['referenceable_types'] = array(
        '#type' => 'checkboxes',
        '#title' => t('Content types that can be referenced'),
        '#multiple' => TRUE,
        '#default_value' => isset($field['referenceable_types']) ? $field['referenceable_types'] : array(),
        '#options' => array_map('check_plain', node_get_types('names')),
      );
      if (module_exists('views')) {
        $views = array(
          '--' => '--',
        );
        $result = db_query("SELECT name FROM {view_view} ORDER BY name");
        while ($view = db_fetch_array($result)) {
          $views[t('Existing Views')][$view['name']] = $view['name'];
        }
        views_load_cache();
        $default_views = _views_get_default_views();
        foreach ($default_views as $view) {
          $views[t('Default Views')][$view->name] = $view->name;
        }
        if (count($views) > 1) {
          $form['advanced'] = array(
            '#type' => 'fieldset',
            '#title' => t('Advanced - Nodes that can be referenced (View)'),
            '#collapsible' => TRUE,
            '#collapsed' => !isset($field['advanced_view']) || $field['advanced_view'] == '--',
          );
          $form['advanced']['advanced_view'] = array(
            '#type' => 'select',
            '#title' => t('View'),
            '#options' => $views,
            '#default_value' => isset($field['advanced_view']) ? $field['advanced_view'] : '--',
            '#description' => t('Choose the "Views module" view that selects the nodes that can be referenced.<br>Note :<ul><li>This will discard the "Content types" settings above. Use the view\'s "filters" section instead.</li><li>Use the view\'s "fields" section to display additional informations about candidate nodes on node creation/edition form.</li><li>Use the view\'s "sort criteria" section to determine the order in which candidate nodes will be displayed.</li></ul>'),
          );
          $form['advanced']['advanced_view_args'] = array(
            '#type' => 'textfield',
            '#title' => t('View arguments'),
            '#default_value' => isset($field['advanced_view_args']) ? $field['advanced_view_args'] : '',
            '#required' => FALSE,
            '#description' => t('Provide a comma separated list of arguments to pass to the view.'),
          );
        }
      }
      return $form;
    case 'save':
      $settings = array(
        'referenceable_types',
      );
      if (module_exists('views')) {
        $settings[] = 'advanced_view';
        $settings[] = 'advanced_view_args';
      }
      return $settings;
    case 'database columns':
      $columns = array(
        'nid' => array(
          'type' => 'int',
          'not null' => TRUE,
          'default' => '0',
        ),
      );
      return $columns;
    case 'filters':
      return array(
        'default' => array(
          'list' => '_nodereference_filter_handler',
          'list-type' => 'list',
          'operator' => 'views_handler_operator_or',
          'value-type' => 'array',
          'extra' => array(
            'field' => $field,
          ),
        ),
      );
  }
}

/**
 * Implementation of hook_field().
 */
function nodereference_field($op, &$node, $field, &$items, $teaser, $page) {
  switch ($op) {
    case 'validate':
      $refs = _nodereference_potential_references($field, TRUE);
      foreach ($items as $delta => $item) {
        $error_field = isset($item['error_field']) ? $item['error_field'] : '';
        unset($item['error_field']);
        if (!empty($item['nid'])) {
          if (!in_array($item['nid'], array_keys($refs))) {
            form_set_error($error_field, t('%name : This post can\'t be referenced.', array(
              '%name' => t($field['widget']['label']),
            )));
          }
        }
      }
      return;
  }
}

/**
 * Implementation of hook_field_formatter_info().
 */
function nodereference_field_formatter_info() {
  return array(
    'default' => array(
      'label' => t('Title (link)'),
      'field types' => array(
        'nodereference',
      ),
    ),
    'plain' => array(
      'label' => t('Title (no link)'),
      'field types' => array(
        'nodereference',
      ),
    ),
    'full' => array(
      'label' => t('Full node'),
      'field types' => array(
        'nodereference',
      ),
    ),
    'teaser' => array(
      'label' => t('Teaser'),
      'field types' => array(
        'nodereference',
      ),
    ),
  );
}

/**
 * Implementation of hook_field_formatter().
 */
function nodereference_field_formatter($field, $item, $formatter, $node) {
  static $titles = array();

  // We store the rendered nids in order to prevent infinite recursion
  // when using the 'full node' / 'teaser' formatters.
  static $recursion_queue = array();
  if (empty($item['nid']) || !is_numeric($item['nid'])) {
    return '';
  }
  if ($formatter == 'full' || $formatter == 'teaser') {

    // If no 'referencing node' is set, we are starting a new 'reference thread'
    if (!isset($node->referencing_node)) {
      $recursion_queue = array();
    }
    $recursion_queue[] = $node->nid;
    if (in_array($item['nid'], $recursion_queue)) {

      // Prevent infinite recursion caused by reference cycles :
      // if the node has already been rendered earlier in this 'thread',
      // we fall back to 'default' (node title) formatter.
      $formatter = 'default';
    }
    elseif ($referenced_node = node_load($item['nid'])) {
      $referenced_node->referencing_node = $node;
      $referenced_node->referencing_field = $field;
      $titles[$item['nid']] = $referenced_node->title;
    }
  }
  if (!isset($titles[$item['nid']])) {
    if ($title = db_result(db_query(db_rewrite_sql("SELECT title FROM {node} WHERE nid=%d"), $item['nid']))) {
      $titles[$item['nid']] = $title ? $title : '';
    }
  }
  if (empty($titles[$item['nid']])) {
    return '';
  }
  switch ($formatter) {
    case 'full':
      return $referenced_node ? node_view($referenced_node, FALSE) : '';
    case 'teaser':
      return $referenced_node ? node_view($referenced_node, TRUE) : '';
    case 'plain':
      return check_plain($titles[$item['nid']]);
    default:
      return $titles[$item['nid']] ? l($titles[$item['nid']], 'node/' . $item['nid']) : '';
  }
}

/**
 * Implementation of hook_widget_info().
 */
function nodereference_widget_info() {
  return array(
    'nodereference_select' => array(
      'label' => t('Select List'),
      'field types' => array(
        'nodereference',
      ),
    ),
    'nodereference_autocomplete' => array(
      'label' => t('Autocomplete Text Field'),
      'field types' => array(
        'nodereference',
      ),
    ),
  );
}

/**
 * Implementation of hook_widget().
 */
function nodereference_widget($op, &$node, $field, &$items) {
  if ($field['widget']['type'] == 'nodereference_select') {
    switch ($op) {
      case 'prepare form values':
        $items_transposed = content_transpose_array_rows_cols($items);
        $items['default nids'] = $items_transposed['nid'];
        break;
      case 'form':
        $form = array();
        $options = _nodereference_potential_references($field, TRUE);
        foreach ($options as $key => $value) {
          $options[$key] = _nodereference_item($field, $value, FALSE);
        }
        if (!$field['required']) {
          $options = array(
            0 => t('<none>'),
          ) + $options;
        }
        $form[$field['field_name']] = array(
          '#tree' => TRUE,
        );
        $form[$field['field_name']]['nids'] = array(
          '#type' => 'select',
          '#title' => t($field['widget']['label']),
          '#default_value' => $items['default nids'],
          '#multiple' => $field['multiple'],
          '#size' => $field['multiple'] ? min(count($options), 6) : 0,
          '#options' => $options,
          '#required' => $field['required'],
          '#description' => content_filter_xss(t($field['widget']['description'])),
        );
        return $form;
      case 'process form values':
        if ($field['multiple']) {

          // if nothing selected, make it 'none'
          if (empty($items['nids'])) {
            $items['nids'] = array(
              0 => '0',
            );
          }
          elseif (count($items['nids']) > 1) {
            unset($items['nids'][0]);
          }
          $items = array_values(content_transpose_array_rows_cols(array(
            'nid' => $items['nids'],
          )));
        }
        else {
          $items[0]['nid'] = $items['nids'];
        }

        // Remove the widget's data representation so it isn't saved.
        unset($items['nids']);
        foreach ($items as $delta => $item) {
          $items[$delta]['error_field'] = $field['field_name'] . '][nids';
        }
    }
  }
  else {
    switch ($op) {
      case 'prepare form values':
        foreach ($items as $delta => $item) {
          if (!empty($items[$delta]['nid'])) {
            $items[$delta]['default node_name'] = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $items[$delta]['nid']));
            $items[$delta]['default node_name'] .= ' [nid:' . $items[$delta]['nid'] . ']';
          }
        }
        break;
      case 'form':
        $form = array();
        $form[$field['field_name']] = array(
          '#tree' => TRUE,
        );
        if ($field['multiple']) {
          $form[$field['field_name']]['#type'] = 'fieldset';
          $form[$field['field_name']]['#description'] = content_filter_xss(t($field['widget']['description']));
          $delta = 0;
          foreach ($items as $item) {
            if ($item['nid']) {
              $form[$field['field_name']][$delta]['node_name'] = array(
                '#type' => 'textfield',
                '#title' => $delta == 0 ? t($field['widget']['label']) : '',
                '#autocomplete_path' => 'nodereference/autocomplete/' . $field['field_name'],
                '#default_value' => $item['default node_name'],
                '#required' => $delta == 0 ? $field['required'] : FALSE,
              );
              $delta++;
            }
          }
          foreach (range($delta, $delta + 2) as $delta) {
            $form[$field['field_name']][$delta]['node_name'] = array(
              '#type' => 'textfield',
              '#title' => $delta == 0 ? t($field['widget']['label']) : '',
              '#autocomplete_path' => 'nodereference/autocomplete/' . $field['field_name'],
              '#default_value' => '',
              '#required' => $delta == 0 ? $field['required'] : FALSE,
            );
          }
        }
        else {
          $form[$field['field_name']][0]['node_name'] = array(
            '#type' => 'textfield',
            '#title' => t($field['widget']['label']),
            '#autocomplete_path' => 'nodereference/autocomplete/' . $field['field_name'],
            '#default_value' => $items[0]['default node_name'],
            '#required' => $field['required'],
            '#description' => content_filter_xss(t($field['widget']['description'])),
          );
        }
        return $form;
      case 'validate':
        foreach ($items as $delta => $item) {
          $error_field = $field['field_name'] . '][' . $delta . '][node_name';
          if (!empty($item['node_name'])) {
            preg_match('/^(?:\\s*|(.*) )?\\[\\s*nid\\s*:\\s*(\\d+)\\s*\\]$/', $item['node_name'], $matches);
            if (!empty($matches)) {

              // explicit nid
              list(, $title, $nid) = $matches;
              if (!empty($title) && ($n = node_load($nid)) && $title != $n->title) {
                form_set_error($error_field, t('%name : Title mismatch. Please check your selection.', array(
                  '%name' => t($field['widget']['label']),
                )));
              }
            }
            else {

              // no explicit nid
              $nids = _nodereference_potential_references($field, FALSE, $item['node_name'], TRUE);
              if (empty($nids)) {
                form_set_error($error_field, t('%name: Found no valid post with that title.', array(
                  '%name' => t($field['widget']['label']),
                )));
              }
              else {

                // TODO:
                // the best thing would be to present the user with an additional form,
                // allowing the user to choose between valid candidates with the same title
                // ATM, we pick the first matching candidate...
                $nid = array_shift(array_keys($nids));
              }
            }
          }
        }
        return;
      case 'process form values':
        foreach ($items as $delta => $item) {
          $nid = 0;
          if (!empty($item['node_name'])) {
            preg_match('/^(?:\\s*|(.*) )?\\[\\s*nid\\s*:\\s*(\\d+)\\s*\\]$/', $item['node_name'], $matches);
            if (!empty($matches)) {

              // explicit nid
              $nid = $matches[2];
            }
            else {

              // no explicit nid
              // TODO :
              // the best thing would be to present the user with an additional form,
              // allowing the user to choose between valid candidates with the same title
              // ATM, we pick the first matching candidate...
              $nids = _nodereference_potential_references($field, FALSE, $item['node_name'], TRUE);
              $nid = !empty($nids) ? array_shift(array_keys($nids)) : 0;
            }
          }

          // Remove the widget's data representation so it isn't saved.
          unset($items[$delta]['node_name']);
          if (!empty($nid)) {
            $items[$delta]['nid'] = $nid;
            $items[$delta]['error_field'] = $field['field_name'] . '][' . $delta . '][node_name';
          }
          elseif ($delta > 0) {

            // Don't save empty fields when they're not the first value (keep '0' otherwise)
            unset($items[$delta]);
          }
        }
        break;
    }
  }
}

/**
 * Fetch an array of all candidate referenced nodes, for use in presenting the selection form to the user.
 */
function _nodereference_potential_references($field, $return_full_nodes = FALSE, $string = '', $exact_string = false) {
  if (module_exists('views') && isset($field['advanced_view']) && $field['advanced_view'] != '--' && ($view = views_get_view($field['advanced_view']))) {

    // advanced field : referenceable nodes defined by a view
    // let views.module build the query
    // arguments for the view
    $view_args = array();
    if (isset($field['advanced_view_args'])) {

      // TODO: Support Tokens using token.module ?
      $view_args = array_map(trim, explode(',', $field['advanced_view_args']));
    }
    if (isset($string)) {
      views_view_add_filter($view, 'node', 'title', $exact_string ? '=' : 'contains', $string, null);
    }

    // we do need title field, so add it if not present (unlikely, but...)
    $has_title = array_reduce($view->field, create_function('$a, $b', 'return ($b["field"] == "title") || $a;'), false);
    if (!$has_title) {
      views_view_add_field($view, 'node', 'title', '');
    }
    views_load_cache();
    views_sanitize_view($view);

    // make sure the fields get included in the query
    $view->page = true;
    $view->page_type = 'list';

    // make sure the query is not cached
    unset($view->query);

    // Views 1.5-
    $view->is_cacheable = FALSE;

    // Views 1.6+
    $view_result = views_build_view('result', $view, $view_args);
    $result = $view_result['result'];
  }
  else {

    // standard field : referenceable nodes defined by content types
    // build the appropriate query
    $related_types = array();
    $args = array();
    if (isset($field['referenceable_types'])) {
      foreach ($field['referenceable_types'] as $related_type) {
        if ($related_type) {
          $related_types[] = " n.type = '%s'";
          $args[] = $related_type;
        }
      }
    }
    $related_clause = implode(' OR ', $related_types);
    if (!count($related_types)) {
      return array();
    }
    if ($string !== '') {
      $string_clause = $exact_string ? " AND n.title = '%s'" : " AND n.title LIKE '%%%s%'";
      $related_clause = "(" . $related_clause . ")" . $string_clause;
      $args[] = $string;
    }
    $result = db_query(db_rewrite_sql("SELECT n.nid, n.title AS node_title, n.type AS node_type FROM {node} n WHERE " . $related_clause . " ORDER BY n.title, n.type"), $args);
  }
  if (db_num_rows($result) == 0) {
    return array();
  }
  $rows = array();
  while ($node = db_fetch_object($result)) {
    if ($return_full_nodes) {
      $rows[$node->nid] = $node;
    }
    else {
      $rows[$node->nid] = $node->node_title;
    }
  }
  return $rows;
}

/**
 * Retrieve a pipe delimited string of autocomplete suggestions
 */
function nodereference_autocomplete($field_name, $string = '') {
  $fields = content_fields();
  $field = $fields[$field_name];
  $matches = array();
  foreach (_nodereference_potential_references($field, TRUE, $string) as $row) {
    $matches[$row->node_title . ' [nid:' . $row->nid . ']'] = _nodereference_item($field, $row);
  }
  print drupal_to_js($matches);
  exit;
}
function _nodereference_item($field, $item, $html = TRUE) {
  if (module_exists('views') && isset($field['advanced_view']) && $field['advanced_view'] != '--' && ($view = views_get_view($field['advanced_view']))) {
    $output = theme('nodereference_item_advanced', $item, $view);
    if (!$html) {

      // Views theming runs check_plain (htmlentities) on the values.
      // We reverse that with html_entity_decode.
      $output = html_entity_decode(strip_tags($output), ENT_QUOTES);
    }
  }
  else {
    $output = theme('nodereference_item_simple', $item);
    $output = $html ? check_plain($output) : $output;
  }
  return $output;
}
function theme_nodereference_item_advanced($item, $view) {
  $fields = _views_get_fields();
  $item_fields = array();
  foreach ($view->field as $field) {
    $value = views_theme_field('views_handle_field', $field['queryname'], $fields, $field, $item, $view);

    // remove link tags (ex : for node titles)
    $value = preg_replace('/<a[^>]*>(.*)<\\/a>/iU', '$1', $value);
    if (!empty($value)) {
      $item_fields[] = "<span class='view-field view-data-{$field['queryname']}'>{$value}</span>";
    }
  }
  $output = implode(' - ', $item_fields);
  $output = "<span class='view-item view-item-{$view->name}'>{$output}</span>";
  return $output;
}
function theme_nodereference_item_simple($item) {
  return $item->node_title;
}

/**
 * Provide a list of nodes to filter on.
 */
function _nodereference_filter_handler($op, $filterinfo) {
  $options = array(
    0 => t('<empty>'),
  );
  $options = $options + _nodereference_potential_references($filterinfo['extra']['field']);
  return $options;
}

/**
 * Implementation of hook_panels_relationships().
 */
function nodereference_panels_relationships() {
  $args = array();
  $args['node_from_noderef'] = array(
    'title' => t('Node from reference'),
    'keyword' => 'nodereference',
    'description' => t('Adds a node from a node reference in a node context; if multiple nodes are referenced, this will get the first referenced node only.'),
    'required context' => new panels_required_context(t('Node'), 'node'),
    'context' => 'nodereference_node_from_noderef_context',
    'settings form' => 'nodereference_node_from_noderef_settings_form',
    'settings form validate' => 'nodereference_node_from_noderef_settings_form_validate',
  );
  return $args;
}

/**
 * Return a new panels context based on an existing context
 */
function nodereference_node_from_noderef_context($context = NULL, $conf) {

  // If unset it wants a generic, unfilled context, which is just NULL
  if (empty($context->data)) {
    return panels_context_create_empty('node', NULL);
  }
  if (isset($context->data->{$conf['field_name']}[0]['nid']) && ($nid = $context->data->{$conf['field_name']}[0]['nid'])) {
    if ($node = node_load($nid)) {
      return panels_context_create('node', $node);
    }
  }
}

/**
 * Settings form for the panels relationship.
 */
function nodereference_node_from_noderef_settings_form($conf) {
  $options = array();
  foreach (content_fields() as $field) {
    if ($field['type'] == 'nodereference') {
      $options[$field['field_name']] = t($field['widget']['label']);
    }
  }
  $form['field_name'] = array(
    '#title' => t('Node reference field'),
    '#type' => 'select',
    '#options' => $options,
    '#default_value' => $conf['field_name'],
    '#prefix' => '<div class="clear-block">',
    '#suffix' => '</div>',
  );
  return $form;
}

Functions

Namesort descending Description
nodereference_autocomplete Retrieve a pipe delimited string of autocomplete suggestions
nodereference_field Implementation of hook_field().
nodereference_field_formatter Implementation of hook_field_formatter().
nodereference_field_formatter_info Implementation of hook_field_formatter_info().
nodereference_field_info Implementation of hook_field_info().
nodereference_field_settings Implementation of hook_field_settings().
nodereference_menu Implementation of hook_menu().
nodereference_node_from_noderef_context Return a new panels context based on an existing context
nodereference_node_from_noderef_settings_form Settings form for the panels relationship.
nodereference_panels_relationships Implementation of hook_panels_relationships().
nodereference_widget Implementation of hook_widget().
nodereference_widget_info Implementation of hook_widget_info().
theme_nodereference_item_advanced
theme_nodereference_item_simple
_nodereference_filter_handler Provide a list of nodes to filter on.
_nodereference_item
_nodereference_potential_references Fetch an array of all candidate referenced nodes, for use in presenting the selection form to the user.