You are here

menu_link_weight.module in Menu Link Weight 7

Same filename and directory in other branches
  1. 8.2 menu_link_weight.module
  2. 8 menu_link_weight.module

Replaces the menu link weight dropdown with a tabledrag widget.

File

menu_link_weight.module
View source
<?php

/**
 * @file
 * Replaces the menu link weight dropdown with a tabledrag widget.
 */

/**
 * Include functionality related to reordering of options by other modules.
 */
require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'menu_link_weight') . '/menu_link_weight.reorder.inc';

/**
 * Minimum weight of a menu link. In Drupal core this is currently -50.
 */
define('MENU_LINK_WEIGHT_MIN_DELTA', -50);

/**
 * Maximum weight of a menu link. In Drupal core this is currently 50.
 */
define('MENU_LINK_WEIGHT_MAX_DELTA', 50);

/**
 * Implements hook_form_FORM_ID_alter().
 */
function menu_link_weight_form_node_form_alter(&$form, &$form_state) {
  if (!isset($form['menu']) || !isset($form['menu']['link']) || isset($form['menu']['#access']) && $form['menu']['#access'] === FALSE || isset($form['menu']['link']['parent']['#access']) && $form['menu']['link']['parent']['#access'] === FALSE) {

    // If there is no menu, or the user doesn't have access to the
    // menu link options, then do nothing.
    return;
  }

  // Remove the "weight" widget.
  unset($form['menu']['link']['weight']);

  // Add submission/validation handlers.
  $form['#validate'][] = 'menu_link_weight_node_form_validate';
  $form['#submit'][] = 'menu_link_weight_node_form_submit';

  // Add the Menu Link weight fieldset.
  $form['menu']['link']['menu_link_weight'] = array(
    '#tree' => TRUE,
    '#type' => 'fieldset',
    '#collapsible' => FALSE,
    '#title' => t('Menu link weight'),
    '#theme' => 'menu_link_weight_reorder_element',
    '#prefix' => '<div id="menu-link-weight-wrapper">',
    '#suffix' => '</div>',
    // Tell the fieldset process function to process this field by setting
    // the #menu_link_weight_process flag to TRUE.
    '#menu_link_weight_process' => TRUE,
  );

  // Define the "db_weights" element, which will hold hidden fields with
  // the values of the menu links in the database. Upon validation we will
  // check whether the weights are still the same as when the form was
  // built, to make sure users won't overwrite each other's changes.
  $form['menu']['link']['db_weights'] = array(
    '#tree' => TRUE,
  );
  $form['#attached']['js'][] = drupal_get_path('module', 'menu_link_weight') . '/js/menu_link_weight.js';
  if (module_exists('hierarchical_select')) {
    $form['#attached']['js'][] = drupal_get_path('module', 'menu_link_weight') . '/js/menu_link_weight.hierarchical_select.js';
  }

  // Define the AJAX callback for changes in the Parent element.
  $form['menu']['link']['parent']['#ajax'] = array(
    'callback' => 'menu_link_weight_parent_ajax_callback',
    'wrapper' => 'menu-link-weight-wrapper',
  );

  // This next button will not be displayed if JS is enabled.
  $form['menu']['link']['menu_link_weight_nojs'] = array(
    '#type' => 'submit',
    '#value' => menu_link_weight_get_button_text(),
    '#prefix' => '<noscript>',
    '#suffix' => '</noscript>',
    // No need to validate when submitting this.
    '#limit_validation_errors' => array(),
    '#validate' => array(),
    '#submit' => array(
      'menu_link_weight_node_form_submit',
    ),
  );
}

/**
 * Implements hook_form_FORM_ID_alter() for menu_overview_form().
 *
 * Adds an anchor tag so we can link to a menu item directly from the node form.
 *
 * @see menu_link_weight_node_element_process
 */
function menu_link_weight_form_menu_overview_form_alter(&$form, &$form_state) {
  foreach (element_children($form) as $element) {
    if (substr($element, 0, 5) == 'mlid:') {
      $mlid = $form[$element]['#item']['mlid'];

      // Add an offset of 60 pixels to the anchor tag so it doesn't "headbutt"
      // the top of the screen.
      $html = '<span style="display: inline-block; margin-top: -60px; padding-top: 60px;" id="menu-link-weight-mlid-' . $mlid . '"></span>';
      $form[$element]['title']['#prefix'] = isset($form[$element]['title']['#prefix']) ? $form[$element]['title']['#prefix'] . $html : $html;
    }
  }
}

