nodereference.module in Content Construction Kit (CCK) 5
Defines a field type for referencing one node from another.
File
nodereference.moduleView 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;
}