array( 'label' => t('Menu link'), 'description' => t('This field stores a reference to a menu link in the database.'), 'settings' => array( 'link_path_field' => FALSE, ), 'instance_settings' => array( 'menu_options' => array('main-menu'), ), 'default_widget' => 'menu_link_default', 'default_formatter' => 'menu_link_link', ), ); } /** * Implements hook_field_settings_form(). */ function menu_link_field_settings_form($field, $instance, $has_data) { $settings = $field['settings']; $form = array(); /* @todo Allow menu link fields to be used as a reference field? http://drupal.org/node/1028344 $form['link_path_field'] = array( '#type' => 'checkbox', '#title' => t('Enable Path field'), '#default_value' => $settings['link_path_field'], '#description' => t('Allow users to set the path of menu links. When not checked the uri of the entity being edited is used.'), );*/ return $form; } /** * Implements hook_field_instance_settings_form(). */ function menu_link_field_instance_settings_form($field, $instance) { $settings = $instance['settings']; $form['menu_options'] = array( '#type' => 'checkboxes', '#title' => t('Available menus'), '#default_value' => $settings['menu_options'], '#options' => menu_get_menus(), '#description' => t('The menus available to place links in for this field.'), '#required' => TRUE, '#weight' => -5, ); return $form; } /** * Implements hook_field_load(). */ function menu_link_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) { foreach ($entities as $id => $entity) { foreach ($items[$id] as $delta => &$item) { $item['options'] = !empty($item['options']) ? unserialize($item['options']) : array(); $item['external'] = (!empty($item['link_path']) && $item['link_path'] != '' && url_is_external($item['link_path'])) ? 1 : 0; } } } /** * Implements hook_field_validate(). * * Possible error codes: * - 'menu_link_duplicate': A menu link can only be referenced once per entity * per field. * - 'menu_link_invalid_parent': Selected parent menu link does not exist or may * not be selected. * - 'menu_link_no_entity_uri': No link path is provided while entity being * edited has no URI of its own. */ function menu_link_field_validate($entity_type, $entity, $field, $instance, $langcode, &$items, &$errors) { if (!empty($entity)) { list($id, , ) = entity_extract_ids($entity_type, $entity); } // Build an array of existing menu link IDs so they can be loaded with // menu_link_load_multiple(); $mlids = array(); foreach ($items as $delta => &$item) { if (menu_link_field_is_empty($item, $field)) { continue; } if (!empty($item['mlid']) && (int)$item['mlid'] > 0) { $mlids[] = (int)$item['mlid']; } if (!empty($item['plid']) && (int)$item['plid'] > 0) { $mlids[] = (int)$item['plid']; } } $menu_links = !empty($mlids) ? menu_link_load_multiple($mlids) : array(); $mlids = array(); foreach ($items as $delta => &$item) { if (menu_link_field_is_empty($item, $field)) { continue; } if (empty($item['menu_name']) || !in_array($item['menu_name'], $instance['settings']['menu_options'])) { $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'menu_link_invalid_parent', 'message' => t('%name: Invalid menu name.', array('%name' => t($instance['label']))), ); } if (!empty($item['plid'])) { if (!isset($menu_links[$item['plid']]) || !in_array($menu_links[$item['plid']]['menu_name'], $instance['settings']['menu_options'])) { $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'menu_link_invalid_parent', 'message' => t('%name: Invalid parent menu link.', array('%name' => t($instance['label']))), ); } } if (!empty($item['mlid'])) { if (isset($mlids[$item['mlid']])) { $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'menu_link_duplicate', 'message' => t('%name: A menu link can only be referenced once per entity per field.', array('%name' => t($instance['label']))), ); } else { $mlids[$item['mlid']] = $item['mlid']; } } if (!empty($field['settings']['link_path_field']) && empty($item['link_path']) && !empty($id)) { $uri = entity_uri($entity_type, $entity); if ($uri === NULL) { $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'menu_link_no_entity_uri', 'message' => t('%name: No link path is provided while entity being edited has no URI of its own.', array('%name' => t($instance['label']))), ); } } } } /** * Implements hook_field_presave(). * * @see menu_link_menu_link_update() */ function menu_link_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) { foreach ($items as $delta => &$item) { if (empty($item['plid'])) { $item['plid'] = 0; } $item += array( 'options' => array(), 'hidden' => 0, 'expanded' => 0, 'weight' => 0, ); // Add a key to indicate we are saving menu links from within a field. This // key is not stored in the database and will only be available during the // current request. // @see menu_link_menu_link_update() $item['menu_link_field_save'] = $field['field_name']; // TODO This could override the module column! $item['module'] = 'menu_link'; } } /** * Implements hook_field_insert(). */ function menu_link_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) { $path = _menu_link_path($entity_type, $entity, $langcode); foreach ($items as $delta => &$item) { if (empty($field['settings']['link_path_field']) || empty($item['link_path'])) { $item['link_path'] = $path; } if (!menu_link_save($item)) { drupal_set_message(t('There was an error saving the menu link.'), 'error'); } $item['options'] = serialize($item['options']); } } /** * Implements hook_field_update(). */ function menu_link_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) { list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); // Load the field items as they are stored in the database before update. $original = entity_create_stub_entity($entity_type, array($id, $vid, $bundle)); field_attach_load($entity_type, array($id => $original), FIELD_LOAD_CURRENT, $options = array('field_id' => $field['id'])); // Initially asume that all links are being deleted; later on in this function, // links that are kept are removed from this array. $delete_links = array(); if (!empty($original->{$instance['field_name']}[$langcode])) { foreach($original->{$instance['field_name']}[$langcode] as $item) { $delete_links[$item['mlid']] = $item['mlid']; } } $path = _menu_link_path($entity_type, $entity, $langcode); foreach ($items as $delta => &$item) { if (empty($field['settings']['link_path_field']) || empty($item['link_path'])) { $item['link_path'] = $path; } if (!menu_link_save($item)) { drupal_set_message(t('There was an error saving the menu link.'), 'error'); // TODO what to do? } $item['options'] = serialize($item['options']); // Don't remove menu links that are being kept. unset($delete_links[$item['mlid']]); } // Delete any menu links that are no longer used. if (!empty($delete_links)) { menu_link_delete_multiple($delete_links); } } /** * Helper function to build a menu link path based on an entity. */ function _menu_link_path($entity_type, $entity, $langcode) { $uri = entity_uri($entity_type, $entity); if (url_is_external($uri['path'])) { $path = url($uri['path'], $uri['options']); } else { $path = drupal_get_normal_path($uri['path'], $langcode); if (!empty($uri['options']['query'])) { $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($uri['options']['query']); } if (!empty($uri['options']['fragment'])) { $path .= '#' . $uri['options']['fragment']; } } return $path; } /** * Implements hook_field_delete(). */ function menu_link_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) { $mlids = array(); foreach ($items as $delta => $item) { $mlids[] = $item['mlid']; } if (!empty($mlids)) { // Only delete menu links that are (still) owned by the Menu link module. $mlids = db_select('menu_links') ->fields('menu_links', array('mlid')) ->condition('module', 'menu_link') ->condition('mlid', $mlids) ->execute() ->fetchCol(); if (!empty($mlids)) { menu_link_delete_multiple($mlids); } } } /** * Implements hook_field_is_empty(). */ function menu_link_field_is_empty($item, $field) { if (!empty($item['delete']) || !trim($item['link_title']) || empty($item['menu_name']) || (empty($item['plid']) && $item['plid'] != '0')) { return TRUE; } return FALSE; } /** * Implements hook_field_formatter_info(). */ function menu_link_field_formatter_info() { return array( 'menu_link_link' => array( 'label' => t('Link'), 'field types' => array('menu_link'), ), ); } /** * Implements hook_field_formatter_prepare_view(). */ function menu_link_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) { $mlids = array(); // Collect every possible menu link attached to any of the fieldable entities. foreach ($entities as $id => $entity) { foreach ($items[$id] as $delta => $item) { // Prevent doing things twice. if (empty($item['title'])) { // Force the array key to prevent duplicates. $mlids[$item['mlid']] = $item['mlid']; } } } if (!empty($mlids)) { $menu_links = menu_link_load_multiple($mlids); // Iterate through the fieldable entities again to attach the loaded menu // link data. foreach ($entities as $id => $entity) { $rekey = FALSE; foreach ($items[$id] as $delta => $item) { // Check whether the menu link field instance value could be loaded. if (isset($menu_links[$item['mlid']])) { // Replace the instance value with the menu link data. $items[$id][$delta] = $menu_links[$item['mlid']]; } // Otherwise, unset the instance value, since the menu link does not exist. else { unset($items[$id][$delta]); $rekey = TRUE; } } if ($rekey) { // Rekey the items array. $items[$id] = array_values($items[$id]); } } } } /** * Implements hook_field_formatter_view(). */ function menu_link_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { $element = array(); // If the default formatter is set to "hidden" but the field is being // displayed using this formatter, some module don't properly invoke // hook_field_formatter_prepare_view() which is essential for this formatter. // Let's make sure it has been invoked. // TODO remove if resolved. if ($instance['display']['default']['type'] == 'hidden') { list($id, , ) = entity_extract_ids($entity_type, $entity); $items = array($id => &$items); menu_link_field_formatter_prepare_view( $entity_type, array($id => $entity), $field, array($id => $instance), $langcode, $items, array($id => $display) ); $items = $items[$id]; } switch ($display['type']) { case 'menu_link_link': foreach ($items as $delta => $item) { $element[$delta] = array( '#type' => 'link', '#title' => $item['title'], '#href' => $item['href'], '#options' => $item['localized_options'], ); } break; } return $element; } /** * Implements hook_field_widget_info(). */ function menu_link_field_widget_info() { return array( 'menu_link_default' => array( 'label' => t('Select list'), 'field types' => array('menu_link'), 'settings' => array( 'description_field' => TRUE, 'expanded_field' => FALSE ), ), ); } /** * Implements hook_field_widget_settings_form(). */ function menu_link_field_widget_settings_form($field, $instance) { $widget = $instance['widget']; $settings = $widget['settings']; $form['description_field'] = array( '#type' => 'checkbox', '#title' => t('Enable Description field'), '#default_value' => $settings['description_field'], '#description' => t('The description field is used as a tooltip when the mouse hovers over the menu link.'), '#weight' => 5, ); $form['expanded_field'] = array( '#type' => 'checkbox', '#title' => t('Enable Expanded field'), '#default_value' => $settings['expanded_field'], '#description' => t('The expanded field is a checkbox which, if selected and the menu link has children, makes the menu link always appear expanded.'), '#weight' => 5, ); return $form; } /** * Implements hook_field_widget_form(). */ function menu_link_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { $module_path = drupal_get_path('module', 'menu_link'); $element += array( '#input' => TRUE, '#type' => ($field['cardinality'] == 1) ? 'fieldset' : 'container', '#element_validate' => array('menu_link_field_widget_validate'), '#attached' => array( 'js' => array($module_path . '/menu_link.field.js'), 'css' => array($module_path . '/menu_link.field.css'), ), '#attributes' => array( 'class' => array('menu-link-item', 'menu-link-auto-title', 'menu-link-auto-fieldset-summary'), ) ); // Populate the element with the link data. foreach (array('mlid', 'link_path', 'options', 'hidden', 'expanded') as $key) { if (isset($items[$delta][$key])) { $element[$key] = array('#type' => 'value', '#value' => $items[$delta][$key]); } } $menus = menu_get_menus(); $available_menus = array_combine($instance['settings']['menu_options'], $instance['settings']['menu_options']); $options = _menu_get_options($menus, $available_menus, array('mlid' => 0)); $element['parent'] = array( '#type' => 'select', '#title' => t('Parent menu item'), '#options' => $options, '#empty_value' => '_none', '#attributes' => array( 'class' => array('menu-link-item-parent'), 'title' => t('Choose the menu item to be the parent for this link.'), ), '#required' => !empty($element['#required']), ); if (isset($items[$delta]['menu_name'], $items[$delta]['plid'])) { $element['parent']['#default_value'] = $items[$delta]['menu_name'] . ':' . $items[$delta]['plid']; } $element['weight'] = array( '#type' => 'weight', '#title' => t('Weight'), '#delta' => 50, '#default_value' => isset($items[$delta]['weight']) ? $items[$delta]['weight'] : 0, '#attributes' => array( 'class' => array('menu-link-item-weight'), 'title' => t('Menu links with smaller weights are displayed before links with larger weights.'), ), ); $element['link_title'] = array( '#type' => 'textfield', '#title' => t('Title'), '#default_value' => isset($items[$delta]['link_title']) ? $items[$delta]['link_title'] : '', '#size' => 50, '#maxlength' => 255, '#attributes' => array( 'class' => array('menu-link-item-title'), ), '#description' => t('The text to be used for this link in the menu.'), '#required' => !empty($element['#required']), ); if (!empty($field['settings']['link_path_field'])) { $element['link_path'] = array( '#type' => 'textfield', '#title' => t('Path'), '#default_value' => isset($items[$delta]['link_path']) ? $items[$delta]['link_path'] : '', '#size' => 50, '#maxlength' => 255, '#attributes' => array( 'class' => array('menu-link-item-path'), ), '#description' => t('The path for this menu link.'), ); } if (!empty($instance['widget']['settings']['fragment_field'])) { $element['fragment'] = array( '#type' => 'textfield', '#title' => t('URL Fragment'), '#field_prefix' => '#', '#default_value' => isset($items[$delta]['options']['attributes']['fragment']) ? $items[$delta]['options']['attributes']['fragment'] : '', '#size' => 10, '#maxlength' => 255, '#attributes' => array( 'class' => array('menu-link-item-fragment'), ), ); } if (!empty($instance['widget']['settings']['expanded_field'])) { $element['expanded'] = array( '#type' => 'checkbox', '#title' => t('Expanded'), '#title_display' => 'before', '#default_value' => isset($items[$delta]['expanded']) ? $items[$delta]['expanded'] : 0, '#attributes' => array( 'class' => array('menu-link-item-expanded'), 'title' => t('If selected and this menu link has children, the menu will always appear expanded.'), ), '#weight' => 5, ); } if (!empty($instance['widget']['settings']['description_field'])) { $element['description'] = array( '#type' => 'textarea', '#title' => t('Description'), '#default_value' => isset($items[$delta]['options']['attributes']['title']) ? $items[$delta]['options']['attributes']['title'] : '', '#rows' => 1, '#description' => t('Shown when hovering over the menu link.'), '#attributes' => array('class' => array('menu-link-item-description')), '#weight' => 10, ); } return $element; } /** * Form element validate handler for menu link field widget. */ function menu_link_field_widget_validate($element, &$form_state) { $item = drupal_array_get_nested_value($form_state['values'], $element['#parents']); if (!empty($item['parent']) && $item['parent'] != '_none') { list($item['menu_name'], $item['plid']) = explode(':', $item['parent']); } $item['link_title'] = trim($item['link_title']); if (!empty($item['description']) && trim($item['description'])) { $item['options']['attributes']['title'] = trim($item['description']); } else { // If the description field was left empty, remove the title attribute // from the menu link. unset($item['options']['attributes']['title']); } if (!empty($item['fragment']) && trim($item['fragment'])) { $item['options']['fragment'] = trim($item['fragment']); } else { unset($item['options']['fragment']); } form_set_value($element, $item, $form_state); } /** * Implements hook_field_widget_error(). */ function menu_link_field_widget_error($element, $error, $form, &$form_state) { switch ($error['error']) { case 'menu_link_invalid_parent': $error_element = $element['parent']; break; default: $error_element = $element['link_title']; break; } form_error($error_element, $error['message']); }