1<?php 2// $Id$ 3 4/** 5 * @file 6 * Defines a field type for referencing one node from another. 7 */ 8 9/** 10 * Implementation of hook_menu(). 11 */ 12function nodereference_menu() { 13 $items = array(); 14 $items['nodereference/autocomplete'] = array( 15 'title' => 'Nodereference autocomplete', 16 'page callback' => 'nodereference_autocomplete', 17 'access callback' => 'nodereference_autocomplete_access', 18 'access arguments' => array(2), 19 'type' => MENU_CALLBACK 20 ); 21 return $items; 22} 23 24/** 25 * Implementation of hook_theme(). 26 */ 27function nodereference_theme() { 28 return array( 29 'nodereference_select' => array( 30 'arguments' => array('element' => NULL), 31 ), 32 'nodereference_buttons' => array( 33 'arguments' => array('element' => NULL), 34 ), 35 'nodereference_autocomplete' => array( 36 'arguments' => array('element' => NULL), 37 ), 38 'nodereference_formatter_default' => array( 39 'arguments' => array('element'), 40 ), 41 'nodereference_formatter_plain' => array( 42 'arguments' => array('element'), 43 ), 44 'nodereference_formatter_full' => array( 45 'arguments' => array('element'), 46 'function' => 'theme_nodereference_formatter_full_teaser', 47 ), 48 'nodereference_formatter_teaser' => array( 49 'arguments' => array('element'), 50 'function' => 'theme_nodereference_formatter_full_teaser', 51 ), 52 ); 53} 54 55/** 56 * Implementaion of hook_ctools_plugin_directory(). 57 */ 58function nodereference_ctools_plugin_directory($module, $plugin) { 59 if ($module == 'ctools' && $plugin == 'relationships') { 60 return 'panels/' . $plugin; 61 } 62} 63 64/** 65 * Implementation of hook_field_info(). 66 */ 67function nodereference_field_info() { 68 return array( 69 'nodereference' => array( 70 'label' => t('Node reference'), 71 'description' => t('Store the ID of a related node as an integer value.'), 72// 'content_icon' => 'icon_content_noderef.png', 73 ), 74 ); 75} 76 77/** 78 * Implementation of hook_field_settings(). 79 */ 80function nodereference_field_settings($op, $field) { 81 switch ($op) { 82 case 'form': 83 $form = array(); 84 $form['referenceable_types'] = array( 85 '#type' => 'checkboxes', 86 '#title' => t('Content types that can be referenced'), 87 '#multiple' => TRUE, 88 '#default_value' => is_array($field['referenceable_types']) ? $field['referenceable_types'] : array(), 89 '#options' => array_map('check_plain', node_get_types('names')), 90 ); 91 if (module_exists('views')) { 92 $views = array('--' => '--'); 93 $all_views = views_get_all_views(); 94 foreach ($all_views as $view) { 95 // Only 'node' views that have fields will work for our purpose. 96 if ($view->base_table == 'node' && !empty($view->display['default']->display_options['fields'])) { 97 if ($view->type == 'Default') { 98 $views[t('Default Views')][$view->name] = $view->name; 99 } 100 else { 101 $views[t('Existing Views')][$view->name] = $view->name; 102 } 103 } 104 } 105 106 $form['advanced'] = array( 107 '#type' => 'fieldset', 108 '#title' => t('Advanced - Nodes that can be referenced (View)'), 109 '#collapsible' => TRUE, 110 '#collapsed' => !isset($field['advanced_view']) || $field['advanced_view'] == '--', 111 ); 112 if (count($views) > 1) { 113 $form['advanced']['advanced_view'] = array( 114 '#type' => 'select', 115 '#title' => t('View used to select the nodes'), 116 '#options' => $views, 117 '#default_value' => isset($field['advanced_view']) ? $field['advanced_view'] : '--', 118 '#description' => t('<p>Choose the "Views module" view that selects the nodes that can be referenced.<br />Note:</p>') . 119 t('<ul><li>Only views that have fields will work for this purpose.</li><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>'), 120 ); 121 $form['advanced']['advanced_view_args'] = array( 122 '#type' => 'textfield', 123 '#title' => t('View arguments'), 124 '#default_value' => isset($field['advanced_view_args']) ? $field['advanced_view_args'] : '', 125 '#required' => FALSE, 126 '#description' => t('Provide a comma separated list of arguments to pass to the view.'), 127 ); 128 } 129 else { 130 $form['advanced']['no_view_help'] = array( 131 '#value' => t('<p>The list of nodes that can be referenced can be based on a "Views module" view but no appropriate views were found. <br />Note:</p>') . 132 t('<ul><li>Only views that have fields will work for this purpose.</li><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>'), 133 ); 134 } 135 } 136 return $form; 137 138 case 'save': 139 $settings = array('referenceable_types'); 140 if (module_exists('views')) { 141 $settings[] = 'advanced_view'; 142 $settings[] = 'advanced_view_args'; 143 } 144 return $settings; 145 146 case 'database columns': 147 $columns = array( 148 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'index' => TRUE), 149 ); 150 return $columns; 151 152 case 'views data': 153 $data = content_views_field_views_data($field); 154 $db_info = content_database_info($field); 155 $table_alias = content_views_tablename($field); 156 157 // Filter: swap the handler to the 'in' operator. 158 $data[$table_alias][$field['field_name'] .'_nid']['filter']['handler'] = 'content_handler_filter_many_to_one'; 159 // Argument: use node.title for summaries. 160 $data["node_$table_alias"]['table']['join']['node'] = array( 161 'table' => 'node', 162 'field' => 'nid', 163 'left_table' => $table_alias, 164 'left_field' => $field['field_name'] .'_nid', 165 ); 166 $data[$table_alias][$field['field_name'] .'_nid']['argument']['handler'] = 'content_handler_argument_reference'; 167 $data[$table_alias][$field['field_name'] .'_nid']['argument']['name table'] = "node_$table_alias"; 168 $data[$table_alias][$field['field_name'] .'_nid']['argument']['name field'] = 'title'; 169 // Relationship: add a relationship for related node. 170 $data[$table_alias][$field['field_name'] .'_nid']['relationship'] = array( 171 'base' => 'node', 172 'field' => $db_info['columns']['nid']['column'], 173 'handler' => 'content_handler_relationship', 174 'label' => t($field['widget']['label']), 175 'content_field_name' => $field['field_name'], 176 ); 177 return $data; 178 } 179} 180 181/** 182 * Implementation of hook_field(). 183 */ 184function nodereference_field($op, &$node, $field, &$items, $teaser, $page) { 185 static $sanitized_nodes = array(); 186 187 switch ($op) { 188 // When preparing a translation, load any translations of existing references. 189 case 'prepare translation': 190 $addition = array(); 191 $addition[$field['field_name']] = array(); 192 if (isset($node->translation_source->$field['field_name']) && is_array($node->translation_source->$field['field_name'])) { 193 foreach ($node->translation_source->$field['field_name'] as $key => $reference) { 194 $reference_node = node_load($reference['nid']); 195 // Test if the referenced node type is translatable and, if so, 196 // load translations if the reference is not for the current language. 197 // We can assume the translation module is present because it invokes 'prepare translation'. 198 if (translation_supported_type($reference_node->type) && !empty($reference_node->language) && $reference_node->language != $node->language && $translations = translation_node_get_translations($reference_node->tnid)) { 199 // If there is a translation for the current language, use it. 200 $addition[$field['field_name']][] = array( 201 'nid' => isset($translations[$node->language]) ? $translations[$node->language]->nid : $reference['nid'], 202 ); 203 } 204 } 205 } 206 return $addition; 207 208 case 'validate': 209 // Extract nids to check. 210 $ids = array(); 211 foreach ($items as $delta => $item) { 212 if (is_array($item) && !empty($item['nid'])) { 213 if (is_numeric($item['nid'])) { 214 $ids[] = $item['nid']; 215 } 216 else { 217 $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; 218 if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); 219 form_set_error($error_element, t("%name: invalid input.", array('%name' => t($field['widget']['label'])))); 220 } 221 } 222 } 223 // Prevent performance hog if there are no ids to check. 224 if ($ids) { 225 $refs = _nodereference_potential_references($field, '', NULL, $ids); 226 foreach ($items as $delta => $item) { 227 if (is_array($item)) { 228 $error_element = isset($item['_error_element']) ? $item['_error_element'] : ''; 229 if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']); 230 if (!empty($item['nid']) && !isset($refs[$item['nid']])) { 231 form_set_error($error_element, t("%name: this post can't be referenced.", array('%name' => t($field['widget']['label'])))); 232 } 233 } 234 } 235 } 236 return $items; 237 238 case 'sanitize': 239 // We can't just check the node is 'referenceable', because Views-mode 240 // could rely on 'current user' (at edit time). 241 242 // Extract nids to check. 243 $ids = array(); 244 foreach ($items as $delta => $item) { 245 if (is_array($item)) { 246 // Default to 'non accessible'. 247 $items[$delta]['safe'] = array(); 248 if (!empty($item['nid']) && is_numeric($item['nid'])) { 249 $ids[] = $item['nid']; 250 } 251 } 252 } 253 if ($ids) { 254 // Load information about nids that we haven't already loaded during 255 // this page request. 256 $missing_ids = array_diff($ids, array_keys($sanitized_nodes)); 257 if (!empty($missing_ids)) { 258 $where = array('n.nid in ('. db_placeholders($missing_ids) . ')'); 259 if (!user_access('administer nodes')) { 260 $where[] = 'n.status = 1'; 261 } 262 $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, n.status FROM {node} n WHERE '. implode(' AND ', $where)), $missing_ids); 263 while ($row = db_fetch_array($result)) { 264 $sanitized_nodes[$row['nid']] = $row; 265 } 266 } 267 foreach ($items as $delta => $item) { 268 if (is_array($item) && !empty($item['nid']) && isset($sanitized_nodes[$item['nid']])) { 269 $items[$delta]['safe'] = $sanitized_nodes[$item['nid']]; 270 } 271 } 272 } 273 return $items; 274 } 275} 276 277/** 278 * Implementation of hook_content_is_empty(). 279 */ 280function nodereference_content_is_empty($item, $field) { 281 if (empty($item['nid'])) { 282 return TRUE; 283 } 284 return FALSE; 285} 286 287/** 288 * Implementation of hook_field_formatter_info(). 289 */ 290function nodereference_field_formatter_info() { 291 return array( 292 'default' => array( 293 'label' => t('Title (link)'), 294 'field types' => array('nodereference'), 295 'multiple values' => CONTENT_HANDLE_CORE, 296 ), 297 'plain' => array( 298 'label' => t('Title (no link)'), 299 'field types' => array('nodereference'), 300 'multiple values' => CONTENT_HANDLE_CORE, 301 ), 302 'full' => array( 303 'label' => t('Full node'), 304 'field types' => array('nodereference'), 305 'multiple values' => CONTENT_HANDLE_CORE, 306 ), 307 'teaser' => array( 308 'label' => t('Teaser'), 309 'field types' => array('nodereference'), 310 'multiple values' => CONTENT_HANDLE_CORE, 311 ), 312 ); 313} 314 315/** 316 * Theme function for 'default' nodereference field formatter. 317 */ 318function theme_nodereference_formatter_default($element) { 319 $output = ''; 320 if (!empty($element['#item']['safe']['nid'])) { 321 $output = l($element['#item']['safe']['title'], 'node/'. $element['#item']['safe']['nid']); 322 if (!$element['#item']['safe']['status']) { 323 $output = '<span class="node-unpublished"> '. t('(Unpublished)') ." $output</span>"; 324 } 325 } 326 return $output; 327} 328 329/** 330 * Theme function for 'plain' nodereference field formatter. 331 */ 332function theme_nodereference_formatter_plain($element) { 333 $output = ''; 334 if (!empty($element['#item']['safe']['nid'])) { 335 $output = check_plain($element['#item']['safe']['title']); 336 if (!$element['#item']['safe']['status']) { 337 $output = '<span class="node-unpublished"> '. t('(Unpublished)') ." $output</span>"; 338 } 339 } 340 return $output; 341} 342 343/** 344 * Proxy theme function for 'full' and 'teaser' nodereference field formatters. 345 */ 346function theme_nodereference_formatter_full_teaser($element) { 347 static $recursion_queue = array(); 348 $output = ''; 349 if (!empty($element['#item']['safe']['nid'])) { 350 $nid = $element['#item']['safe']['nid']; 351 $node = $element['#node']; 352 $field = content_fields($element['#field_name'], $element['#type_name']); 353 // If no 'referencing node' is set, we are starting a new 'reference thread' 354 if (!isset($node->referencing_node)) { 355 $recursion_queue = array(); 356 } 357 $recursion_queue[] = $node->nid; 358 if (in_array($nid, $recursion_queue)) { 359 // Prevent infinite recursion caused by reference cycles: 360 // if the node has already been rendered earlier in this 'thread', 361 // we fall back to 'default' (node title) formatter. 362 return theme('nodereference_formatter_default', $element); 363 } 364 if ($referenced_node = node_load($nid)) { 365 $referenced_node->referencing_node = $node; 366 $referenced_node->referencing_field = $field; 367 $output = node_view($referenced_node, $element['#formatter'] == 'teaser'); 368 } 369 } 370 return $output; 371} 372 373/** 374 * Helper function for formatters. 375 * 376 * Store node titles collected in the curent request. 377 */ 378function _nodereference_titles($nid, $known_title = NULL) { 379 static $titles = array(); 380 if (!isset($titles[$nid])) { 381 $title = $known_title ? $known_title : db_result(db_query(db_rewrite_sql("SELECT n.title FROM {node} n WHERE n.nid=%d"), $nid)); 382 $titles[$nid] = $title ? $title : ''; 383 } 384 return $titles[$nid]; 385} 386 387/** 388 * Implementation of hook_widget_info(). 389 * 390 * We need custom handling of multiple values for the nodereference_select 391 * widget because we need to combine them into a options list rather 392 * than display multiple elements. 393 * 394 * We will use the content module's default handling for default value. 395 * 396 * Callbacks can be omitted if default handing is used. 397 * They're included here just so this module can be used 398 * as an example for custom modules that might do things 399 * differently. 400 */ 401function nodereference_widget_info() { 402 return array( 403 'nodereference_select' => array( 404 'label' => t('Select list'), 405 'field types' => array('nodereference'), 406 'multiple values' => CONTENT_HANDLE_MODULE, 407 'callbacks' => array( 408 'default value' => CONTENT_CALLBACK_DEFAULT, 409 ), 410 ), 411 'nodereference_buttons' => array( 412 'label' => t('Check boxes/radio buttons'), 413 'field types' => array('nodereference'), 414 'multiple values' => CONTENT_HANDLE_MODULE, 415 'callbacks' => array( 416 'default value' => CONTENT_CALLBACK_DEFAULT, 417 ), 418 ), 419 'nodereference_autocomplete' => array( 420 'label' => t('Autocomplete text field'), 421 'field types' => array('nodereference'), 422 'multiple values' => CONTENT_HANDLE_CORE, 423 'callbacks' => array( 424 'default value' => CONTENT_CALLBACK_DEFAULT, 425 ), 426 ), 427 ); 428} 429 430/** 431 * Implementation of FAPI hook_elements(). 432 * 433 * Any FAPI callbacks needed for individual widgets can be declared here, 434 * and the element will be passed to those callbacks for processing. 435 * 436 * Drupal will automatically theme the element using a theme with 437 * the same name as the hook_elements key. 438 * 439 * Autocomplete_path is not used by text_widget but other widgets can use it 440 * (see nodereference and userreference). 441 */ 442function nodereference_elements() { 443 return array( 444 'nodereference_select' => array( 445 '#input' => TRUE, 446 '#columns' => array('uid'), '#delta' => 0, 447 '#process' => array('nodereference_select_process'), 448 ), 449 'nodereference_buttons' => array( 450 '#input' => TRUE, 451 '#columns' => array('uid'), '#delta' => 0, 452 '#process' => array('nodereference_buttons_process'), 453 ), 454 'nodereference_autocomplete' => array( 455 '#input' => TRUE, 456 '#columns' => array('name'), '#delta' => 0, 457 '#process' => array('nodereference_autocomplete_process'), 458 '#autocomplete_path' => FALSE, 459 ), 460 ); 461} 462 463/** 464 * Implementation of hook_widget_settings(). 465 */ 466function nodereference_widget_settings($op, $widget) { 467 switch ($op) { 468 case 'form': 469 $form = array(); 470 $match = isset($widget['autocomplete_match']) ? $widget['autocomplete_match'] : 'contains'; 471 $size = (isset($widget['size']) && is_numeric($widget['size'])) ? $widget['size'] : 60; 472 if ($widget['type'] == 'nodereference_autocomplete') { 473 $form['autocomplete_match'] = array( 474 '#type' => 'select', 475 '#title' => t('Autocomplete matching'), 476 '#default_value' => $match, 477 '#options' => array( 478 'starts_with' => t('Starts with'), 479 'contains' => t('Contains'), 480 ), 481 '#description' => t('Select the method used to collect autocomplete suggestions. Note that <em>Contains</em> can cause performance issues on sites with thousands of nodes.'), 482 ); 483 $form['size'] = array( 484 '#type' => 'textfield', 485 '#title' => t('Size of textfield'), 486 '#default_value' => $size, 487 '#element_validate' => array('_element_validate_integer_positive'), 488 '#required' => TRUE, 489 ); 490 } 491 else { 492 $form['autocomplete_match'] = array('#type' => 'hidden', '#value' => $match); 493 $form['size'] = array('#type' => 'hidden', '#value' => $size); 494 } 495 return $form; 496 497 case 'save': 498 return array('autocomplete_match', 'size'); 499 } 500} 501 502/** 503 * Implementation of hook_widget(). 504 * 505 * Attach a single form element to the form. It will be built out and 506 * validated in the callback(s) listed in hook_elements. We build it 507 * out in the callbacks rather than here in hook_widget so it can be 508 * plugged into any module that can provide it with valid 509 * $field information. 510 * 511 * Content module will set the weight, field name and delta values 512 * for each form element. This is a change from earlier CCK versions 513 * where the widget managed its own multiple values. 514 * 515 * If there are multiple values for this field, the content module will 516 * call this function as many times as needed. 517 * 518 * @param $form 519 * the entire form array, $form['#node'] holds node information 520 * @param $form_state 521 * the form_state, $form_state['values'][$field['field_name']] 522 * holds the field's form values. 523 * @param $field 524 * the field array 525 * @param $items 526 * array of default values for this field 527 * @param $delta 528 * the order of this item in the array of subelements (0, 1, 2, etc) 529 * 530 * @return 531 * the form item for a single element for this field 532 */ 533function nodereference_widget(&$form, &$form_state, $field, $items, $delta = 0) { 534 switch ($field['widget']['type']) { 535 case 'nodereference_select': 536 $element = array( 537 '#type' => 'nodereference_select', 538 '#default_value' => $items, 539 ); 540 break; 541 542 case 'nodereference_buttons': 543 $element = array( 544 '#type' => 'nodereference_buttons', 545 '#default_value' => $items, 546 ); 547 break; 548 549 case 'nodereference_autocomplete': 550 $element = array( 551 '#type' => 'nodereference_autocomplete', 552 '#default_value' => isset($items[$delta]) ? $items[$delta] : NULL, 553 '#value_callback' => 'nodereference_autocomplete_value', 554 ); 555 break; 556 } 557 return $element; 558} 559 560/** 561 * Value for a nodereference autocomplete element. 562 * 563 * Substitute in the node title for the node nid. 564 */ 565function nodereference_autocomplete_value($element, $edit = FALSE) { 566 $field_key = $element['#columns'][0]; 567 if (!empty($element['#default_value'][$field_key])) { 568 $nid = $element['#default_value'][$field_key]; 569 $value = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $nid)); 570 $value .= ' [nid:'. $nid .']'; 571 return array($field_key => $value); 572 } 573 return array($field_key => NULL); 574} 575 576/** 577 * Process an individual element. 578 * 579 * Build the form element. When creating a form using FAPI #process, 580 * note that $element['#value'] is already set. 581 * 582 * The $fields array is in $form['#field_info'][$element['#field_name']]. 583 */ 584function nodereference_select_process($element, $edit, $form_state, $form) { 585 // The nodereference_select widget doesn't need to create its own 586 // element, it can wrap around the optionwidgets_select element. 587 // This will create a new, nested instance of the field. 588 // Add a validation step where the value can be unwrapped. 589 $field_key = $element['#columns'][0]; 590 $element[$field_key] = array( 591 '#type' => 'optionwidgets_select', 592 '#default_value' => isset($element['#value']) ? $element['#value'] : '', 593 // The following values were set by the content module and need 594 // to be passed down to the nested element. 595 '#title' => $element['#title'], 596 '#required' => $element['#required'], 597 '#description' => $element['#description'], 598 '#field_name' => $element['#field_name'], 599 '#type_name' => $element['#type_name'], 600 '#delta' => $element['#delta'], 601 '#columns' => $element['#columns'], 602 ); 603 if (empty($element[$field_key]['#element_validate'])) { 604 $element[$field_key]['#element_validate'] = array(); 605 } 606 array_unshift($element[$field_key]['#element_validate'], 'nodereference_optionwidgets_validate'); 607 return $element; 608} 609 610/** 611 * Process an individual element. 612 * 613 * Build the form element. When creating a form using FAPI #process, 614 * note that $element['#value'] is already set. 615 * 616 * The $fields array is in $form['#field_info'][$element['#field_name']]. 617 */ 618function nodereference_buttons_process($element, $edit, $form_state, $form) { 619 // The nodereference_select widget doesn't need to create its own 620 // element, it can wrap around the optionwidgets_select element. 621 // This will create a new, nested instance of the field. 622 // Add a validation step where the value can be unwrapped. 623 $field_key = $element['#columns'][0]; 624 $element[$field_key] = array( 625 '#type' => 'optionwidgets_buttons', 626 '#default_value' => isset($element['#value']) ? $element['#value'] : '', 627 // The following values were set by the content module and need 628 // to be passed down to the nested element. 629 '#title' => $element['#title'], 630 '#required' => $element['#required'], 631 '#description' => $element['#description'], 632 '#field_name' => $element['#field_name'], 633 '#type_name' => $element['#type_name'], 634 '#delta' => $element['#delta'], 635 '#columns' => $element['#columns'], 636 ); 637 if (empty($element[$field_key]['#element_validate'])) { 638 $element[$field_key]['#element_validate'] = array(); 639 } 640 array_unshift($element[$field_key]['#element_validate'], 'nodereference_optionwidgets_validate'); 641 return $element; 642} 643 644/** 645 * Process an individual element. 646 * 647 * Build the form element. When creating a form using FAPI #process, 648 * note that $element['#value'] is already set. 649 * 650 */ 651function nodereference_autocomplete_process($element, $edit, $form_state, $form) { 652 653 // The nodereference autocomplete widget doesn't need to create its own 654 // element, it can wrap around the text_textfield element and add an autocomplete 655 // path and some extra processing to it. 656 // Add a validation step where the value can be unwrapped. 657 $field_key = $element['#columns'][0]; 658 659 $element[$field_key] = array( 660 '#type' => 'text_textfield', 661 '#default_value' => isset($element['#value']) ? $element['#value'] : '', 662 '#autocomplete_path' => 'nodereference/autocomplete/'. $element['#field_name'], 663 // The following values were set by the content module and need 664 // to be passed down to the nested element. 665 '#title' => $element['#title'], 666 '#required' => $element['#required'], 667 '#description' => $element['#description'], 668 '#field_name' => $element['#field_name'], 669 '#type_name' => $element['#type_name'], 670 '#delta' => $element['#delta'], 671 '#columns' => $element['#columns'], 672 ); 673 if (empty($element[$field_key]['#element_validate'])) { 674 $element[$field_key]['#element_validate'] = array(); 675 } 676 array_unshift($element[$field_key]['#element_validate'], 'nodereference_autocomplete_validate'); 677 678 // Used so that hook_field('validate') knows where to flag an error. 679 $element['_error_element'] = array( 680 '#type' => 'value', 681 // Wrapping the element around a text_textfield element creates a 682 // nested element, so the final id will look like 'field-name-0-nid-nid'. 683 '#value' => implode('][', array_merge($element['#parents'], array($field_key, $field_key))), 684 ); 685 return $element; 686} 687 688/** 689 * Validate a select/buttons element. 690 * 691 * Remove the wrapper layer and set the right element's value. 692 * We don't know exactly where this element is, so we drill down 693 * through the element until we get to our key. 694 * 695 * We use $form_state['values'] instead of $element['#value'] 696 * to be sure we have the most accurate value when other modules 697 * like optionwidgets are using #element_validate to alter the value. 698 */ 699function nodereference_optionwidgets_validate($element, &$form_state) { 700 $field_key = $element['#columns'][0]; 701 702 $value = $form_state['values']; 703 $new_parents = array(); 704 foreach ($element['#parents'] as $parent) { 705 $value = $value[$parent]; 706 // Use === to be sure we get right results if parent is a zero (delta) value. 707 if ($parent === $field_key) { 708 $element['#parents'] = $new_parents; 709 form_set_value($element, $value, $form_state); 710 break; 711 } 712 $new_parents[] = $parent; 713 } 714} 715 716/** 717 * Validate an autocomplete element. 718 * 719 * Remove the wrapper layer and set the right element's value. 720 * This will move the nested value at 'field-name-0-nid-nid' 721 * back to its original location, 'field-name-0-nid'. 722 */ 723function nodereference_autocomplete_validate($element, &$form_state) { 724 $field_name = $element['#field_name']; 725 $type_name = $element['#type_name']; 726 $field = content_fields($field_name, $type_name); 727 $field_key = $element['#columns'][0]; 728 $delta = $element['#delta']; 729 $value = $element['#value'][$field_key]; 730 $nid = NULL; 731 if (!empty($value)) { 732 preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $value, $matches); 733 if (!empty($matches)) { 734 // Explicit [nid:n]. 735 list(, $title, $nid) = $matches; 736 if (!empty($title) && ($n = node_load($nid)) && trim($title) != trim($n->title)) { 737 form_error($element[$field_key], t('%name: title mismatch. Please check your selection.', array('%name' => t($field['widget']['label'])))); 738 } 739 } 740 else { 741 // No explicit nid. 742 $reference = _nodereference_potential_references($field, $value, 'equals', NULL, 1); 743 if (empty($reference)) { 744 form_error($element[$field_key], t('%name: found no valid post with that title.', array('%name' => t($field['widget']['label'])))); 745 } 746 else { 747 // TODO: 748 // the best thing would be to present the user with an additional form, 749 // allowing the user to choose between valid candidates with the same title 750 // ATM, we pick the first matching candidate... 751 $nid = key($reference); 752 } 753 } 754 } 755 form_set_value($element, $nid, $form_state); 756} 757 758/** 759 * Implementation of hook_allowed_values(). 760 */ 761function nodereference_allowed_values($field) { 762 $references = _nodereference_potential_references($field); 763 764 $options = array(); 765 foreach ($references as $key => $value) { 766 $options[$key] = $value['rendered']; 767 } 768 769 return $options; 770} 771 772/** 773 * Fetch an array of all candidate referenced nodes. 774 * 775 * This info is used in various places (allowed values, autocomplete results, 776 * input validation...). Some of them only need the nids, others nid + titles, 777 * others yet nid + titles + rendered row (for display in widgets). 778 * The array we return contains all the potentially needed information, and lets 779 * consumers use the parts they actually need. 780 * 781 * @param $field 782 * The field description. 783 * @param $string 784 * Optional string to filter titles on (used by autocomplete). 785 * @param $match 786 * Operator to match filtered name against, can be any of: 787 * 'contains', 'equals', 'starts_with' 788 * @param $ids 789 * Optional node ids to lookup (the $string and $match arguments will be 790 * ignored). 791 * @param $limit 792 * If non-zero, limit the size of the result set. 793 * 794 * @return 795 * An array of valid nodes in the form: 796 * array( 797 * nid => array( 798 * 'title' => The node title, 799 * 'rendered' => The text to display in widgets (can be HTML) 800 * ), 801 * ... 802 * ) 803 */ 804function _nodereference_potential_references($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) { 805 static $results = array(); 806 807 // Create unique id for static cache. 808 $cid = $field['field_name'] .':'. $match .':'. ($string !== '' ? $string : implode('-', $ids)) .':'. $limit; 809 if (!isset($results[$cid])) { 810 $references = FALSE; 811 if (module_exists('views') && !empty($field['advanced_view']) && $field['advanced_view'] != '--') { 812 $references = _nodereference_potential_references_views($field, $string, $match, $ids, $limit); 813 } 814 // If the view doesn't exist, we got FALSE, and fallback to the regular 'standard mode'. 815 816 if ($references === FALSE) { 817 $references = _nodereference_potential_references_standard($field, $string, $match, $ids, $limit); 818 } 819 820 // Store the results. 821 $results[$cid] = !empty($references) ? $references : array(); 822 } 823 824 return $results[$cid]; 825} 826 827/** 828 * Helper function for _nodereference_potential_references(): 829 * case of Views-defined referenceable nodes. 830 */ 831function _nodereference_potential_references_views($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) { 832 $view_name = $field['advanced_view']; 833 834 if ($view = views_get_view($view_name)) { 835 // We add a display, and let it derive from the 'default' display. 836 // TODO: We should let the user pick a display in the fields settings - sort of requires AHAH... 837 $display = $view->add_display('content_references'); 838 $view->set_display($display); 839 840 // TODO from merlinofchaos on IRC : arguments using summary view can defeat the style setting. 841 // We might also need to check if there's an argument, and set *its* style_plugin as well. 842 $view->display_handler->set_option('style_plugin', 'content_php_array_autocomplete'); 843 $view->display_handler->set_option('row_plugin', 'fields'); 844 // Used in content_plugin_style_php_array::render(), to get 845 // the 'field' to be used as title. 846 $view->display_handler->set_option('content_title_field', 'title'); 847 848 // Additional options to let content_plugin_display_references::query() 849 // narrow the results. 850 $options = array( 851 'table' => 'node', 852 'field_string' => 'title', 853 'string' => $string, 854 'match' => $match, 855 'field_id' => 'nid', 856 'ids' => $ids, 857 ); 858 $view->display_handler->set_option('content_options', $options); 859 860 // TODO : for consistency, a fair amount of what's below 861 // should be moved to content_plugin_display_references 862 863 // Limit result set size. 864 $limit = isset($limit) ? $limit : 0; 865 $view->display_handler->set_option('items_per_page', $limit); 866 867 // Get arguments for the view. 868 if (!empty($field['advanced_view_args'])) { 869 // TODO: Support Tokens using token.module ? 870 $view_args = array_map('trim', explode(',', $field['advanced_view_args'])); 871 } 872 else { 873 $view_args = array(); 874 } 875 876 // We do need title field, so add it if not present (unlikely, but...) 877 $fields = $view->get_items('field', $display); 878 if (!isset($fields['title'])) { 879 $view->add_item($display, 'field', 'node', 'title'); 880 } 881 882 // If not set, make all fields inline and define a separator. 883 $options = $view->display_handler->get_option('row_options'); 884 if (empty($options['inline'])) { 885 $options['inline'] = drupal_map_assoc(array_keys($view->get_items('field', $display))); 886 } 887 if (empty($options['separator'])) { 888 $options['separator'] = '-'; 889 } 890 $view->display_handler->set_option('row_options', $options); 891 892 // Make sure the query is not cached 893 $view->is_cacheable = FALSE; 894 895 // Get the results. 896 $result = $view->execute_display($display, $view_args); 897 } 898 else { 899 $result = FALSE; 900 } 901 902 return $result; 903} 904 905/** 906 * Helper function for _nodereference_potential_references(): 907 * referenceable nodes defined by content types. 908 */ 909function _nodereference_potential_references_standard($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) { 910 $related_types = array(); 911 $where = array(); 912 $args = array(); 913 914 if (is_array($field['referenceable_types'])) { 915 foreach (array_filter($field['referenceable_types']) as $related_type) { 916 $related_types[] = "n.type = '%s'"; 917 $args[] = $related_type; 918 } 919 } 920 921 $where[] = implode(' OR ', $related_types); 922 923 if (!count($related_types)) { 924 return array(); 925 } 926 927 if ($string !== '') { 928 $like = $GLOBALS["db_type"] == 'pgsql' ? "ILIKE" : "LIKE"; 929 $match_clauses = array( 930 'contains' => "$like '%%%s%%'", 931 'equals' => "= '%s'", 932 'starts_with' => "$like '%s%%'", 933 ); 934 $where[] = 'n.title '. (isset($match_clauses[$match]) ? $match_clauses[$match] : $match_clauses['contains']); 935 $args[] = $string; 936 } 937 elseif ($ids) { 938 $where[] = 'n.nid IN (' . db_placeholders($ids) . ')'; 939 $args = array_merge($args, $ids); 940 } 941 942 $where_clause = $where ? 'WHERE ('. implode(') AND (', $where) .')' : ''; 943 $sql = db_rewrite_sql("SELECT n.nid, n.title AS node_title, n.type AS node_type FROM {node} n $where_clause ORDER BY n.title, n.type"); 944 $result = $limit ? db_query_range($sql, $args, 0, $limit) : db_query($sql, $args); 945 $references = array(); 946 while ($node = db_fetch_object($result)) { 947 $references[$node->nid] = array( 948 'title' => $node->node_title, 949 'rendered' => check_plain($node->node_title), 950 ); 951 } 952 953 return $references; 954} 955 956/** 957 * Check access to the menu callback of the autocomplete widget. 958 * 959 * Check for both 'edit' and 'view' access in the unlikely event 960 * a user has edit but not view access. 961 */ 962function nodereference_autocomplete_access($field_name) { 963 return user_access('access content') && ($field = content_fields($field_name)) && isset($field['field_name']) && content_access('view', $field) && content_access('edit', $field); 964} 965 966/** 967 * Menu callback; Retrieve a pipe delimited string of autocomplete suggestions for existing users 968 */ 969function nodereference_autocomplete($field_name, $string = '') { 970 $fields = content_fields(); 971 $field = $fields[$field_name]; 972 $match = isset($field['widget']['autocomplete_match']) ? $field['widget']['autocomplete_match'] : 'contains'; 973 $matches = array(); 974 975 $references = _nodereference_potential_references($field, $string, $match, array(), 10); 976 foreach ($references as $id => $row) { 977 // Add a class wrapper for a few required CSS overrides. 978 $matches[$row['title'] ." [nid:$id]"] = '<div class="reference-autocomplete">'. $row['rendered'] . '</div>'; 979 } 980 drupal_json($matches); 981} 982 983/** 984 * Implementation of hook_node_types. 985 */ 986function nodereference_node_type($op, $info) { 987 switch ($op) { 988 case 'update': 989 // Reflect type name changes to the 'referenceable types' settings. 990 if (!empty($info->old_type) && $info->old_type != $info->type) { 991 // content.module's implementaion of hook_node_type() has already 992 // refreshed _content_type_info(). 993 $fields = content_fields(); 994 $rebuild = FALSE; 995 foreach ($fields as $field_name => $field) { 996 if ($field['type'] == 'nodereference' && isset($field['referenceable_types'][$info->old_type])) { 997 $field['referenceable_types'][$info->type] = empty($field['referenceable_types'][$info->old_type]) ? 0 : $info->type; 998 unset($field['referenceable_types'][$info->old_type]); 999 content_field_instance_update($field, FALSE); 1000 $rebuild = TRUE; 1001 } 1002 } 1003 1004 // Clear caches and rebuild menu only if any field has been updated. 1005 if ($rebuild) { 1006 content_clear_type_cache(TRUE); 1007 menu_rebuild(); 1008 } 1009 } 1010 break; 1011 } 1012} 1013 1014/** 1015 * Theme preprocess function. 1016 * 1017 * Allows specific node templates for nodes displayed as values of a 1018 * nodereference field with the 'full node' / 'teaser' formatters. 1019 */ 1020function nodereference_preprocess_node(&$vars) { 1021 // The 'referencing_field' attribute of the node is added by the 'teaser' 1022 // and 'full node' formatters. 1023 if (!empty($vars['node']->referencing_field)) { 1024 $node = $vars['node']; 1025 $field = $node->referencing_field; 1026 $vars['template_files'][] = 'node-nodereference'; 1027 $vars['template_files'][] = 'node-nodereference-'. $field['field_name']; 1028 $vars['template_files'][] = 'node-nodereference-'. $node->type; 1029 $vars['template_files'][] = 'node-nodereference-'. $field['field_name'] .'-'. $node->type; 1030 } 1031} 1032 1033/** 1034 * FAPI theme for an individual elements. 1035 * 1036 * The textfield or select is already rendered by the 1037 * textfield or select themes and the html output 1038 * lives in $element['#children']. Override this theme to 1039 * make custom changes to the output. 1040 * 1041 * $element['#field_name'] contains the field name 1042 * $element['#delta] is the position of this element in the group 1043 */ 1044function theme_nodereference_select($element) { 1045 return $element['#children']; 1046} 1047 1048function theme_nodereference_buttons($element) { 1049 return $element['#children']; 1050} 1051 1052function theme_nodereference_autocomplete($element) { 1053 return $element['#children']; 1054} 1055