You are here

multiple_node_menu.module in Multiple Node Menu 7

Same filename and directory in other branches
  1. 8 multiple_node_menu.module
  2. 6 multiple_node_menu.module

Add multiple menu management capabilities to node form.

File

multiple_node_menu.module
View source
<?php

/**
 * @file
 * Add multiple menu management capabilities to node form.
 */

/**
 * Implements hook_module_implements_alter().
 *
 * Prevent Menu module from adding a menu link form to node forms.
 */
function multiple_node_menu_module_implements_alter(&$implementations, $hook) {

  // Only 'hook_node_delete' implementation is useful for us.
  if (in_array($hook, array(
    'node_insert',
    'node_update',
    'node_prepare',
    'form_node_form_alter',
    'node_submit',
  ))) {
    unset($implementations['menu']);
  }
}

/**
 * Implements hook_node_insert().
 */
function multiple_node_menu_node_insert($node) {
  multiple_node_menu_save($node);
}

/**
 * Implements hook_node_update().
 */
function multiple_node_menu_node_update($node) {
  multiple_node_menu_save($node);
}

/**
 * Helper function to save menu items. Used on hook_node_insert() and
 * hook_node_update().
 *
 * @param $node
 *   Node object to save menu links for.
 */
function multiple_node_menu_save($node) {
  if (!empty($node->multiple_node_menu_links_deleted)) {
    foreach ($node->multiple_node_menu_links_deleted as $deleted_link) {
      menu_link_delete($deleted_link);
    }
  }
  if (empty($node->multiple_node_menu_links)) {
    return;
  }
  foreach ($node->multiple_node_menu_links as $delta => &$link) {
    if (!empty($link['link_title'])) {

      // Set path to node being saved.
      $link['link_path'] = "node/{$node->nid}";

      // If 'Enabled' checkbox was ticked. Set hidden to FALSE and vice-versa.
      if (isset($link['enabled'])) {
        $link['hidden'] = (int) (!$link['enabled']);
      }
      if (!empty($link['description'])) {
        $link['options']['attributes']['title'] = $link['description'];
      }
      else {
        unset($link['options']['attributes']['title']);
      }

      // Add support for i18n_menu and other language aware modules. Set link
      // language to node language.
      $link['language'] = empty($node->language) ? LANGUAGE_NONE : $node->language;

      // Actually save the link.
      if (!menu_link_save($link)) {

        // @todo Not sure in what situations we should get here, but it doesn't
        // look quite right to display error mensages on a loop like this.
        drupal_set_message(t('There was an error saving the menu link.'), 'error');
      }
    }
  }
}

/**
 * Implements hook_node_prepare().
 */