/**
 * Gets the name for the "No-Javascript" button we want to use.
 *
 * @return string
 *   Translated text to show on the button.
 */
function menu_link_weight_get_button_text() {
  return t('Change parent (update list of weights)');
}

/**
 * AJAX callback that returns the updated menu_link_weight element.
 *
 * This will update whenever the selection of the menu link parent changes.
 */
function menu_link_weight_parent_ajax_callback($form, $form_state) {
  return $form['menu']['link']['menu_link_weight'];
}

/**
 * Implements hook_element_info_alter().
 *
 * @see https://drupal.stackexchange.com/questions/49216/how-to-add-a-process-callback-to-a-fieldset-form-element-without-overwriting-th
 */
function menu_link_weight_element_info_alter(&$types) {

  // Set the Menu link weight process function all fieldsets, but check
  // within the process function whether we are editing a Menu Link Weight
  // element.
  $types['fieldset']['#process'][] = 'menu_link_weight_node_element_process';
}

/**
 * Process callback for the menu link weight element.
 */
function menu_link_weight_node_element_process($element, &$form_state, &$complete_form) {
  if (empty($element['#menu_link_weight_process'])) {
    return $element;
  }
  $options = array();

  // Find out which parent to select after loading (or AJAX reloading) the form.
  $parent_element = $complete_form['menu']['link']['parent'];
  $parent_value = _menu_link_weight_get_parent_value_from_element($parent_element, $form_state);
  if (strstr($parent_value, ':')) {
    list($menu_name, $plid) = explode(':', $parent_value);
  }
  else {
    return $element;
  }

  // Get the menu title for the parent based on the current menu selection.
  if ($plid == 0) {
    $menu = menu_load($menu_name);
    $parent_menu_link = l($menu['title'], 'admin/structure/menu/manage/' . $menu_name);
  }
  else {
    $link = menu_link_load($plid);
    $parent_menu_link = l($link['link_title'], 'admin/structure/menu/manage/' . $menu_name, array(
      'attributes' => array(
        'target' => '_blank',
      ),
      'fragment' => 'menu-link-weight-mlid-' . $plid,
      'html' => TRUE,
    ));
  }
  $element['#description'] = t('Change the weight of the links within the !parent_menu_link menu by dragging the items up/down.', array(
    '!parent_menu_link' => filter_xss_admin($parent_menu_link),
  ));

  // Get the ID for the current menu link.
  $current_mlid = !empty($complete_form['menu']['link']['mlid']['#value']) ? $complete_form['menu']['link']['mlid']['#value'] : NULL;

  // Get the title for the current menu link.
  $new_item_title = isset($form_state['values']['menu']['link_title']) ? $form_state['values']['menu']['link_title'] : $complete_form['menu']['link']['link_title']['#default_value'];

  // Get the options array we will use to build the form elements for every
  // menu link.
  $options = _menu_link_weight_get_options($menu_name, $plid, $current_mlid, $new_item_title);

  // Allow other modules to reorder the options, if applicable.
  if (!empty($form_state['menu_link_weight_relative_position'][$parent_value])) {
    $options = _menu_link_weight_reorder_options($options, $form_state['menu_link_weight_relative_position'][$parent_value]);
  }

  // Build the form elements using the options array.
  foreach ($options as $mlid => $info) {
    $element[$mlid] = array(
      'name' => array(
        // The title should have already been sanitized here!
        '#markup' => $info['title'],
      ),
      'weight' => array(
        '#type' => 'weight',
        '#title' => t('Weight'),
        '#default_value' => $info['weight'],
        '#delta' => MENU_LINK_WEIGHT_MAX_DELTA,
        '#title_display' => 'invisible',
      ),
    );
    if ($mlid != 'link_current') {

      // Save the current weight in the database of the rendered menu links
      // into the form, so that we can give an error if the weights have changed
      // by the time the form is submitted.
      $complete_form['menu']['link']['db_weights'][$mlid] = array(
        '#type' => 'hidden',
        '#value' => $info['db_weight'],
      );
    }
  }
  return $element;
}

/**
 * Gets the menu link parent value from the form.
 *
 * Helper function for menu link weight process callback.
 *
 * @param array $parent_element
 *   Menu link parent form element.
 * @param array $form_state
 *   Form state array.
 *
 * @return string
 *   Menu link
 */
