menu_link_weight.module in Menu Link Weight 7
Same filename and directory in other branches
Replaces the menu link weight dropdown with a tabledrag widget.
File
menu_link_weight.moduleView 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
Name | 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
Name | 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. |