function multiple_node_menu_node_prepare($node) {
  if (empty($node->multiple_node_menu_links) && !empty($node->nid)) {
    $node->multiple_node_menu_links = _multiple_node_menu_load($node);
  }
  if (empty($node->multiple_node_menu_links)) {
    $node->multiple_node_menu_links = array();
  }

  // Initialize array that holds deleted items.
  if (empty($node->multiple_node_menu_links_deleted)) {
    $node->multiple_node_menu_links_deleted = array();
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 *
 * Add multiple menu items management capability to node form.
 */
function multiple_node_menu_form_node_form_alter(&$form, &$form_state, $form_id) {
  $node = $form['#node'];

  // Don't add the multiple node menu form when the content type has no
  // available menus assigned to it.
  if (!variable_get('menu_options_' . $node->type, FALSE)) {
    return;
  }
  $form['multiple_node_menu'] = array(
    '#type' => 'fieldset',
    '#title' => t('Menu settings'),
    '#access' => user_access('administer menu'),
    '#collapsible' => TRUE,
    '#collapsed' => !empty($node->multiple_node_menu_links),
    '#group' => 'additional_settings',
    '#attached' => array(
      'js' => array(
        drupal_get_path('module', 'menu') . '/menu.js',
      ),
    ),
    '#tree' => TRUE,
    '#weight' => -2,
    '#attributes' => array(
      'class' => array(
        'menu-link-form',
      ),
    ),
  );

  // Check if the Menu Admin per Menu module is installed on the site. If so,
  // use its finer access settings.
  if (module_exists('menu_admin_per_menu')) {
    $form['multiple_node_menu']['#access'] = _menu_admin_per_menu_access();
  }

  // Check if there's at least one enabled link for setting the 'Provide a menu
  // link' checkbox enabled by default.
  $enabled = FALSE;
  foreach ($node->multiple_node_menu_links as $link) {
    if (empty($link['hidden'])) {
      $enabled = TRUE;
      break;
    }
  }
  $form['multiple_node_menu']['enabled'] = array(
    '#type' => 'checkbox',
    '#title' => t('Provide a menu link'),
    '#default_value' => $enabled,
  );
  $form['multiple_node_menu']['link'] = array(
    '#type' => 'container',
    '#parents' => array(
      'multiple_node_menu',
    ),
    '#states' => array(
      'invisible' => array(
        'input[name="multiple_node_menu[enabled]"]' => array(
          'checked' => FALSE,
        ),
      ),
    ),
    '#prefix' => '<div id="multiple-node-menu-wrapper">',
    '#suffix' => '</div>',
  );
  if (!empty($node->multiple_node_menu_links)) {
    $form['multiple_node_menu']['link']['current_links'] = multiple_node_menu_current_links_form($node);
    $form['multiple_node_menu']['link']['current_links']['#prefix'] = '<h3>' . t('Current menu links') . '</h3>';
  }
  $form['multiple_node_menu']['link']['add_link'] = multiple_node_menu_link_form($node);
  $form['multiple_node_menu']['link']['add_link']['#prefix'] = '<h3>' . t('Add new menu link') . '</h3>';
  $form['multiple_node_menu']['link']['add_link']['add_link_submit'] = array(
    '#type' => 'submit',
    '#value' => t('Add new menu link'),
    '#submit' => array(
      'multiple_node_menu_add_link_submit',
    ),
    // Restrict validation to appropriate fields only.
    '#limit_validation_errors' => array(
      array(
        'multiple_node_menu',
        'add_link',
      ),
    ),
    '#ajax' => array(
      'callback' => 'multiple_node_menu_ajax_callback',
      'wrapper' => 'multiple-node-menu-wrapper',
    ),
  );
}

/**
 * Generate a form for editing a menu links.
 */
function multiple_node_menu_current_links_form($node) {
  $form = array(
    '#tree' => TRUE,
    '#theme' => 'multiple_node_menu_current_links',
  );
  foreach ($node->multiple_node_menu_links as $delta => $link) {
    $form[$delta] = multiple_node_menu_link_form($node, $link, TRUE);
    $form[$delta]['enabled'] = array(
      '#type' => 'checkbox',
      '#title' => t('Enabled'),
      '#default_value' => !$link['hidden'],
    );
    $form[$delta]['remove'] = array(
      '#type' => 'submit',
      '#value' => t('Delete'),
      '#name' => 'remove_' . $delta,
      '#submit' => array(
        'multiple_node_menu_remove_link_submit',
      ),
      // Bypass validation for 'Delete' button.
      '#limit_validation_errors' => array(),
      '#ajax' => array(
        'callback' => 'multiple_node_menu_ajax_callback',
        'wrapper' => 'multiple-node-menu-wrapper',
      ),
    );
  }
  return $form;
}

/**
 * Generate a form for adding/editing a menu link.
 */
function multiple_node_menu_link_form($node, $link = array(), $edit = FALSE) {

  // Set default values.
  _multiple_node_menu_set_defaults($node, $link);

  // menu_parent_options() is goofy and can actually handle either a menu link
  // or a node type both as second argument. Pick based on whether there is
  // a link already.
  $options = menu_parent_options(menu_get_menus(), $node->type);

  // If no possible parent menu items were found, there is nothing to display.
  if (empty($options)) {
    return;
  }
  $form = array();

  // Populate the element with the link data.
  foreach (array(
    'mlid',
    'module',
    'hidden',
    'has_children',
    'customized',
    'options',
    'expanded',
    'hidden',
    'parent_depth_limit',
  ) as $key) {
    $form[$key] = array(
      '#type' => 'value',
      '#value' => $link[$key],
    );
  }
  $form['link_title'] = array(
    '#type' => 'textfield',
    '#title' => t('Menu link title'),
    '#default_value' => $link['link_title'],
  );
  $form['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => !empty($link['description']) ? $link['description'] : '',
    '#rows' => 1,
    '#description' => t('Shown when hovering over the menu link.'),
  );
  $default = !empty($link['mlid']) ? $link['menu_name'] . ':' . $link['plid'] : variable_get('menu_parent_' . $node->type, 'main-menu:0');

  // If the current parent menu item is not present in options, use the first
  // available option as default value.
  // @todo User should not be allowed to access menu link settings in such a
  // case.
  if (!isset($options[$default])) {
    $array = array_keys($options);
    $default = reset($array);
  }
  $form['parent'] = array(
    '#type' => 'select',
    '#title' => t('Parent item'),
    '#default_value' => $default,
    '#options' => $options,
  );
  $form['weight'] = array(
    '#type' => 'weight',
    '#title' => t('Weight'),
    '#delta' => 50,
    '#default_value' => $link['weight'],
    '#description' => t('Menu links with smaller weights are displayed before links with larger weights.'),
  );
  return $form;
}

/**
 * Implements hook_node_submit().
 *
 * @see multiple_node_menu_form_node_form_alter()
 */
function multiple_node_menu_node_submit($node, $form, &$form_state) {
  if (!variable_get('menu_options_' . $node->type, FALSE)) {
    return;
  }

  // Include 'Add new menu link' form values if it was set.
  _multiple_node_menu_add_link($form_state);
  $values = $form_state['values']['multiple_node_menu'];
  if (!empty($values['current_links'])) {
    foreach ($values['current_links'] as $delta => $link) {
      if (!empty($link['parent'])) {

        // Decompose the selected menu parent option into 'menu_name' and 'plid',
        // if the form used the default parent selection widget.
        list($link['menu_name'], $link['plid']) = explode(':', $link['parent']);
      }

      // If 'Provide menu link' checkbox is unchecked. Mark all items disabled.
      if (empty($values['enabled'])) {
        $link['enabled'] = FALSE;
      }

      // Update all links to node object.
      $node->multiple_node_menu_links[$delta] = $link;
    }
  }
}

/**
 * Ajax callback for multiple menu link forms.
 */
function multiple_node_menu_ajax_callback($form, $form_state) {

  // Clear fields for link specific values. The other fields will default to
  // previous entered values.
  $form['multiple_node_menu']['link']['add_link']['link_title']['#value'] = '';
  $form['multiple_node_menu']['link']['add_link']['description']['#value'] = '';
  return $form['multiple_node_menu']['link'];
}

/**
 * Implements hook_theme().
 */
function multiple_node_menu_theme() {
  return array(
    'multiple_node_menu_current_links' => array(
      'render element' => 'element',
    ),
  );
}

/**
 * Theme current menu links table.
 */
function theme_multiple_node_menu_current_links($variables) {
  $form = $variables['element'];
  $rows = array();
  foreach (element_children($form) as $key) {

    // Unset field titles and descriptions.
    foreach (array(
      'link_title',
      'description',
      'parent',
      'weight',
    ) as $field_name) {
      unset($form[$key][$field_name]['#title'], $form[$key][$field_name]['#description']);
    }
    $rows[] = array(
      drupal_render($form[$key]['link_title']) . drupal_render($form[$key]['description']),
      drupal_render($form[$key]['parent']),
      drupal_render($form[$key]['weight']),
      drupal_render($form[$key]['enabled']),
      drupal_render($form[$key]['remove']),
    );
  }
  $headers = array(
    t('Menu link title and description'),
    t('Parent'),
    t('Weight'),
    t('Enabled'),
    t('Operations'),
  );
  return theme('table', array(
    'header' => $headers,
    'rows' => $rows,
    'attributes' => array(
      'id' => 'multiple-node-menu-current-links',
    ),
  ));
}

/**
 * Submit handler for 'Add menu link' button.
 */
function multiple_node_menu_add_link_submit($form, &$form_state) {
  if (_multiple_node_menu_add_link($form_state)) {

    // Cause the form to rebuild so the new items show up.
    $form_state['rebuild'] = TRUE;
  }
}

/**
 * Submit handler for "remove" button.
 */
function multiple_node_menu_remove_link_submit($form, &$form_state) {
  $values =& $form_state['values'];
  $node =& $form_state['node'];
  $delta = key($values['multiple_node_menu']['current_links']);
  $link = $node->multiple_node_menu_links[$delta];

  // Set link to be deleted from database.
  if (!empty($link['mlid'])) {
    $node->multiple_node_menu_links_deleted[] = $link['mlid'];
  }

  // Unset the item so it get removed from the form.
  unset($node->multiple_node_menu_links[$delta]);

  // Cause the form to rebuild so the item is removed from the list.
  $form_state['rebuild'] = TRUE;
}

/**
 * Determines whether a new menu item should be added based on a node_form form
 * state.
 *
 * @param $form_state
 *   Form state passed from Node form.
 * @return
 *   Whether or not a new menu item was added.
 */
function _multiple_node_menu_add_link($form_state) {
  $values =& $form_state['values']['multiple_node_menu']['add_link'];
  if (!empty($values['link_title'])) {
    $node =& $form_state['node'];

    // Decompose parent into 'menu_name' and 'plid'.
    if (!empty($values['parent'])) {
      list($values['menu_name'], $values['plid']) = explode(':', $values['parent']);
    }

    // Append new added link to node object.
    _multiple_node_menu_set_defaults($node, $values);
    $node->multiple_node_menu_links[] = $values;

    // Menu link was added.
    return TRUE;
  }

  // No menu link was added.
  return FALSE;
}

/**
 * Set default values to menu link array().
 */
function _multiple_node_menu_set_defaults($node, &$link) {
  $menu_name = strtok(variable_get('menu_parent_' . $node->type, 'main-menu:0'), ':');
  $link += array(
    'link_title' => '',
    'mlid' => 0,
    'plid' => 0,
    'menu_name' => $menu_name,
    'weight' => 0,
    'options' => array(),
    'module' => 'menu',
    'expanded' => 0,
    'hidden' => 0,
    'has_children' => 0,
    'customized' => 0,
  );

  // Find the depth limit for the parent select.
  if (!isset($link['parent_depth_limit'])) {
    $link['parent_depth_limit'] = _menu_parent_depth_limit($link);
  }
}

/**
 * Load all menu items associated with a node.
 *
 * @param $node
 *   The source path to look up.
 *
 * @return
 *   Array of paths or NULL if none found.
 */
function _multiple_node_menu_load($node) {
  $type_menus = variable_get('menu_options_' . $node->type, array(
    'main-menu' => 'main-menu',
  ));
  if (empty($type_menus)) {
    return NULL;
  }
  $links = db_select('menu_links')
    ->condition('module', 'menu')
    ->condition('router_path', 'node/%')
    ->condition('link_path', 'node/' . $node->nid)
    ->condition('menu_name', $type_menus, 'IN')
    ->fields('menu_links')
    ->execute()
    ->fetchAll(PDO::FETCH_ASSOC);
  foreach ($links as &$link) {

    // Unserialize options array.
    $link['options'] = unserialize($link['options']);

    // Set values used in the form.
    if (!empty($link['options']['attributes']['title'])) {
      $link['description'] = $link['options']['attributes']['title'];
    }
    $link['enabled'] = !$link['hidden'];
  }
  return $links;
}

Functions

Namesort descending Description
multiple_node_menu_add_link_submit Submit handler for 'Add menu link' button.
multiple_node_menu_ajax_callback Ajax callback for multiple menu link forms.
multiple_node_menu_current_links_form Generate a form for editing a menu links.
multiple_node_menu_form_node_form_alter Implements hook_form_BASE_FORM_ID_alter().
multiple_node_menu_link_form Generate a form for adding/editing a menu link.
multiple_node_menu_module_implements_alter Implements hook_module_implements_alter().
multiple_node_menu_node_insert Implements hook_node_insert().
multiple_node_menu_node_prepare Implements hook_node_prepare().
multiple_node_menu_node_submit Implements hook_node_submit().
multiple_node_menu_node_update Implements hook_node_update().
multiple_node_menu_remove_link_submit Submit handler for "remove" button.
multiple_node_menu_save Helper function to save menu items. Used on hook_node_insert() and hook_node_update().
multiple_node_menu_theme Implements hook_theme().
theme_multiple_node_menu_current_links Theme current menu links table.
_multiple_node_menu_add_link Determines whether a new menu item should be added based on a node_form form state.
_multiple_node_menu_load Load all menu items associated with a node.
_multiple_node_menu_set_defaults Set default values to menu link array().