function _menu_link_weight_get_parent_value_from_element(array $parent_element, array $form_state) {
  $value = FALSE;
  if ($parent_element['#type'] == 'hierarchical_select' && function_exists('_hs_process_determine_hsid') && function_exists('_hierarchical_select_process_calculate_selections')) {

    // Added for compatibility with the Hierarchical Select module.
    $hsid = _hs_process_determine_hsid($parent_element, $form_state);
    list($hs_selection, $db_selection) = _hierarchical_select_process_calculate_selections($parent_element, $hsid, $form_state);
    $value = $hs_selection;
  }
  else {
    if (isset($form_state['values']['menu']['parent'])) {
      $value = $form_state['values']['menu']['parent'];
    }
    else {
      $value = !empty($parent_element['#value']) ? $parent_element['#value'] : $parent_element['#default_value'];
    }
  }

  // The menu_link_sync and hierarchical_select modules transform the parent
  // value into an array.
  if (is_array($value) && isset($value[0])) {
    $value = $value[0];
  }
  return $value;
}

/**
 * Gets a list of of options for a specific menu/parent.
 *
 * @param string $menu_name
 *   The name of the menu.
 * @param int $plid
 *   The parent link ID.
 * @param int $current_mlid
 *   The menu link for the current item.
 * @param string $new_item_title
 *   The title for the new menu link to be created.
 *
 * @see _menu_parents_recurse
 *
 * @return array
 *   List of options with index "link_current" or the menu link ID.
 *   Values include:
 *     - title: Santized title for the menu link.
 *     - weight: Calculated new weight.
 *     - db_weight: Current weight in the database, while form is being built.
 */
function _menu_link_weight_get_options($menu_name, $plid, $current_mlid, $new_item_title = NULL) {

  // Get the raw tree from the database.
  $tree = _menu_link_weight_get_tree($menu_name, $plid);

  // Weights will have to be re-ordered from -50 to 50 for fine-grained
  // control over the weight of the new element.
  $weight = MENU_LINK_WEIGHT_MIN_DELTA;
  $options = array();

  // Find out whether to add another (fake) item for the new link.
  $add_link = TRUE;
  foreach ($tree as $data) {
    if ($data['link']['mlid'] == $current_mlid) {
      $add_link = FALSE;
    }
  }

  // Add link on top, if needed.
  if ($add_link === TRUE) {
    $options['link_current'] = array(
      'title' => '<strong><span class="menu-link-weight-link-current">' . truncate_utf8(check_plain($new_item_title), 30, TRUE, FALSE) . '</span></strong> (' . t('provided menu link') . ')',
      'weight' => $weight,
      'db_weight' => NULL,
    );
    $weight++;
  }

  // Loop through the tree again.
  foreach ($tree as $data) {

    // Change the title & ID for the current menu link.
    if ($data['link']['mlid'] == $current_mlid) {
      $id = 'link_current';
      $title = '<strong><span class="menu-link-weight-link-current">' . truncate_utf8(check_plain($new_item_title), 30, TRUE, FALSE) . '</span></strong> (' . t('provided menu link') . ')';
    }
    else {
      $id = $data['link']['mlid'];
      $title = l(truncate_utf8($data['link']['link_title'], 30, TRUE, FALSE), $data['link']['link_path'], array(
        'attributes' => array(
          'target' => '_blank',
        ),
      ));
      if ($data['link']['hidden']) {
        $title .= ' (' . t('disabled') . ')';
      }
    }
    $options[$id] = array(
      'title' => $title,
      'weight' => $weight,
      'db_weight' => $data['link']['weight'],
    );
    $weight++;
  }
  return $options;
}

/**
 * Helper function to get all siblings of an item based on the parent.
 *
 * @param string $menu_name
 *   Name of the menu.
 * @param int $plid
 *   Parent link ID.
 *
 * @return array
 *   List of all items under the plid (that the user has access to).
 */
function _menu_link_weight_get_tree($menu_name, $plid) {
  global $menu_admin;
  if ($plid != 0) {
    $link = menu_Link_load($plid);
    $limit = $link['depth'] + 1;
  }
  else {
    $limit = 1;
  }

  // We indicate that a menu administrator will be running the menu access
  // check.
  $menu_admin = TRUE;
  $tree = menu_build_tree($menu_name, array(
    'active_trail' => array(
      $plid,
    ),
    'only_active_trail' => FALSE,
    'min_depth' => $limit,
    'max_depth' => $limit,
    'conditions' => array(
      'plid' => $plid,
    ),
    // To prevent core bug: https://www.drupal.org/node/1477608.
    // We do not want this call to be cached (which is partly done based on
    // the hash of the parameters), so we put a unique ID in the parameters.
    'do_not_cache' => uniqid(),
  ));
  $menu_admin = FALSE;
  return $tree;
}

