Newer
Older
<?php
// $Id$
/**
* @file
* Defines a field type for referencing one node from another.
/**
* Implementation of hook_menu().
*/
function nodereference_menu($may_cache) {
$items = array();
$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(
Yves Chedemois
committed
'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) {
Yves Chedemois
committed
$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',
Yves Chedemois
committed
'#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>'),
Yves Chedemois
committed
$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';
Yves Chedemois
committed
$settings[] = 'advanced_view_args';
}
return $settings;
case 'database columns':
$columns = array(
'nid' => array('type' => 'int', 'not null' => TRUE, 'default' => '0'),
);
return $columns;
Karen Stevenson
committed
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().
*/
Karen Stevenson
committed
function nodereference_field($op, &$node, $field, &$items, $teaser, $page) {
switch ($op) {
case 'validate':
Yves Chedemois
committed
$refs = _nodereference_potential_references($field, TRUE);
Karen Stevenson
committed
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']))));
Karen Stevenson
committed
}
}
return;
}
}
/**
Karen Stevenson
committed
* Implementation of hook_field_formatter_info().
*/
Karen Stevenson
committed
function nodereference_field_formatter_info() {
return array(
'default' => array(
Yves Chedemois
committed
'label' => t('Title (link)'),
Karen Stevenson
committed
'field types' => array('nodereference'),
),
'plain' => array(
Yves Chedemois
committed
'label' => t('Title (no link)'),
Karen Stevenson
committed
'field types' => array('nodereference'),
),
Yves Chedemois
committed
'full' => array(
Yves Chedemois
committed
'label' => t('Full node'),
Yves Chedemois
committed
'field types' => array('nodereference'),
),
'teaser' => array(
Yves Chedemois
committed
'label' => t('Teaser'),
Yves Chedemois
committed
'field types' => array('nodereference'),
),
Karen Stevenson
committed
);
}
/**
* Implementation of hook_field_formatter().
*/
function nodereference_field_formatter($field, $item, $formatter, $node) {
Yves Chedemois
committed
static $titles = array();
Yves Chedemois
committed
// 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'])) {
Yves Chedemois
committed
return '';
Karen Stevenson
committed
Yves Chedemois
committed
if ($formatter == 'full' || $formatter == 'teaser') {
Yves Chedemois
committed
// 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';
}
Yves Chedemois
committed
elseif ($referenced_node = node_load($item['nid'])) {
Yves Chedemois
committed
$referenced_node->referencing_node = $node;
$referenced_node->referencing_field = $field;
$titles[$item['nid']] = $referenced_node->title;
}
Yves Chedemois
committed
}
Yves Chedemois
committed
if (!isset($titles[$item['nid']])) {
$title = db_result(db_query("SELECT title FROM {node} WHERE nid=%d", $item['nid']));
$titles[$item['nid']] = $title ? $title : '';
}
Yves Chedemois
committed
Karen Stevenson
committed
switch ($formatter) {
Yves Chedemois
committed
case 'full':
Yves Chedemois
committed
return $referenced_node ? node_view($referenced_node, FALSE) : '';
Yves Chedemois
committed
case 'teaser':
Yves Chedemois
committed
return $referenced_node ? node_view($referenced_node, TRUE) : '';
Yves Chedemois
committed
Karen Stevenson
committed
case 'plain':
return check_plain($titles[$item['nid']]);
Karen Stevenson
committed
default:
Yves Chedemois
committed
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(
Yves Chedemois
committed
'label' => t('Select List'),
'field types' => array('nodereference'),
),
'nodereference_autocomplete' => array(
Yves Chedemois
committed
'label' => t('Autocomplete Text Field'),
'field types' => array('nodereference'),
),
);
}
/**
* Implementation of hook_widget().
*/
Karen Stevenson
committed
function nodereference_widget($op, &$node, $field, &$items) {
if ($field['widget']['type'] == 'nodereference_select') {
case 'prepare form values':
Karen Stevenson
committed
$items_transposed = content_transpose_array_rows_cols($items);
$items['default nids'] = $items_transposed['nid'];
break;
case 'form':
$form = array();
Yves Chedemois
committed
$options = _nodereference_potential_references($field, TRUE);
foreach ($options as $key => $value) {
Yves Chedemois
committed
$options[$key] = _nodereference_item($field, $value, FALSE);
if (!$field['required']) {
$options = array(0 => t('<none>')) + $options;
}
Karen Stevenson
committed
$form[$field['field_name']] = array('#tree' => TRUE);
$form[$field['field_name']]['nids'] = array(
'#type' => 'select',
'#title' => t($field['widget']['label']),
Karen Stevenson
committed
'#default_value' => $items['default nids'],
'#multiple' => $field['multiple'],
Yves Chedemois
committed
'#size' => $field['multiple'] ? min(count($options), 6) : 0,
Karen Stevenson
committed
'#options' => $options,
'#required' => $field['required'],
'#description' => content_filter_xss(t($field['widget']['description'])),
);
return $form;
case 'process form values':
Jonathan Chaffer
committed
if ($field['multiple']) {
Yves Chedemois
committed
// if nothing selected, make it 'none'
Karen Stevenson
committed
if (empty($items['nids'])) {
$items['nids'] = array(0 => '0');
Yves Chedemois
committed
// drop the 'none' options if other items were also selected
Karen Stevenson
committed
elseif (count($items['nids']) > 1) {
unset($items['nids'][0]);
Yves Chedemois
committed
Yves Chedemois
committed
$items = array_values(content_transpose_array_rows_cols(array('nid' => $items['nids'])));
Karen Stevenson
committed
$items[0]['nid'] = $items['nids'];
Jonathan Chaffer
committed
}
Jonathan Chaffer
committed
// Remove the widget's data representation so it isn't saved.
Karen Stevenson
committed
unset($items['nids']);
foreach ($items as $delta => $item) {
$items[$delta]['error_field'] = $field['field_name'] .'][nids';
Yves Chedemois
committed
}
}
}
else {
switch ($op) {
case 'prepare form values':
Karen Stevenson
committed
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']));
Karen Stevenson
committed
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'],
Karen Stevenson
committed
'#default_value' => $items[0]['default node_name'],
'#required' => $field['required'],
'#description' => content_filter_xss( t( $field['widget']['description'] )),
return $form;
Yves Chedemois
committed
case 'validate':
foreach ($items as $delta => $item) {
$error_field = $field['field_name'] .']['. $delta .'][node_name';
Yves Chedemois
committed
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) {
Yves Chedemois
committed
form_set_error($error_field, t('%name : Title mismatch. Please check your selection.', array('%name' => t($field['widget']['label']))));
Yves Chedemois
committed
}
}
Yves Chedemois
committed
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));
}
}
Yves Chedemois
committed
}
}
return;
case 'process form values':
Karen Stevenson
committed
foreach ($items as $delta => $item) {
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...
Yves Chedemois
committed
$nids = _nodereference_potential_references($field, FALSE, $item['node_name'], TRUE);
$nid = (!empty($nids)) ? array_shift(array_keys($nids)) : 0;
}
Jonathan Chaffer
committed
}
// Remove the widget's data representation so it isn't saved.
unset($items[$delta]['node_name']);
if (!empty($nid)) {
Karen Stevenson
committed
$items[$delta]['nid'] = $nid;
$items[$delta]['error_field'] = $field['field_name'] .']['. $delta .'][node_name';
Yves Chedemois
committed
}
Yves Chedemois
committed
elseif ($delta > 0) {
// Don't save empty fields when they're not the first value (keep '0' otherwise)
Yves Chedemois
committed
unset($items[$delta]);
Yves Chedemois
committed
}
Jonathan Chaffer
committed
}
}
}
/**
* 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) {
Yves Chedemois
committed
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
Yves Chedemois
committed
// arguments for the view
$view_args = array();
Yves Chedemois
committed
if (isset($field['advanced_view_args'])) {
Yves Chedemois
committed
// TODO: Support Tokens using token.module ?
Yves Chedemois
committed
$view_args = array_map(trim, explode(',', $field['advanced_view_args']));
Yves Chedemois
committed
}
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
Yves Chedemois
committed
unset($view->query); // Views 1.5-
$view->is_cacheable = FALSE; // Views 1.6+
Yves Chedemois
committed
$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) {
Yves Chedemois
committed
$related_types[] = " n.type = '%s'";
$args[] = $related_type;
}
}
}
$related_clause = implode(' OR ', $related_types);
if (!count($related_types)) {
return array();
}
Yves Chedemois
committed
if ($string !== '') {
Yves Chedemois
committed
$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)) {
Jonathan Chaffer
committed
$rows[$node->nid] = $node;
$rows[$node->nid] = $node->node_title;
Jonathan Chaffer
committed
}
}
return $rows;
}
/**
* Retrieve a pipe delimited string of autocomplete suggestions
*/
function nodereference_autocomplete($field_name, $string = '') {
$fields = content_fields();
$field = $fields[$field_name];
foreach (_nodereference_potential_references($field, TRUE, $string) as $row) {
Yves Chedemois
committed
$matches[$row->node_title .' [nid:'. $row->nid .']'] = _nodereference_item($field, $row);
print drupal_to_js($matches);
Jonathan Chaffer
committed
}
Karen Stevenson
committed
function _nodereference_item($field, $item, $html = TRUE) {
Yves Chedemois
committed
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) {
Yves Chedemois
committed
// 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;
}
Karen Stevenson
committed
/**
* Provide a list of nodes to filter on.
Karen Stevenson
committed
*/
function _nodereference_filter_handler($op, $filterinfo) {
Yves Chedemois
committed
$options = array(0 => t('<empty>'));
Karen Stevenson
committed
$options = $options + _nodereference_potential_references($filterinfo['extra']['field']);
return $options;
Yves Chedemois
committed
}
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
/**
* 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;
}