/**
 * Validation hook for the menu_link weight element.
 */
function menu_link_weight_node_form_validate($form, &$form_state) {

  // PAreview gives the following warning when using form_state['input']:
  // "Do not use the raw form_state['input'], use form_state['values'] instead
  // where possible".
  // In this case we *have* to use the raw 'input' values though, as 'values'
  // has already been overwritten with the current weights during
  // menu_link_weight_node_element_process().
  $parent_element = $form['menu']['link']['parent'];
  $value = _menu_link_weight_get_parent_value_from_element($parent_element, $form_state);
  if (strstr($value, ':') && !empty($form_state['input']['menu']['menu_link_weight'])) {
    list($menu_name, $plid) = explode(':', $value);

    // Loop through submitted weights and confirm that the parent link/menu
    // are still the same.
    $weights = $form_state['input']['menu']['menu_link_weight'];
    unset($weights['link_current']);
    foreach ($weights as $mlid => $weight) {
      $link = menu_link_load($mlid);
      if ($link['plid'] != $plid) {
        form_set_error('menu_link_weight', t('The parent for one of the menu links have been changed by another user, please try again.'));
      }
      if ($link['menu_name'] != $menu_name) {
        form_set_error('menu_link_weight', t('The menu for one of the menu links have been changed by another user, please try again.'));
      }
    }
  }
  if (isset($form_state['input']['menu']['db_weights'])) {

    // Check that children and weights are still the same as when the form was
    // loaded. Get the old values from the "hidden" form elements.
    foreach ($form_state['input']['menu']['db_weights'] as $mlid => $db_weight) {
      $link = menu_link_load($mlid);
      if (!empty($link)) {
        if ($link['weight'] != $db_weight) {
          form_set_error('menu_link_weight', t('The menu link weights have been changed by another user, please try again.'));
        }
      }
    }
  }
}

/**
 * Custom submit hook for node form which reorders menu link weights.
 *
 * Note: this wil not reorder links that the current user does not have access
 * to (ie. links to access-controlled nodes).
 */
function menu_link_weight_node_form_submit($form, &$form_state) {

  // For graceful JS degradation.
  if ($form_state['triggering_element']['#value'] == menu_link_weight_get_button_text()) {
    $form_state['values']['menu']['parent'] = $form_state['input']['menu']['parent'];
    $form_state['rebuild'] = TRUE;
    return;
  }

  // Return on empty submissions or if if 'Provide a menu link' not selected.
  if (!isset($form_state['values']['menu']['menu_link_weight']) || !$form_state['values']['menu']['enabled']) {
    return;
  }
  $transaction = db_transaction();
  try {

    // Because the form elements were keyed with the item ids from the database,
    // we can simply iterate through the submitted values.
    foreach ($form_state['values']['menu']['menu_link_weight'] as $mlid => $info) {
      if ($mlid == 'link_current') {

        // Do nothing. Changing the weight of the current link will be handled
        // in hook_node_submit() instead.
        continue;
      }
      $link = menu_link_load($mlid);
      $link['weight'] = $info['weight'];
      menu_link_save($link);
    }
  } catch (Exception $e) {
    $transaction
      ->rollback();
    watchdog_exception('menu_link_weight', $e);
  }
}

/**
 * Implements hook_node_submit().
 *
 * Sets the weight of the current menu link.
 */
function menu_link_weight_node_submit($node, $form, $form_state) {

  // Override the weight of the current link upon node submission.
  if (isset($node->menu['menu_link_weight']['link_current']['weight'])) {
    $node->menu['weight'] = $node->menu['menu_link_weight']['link_current']['weight'];
  }
}

/**
 * Implements hook_theme().
 *
 * We need run our forms through custom theme functions in order to build the
 * table structure which is required by tabledrag.js.
 */
function menu_link_weight_theme() {
  return array(
    // Theme function for the 'simple' example.
    'menu_link_weight_reorder_element' => array(
      'render element' => 'element',
      'file' => 'menu_link_weight.module',
    ),
  );
}

/**
 * Theme callback for the menu_link_weight element in the node form.
 *
 * @see theme_tabledrag_example_simple_form()
 */
function theme_menu_link_weight_reorder_element($variables) {
  $element = $variables['element'];

  // Initialize the variable which will store our table rows.
  $rows = array();

  // Iterate over each element in our $form['menu']['link']['menu_link_weight']
  // array.
  foreach (element_children($element) as $id) {
    $has_tabledrag = TRUE;

    // Before we add our 'weight' column to the row, we need to give the
    // element a custom class so that it can be identified in the
    // drupal_add_tabledrag call.
    $weight_class = 'menu-link-weight-item-weight';
    $element[$id]['weight']['#attributes']['class'] = array(
      $weight_class,
    );

    // To support the tabledrag behaviour, we need to assign each row of the
    // table a class attribute of 'draggable'.
    $class = array(
      'draggable',
    );
    $no_striping = FALSE;
    if ($id == 'link_current') {

      // Make the row which contains the menu link for the node we are editing
      // highlight, by using the "warning" class.
      $class[] = 'warning';
      $class[] = 'menu-link-weight-link-current-row';
      $no_striping = TRUE;
    }

    // We are now ready to add each element of our $form data to the $rows
    // array, so that they end up as individual table cells when rendered
    // in the final table.  We run each element through the drupal_render()
    // function to generate the final html markup for that element.
    $rows[] = array(
      'data' => array(
        drupal_render($element[$id]['name']),
        drupal_render($element[$id]['weight']),
      ),
      'class' => $class,
      'no_striping' => $no_striping,
    );
  }

  // We also need to pass the drupal_add_tabledrag() function an id which will
  // be used to identify the <table> element containing our tabledrag form.
  $table_id = 'menu-link-weight-reorder';

  // We can render our tabledrag table for output.
  $output = theme('table', array(
    'header' => array(
      t('Name'),
      t('Weight'),
    ),
    'rows' => $rows,
    'attributes' => array(
      'id' => $table_id,
    ),
  ));

  // We now call the drupal_add_tabledrag() function in order to add the
  // tabledrag.js functionality onto our page.
  if (isset($has_tabledrag) && $has_tabledrag) {
    drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class);
  }
  return $output;
}

/**
 * Alters hierarchical_select_ajax_commands().
 *
 * Introduced for compatibility with the Hierarchical Select module, which uses
 * a custom AJAX implementation. Makes sure the menu link weight widget
 * updates when the Hierarchical Select selection changes.
 */
function menu_link_weight_hierarchical_select_ajax_commands_alter(&$commands, $context) {
  if (isset($context['form']['menu']['link']['menu_link_weight'])) {
    $commands[] = array(
      'command' => 'menuLinkWeightHierarchicalSelectUpdate',
      'output' => drupal_render($context['form']['menu']['link']['menu_link_weight']),
    );
  }
}

Functions

Namesort descending Description
menu_link_weight_element_info_alter Implements hook_element_info_alter().
menu_link_weight_form_menu_overview_form_alter Implements hook_form_FORM_ID_alter() for menu_overview_form().
menu_link_weight_form_node_form_alter Implements hook_form_FORM_ID_alter().
menu_link_weight_get_button_text Gets the name for the "No-Javascript" button we want to use.
menu_link_weight_hierarchical_select_ajax_commands_alter Alters hierarchical_select_ajax_commands().
menu_link_weight_node_element_process Process callback for the menu link weight element.
menu_link_weight_node_form_submit Custom submit hook for node form which reorders menu link weights.
menu_link_weight_node_form_validate Validation hook for the menu_link weight element.
menu_link_weight_node_submit Implements hook_node_submit().
menu_link_weight_parent_ajax_callback AJAX callback that returns the updated menu_link_weight element.
menu_link_weight_theme Implements hook_theme().
theme_menu_link_weight_reorder_element Theme callback for the menu_link_weight element in the node form.
_menu_link_weight_get_options Gets a list of of options for a specific menu/parent.
_menu_link_weight_get_parent_value_from_element Gets the menu link parent value from the form.
_menu_link_weight_get_tree Helper function to get all siblings of an item based on the parent.

Constants

Namesort descending Description
MENU_LINK_WEIGHT_MAX_DELTA Maximum weight of a menu link. In Drupal core this is currently 50.
MENU_LINK_WEIGHT_MIN_DELTA Minimum weight of a menu link. In Drupal core this is currently -50.