Commit 8a4e0eb9 authored by Dries's avatar Dries

Merge branch '8.x' of git.drupal.org:project/drupal into 8.x

parents 78876b20 ab07a33a
/**
* @file
* RTL base styles for the Contextual module.
*/
.contextual .trigger {
text-align: left;
}
......@@ -4,35 +4,36 @@
* Generic base styles for contextual module.
*/
/**
* Contextual links behavior.
*/
.contextual,
.contextual .contextual-links,
.contextual .trigger {
.contextual-region {
position: relative;
}
.touch .contextual .trigger {
display: block;
}
.contextual .contextual-links {
display: none;
}
.touch .contextual,
.touch .contextual .trigger,
.no-touch .contextual-region:hover .contextual,
.no-touch .contextual-region:hover .contextual-links-trigger-active,
.contextual-active .contextual-links {
.contextual-links-active .contextual-links {
display: block;
}
/**
* Contextual links structure.
* The .element-focusable class extends the .element-invisible class to allow
* the element to be focusable when navigated to via the keyboard.
*
* Add support for hover.
*/
.contextual-region {
position: relative;
.touch .contextual-region .element-invisible.element-focusable,
.contextual-region:hover .element-invisible.element-focusable {
clip: auto;
overflow: visible;
height: auto;
}
.contextual {
position: absolute;
z-index: 999;
}
.contextual .trigger {
overflow: hidden;
position: relative;
text-align: right; /* LTR */
z-index: 1;
/* Override the position for contextual links. */
.contextual-region .element-invisible.element-focusable:active,
.contextual-region .element-invisible.element-focusable:focus,
.contextual-region:hover .element-invisible.element-focusable,
.contextual-region-active .element-invisible.element-focusable,
.touch .contextual-region .element-invisible.element-focusable {
position: relative !important;
}
......@@ -3,55 +3,159 @@
* Attaches behaviors for the Contextual module.
*/
(function ($) {
(function ($, Drupal) {
"use strict";
Drupal.contextualLinks = Drupal.contextualLinks || {};
/**
* Attaches outline behavior for regions associated with contextual links.
*/
Drupal.behaviors.contextualLinks = {
Drupal.behaviors.contextual = {
attach: function (context) {
$(context).find('div.contextual').once('contextual-links', function () {
var $wrapper = $(this);
var $region = $wrapper.closest('.contextual-region');
var $links = $wrapper.find('ul');
var $trigger = $('<a class="trigger" href="#" />').text(Drupal.t('Configure')).click(
function (e) {
e.preventDefault();
e.stopPropagation();
$links.stop(true, true).slideToggle(100);
$wrapper.toggleClass('contextual-active');
}
);
// Attach hover behavior to trigger and ul.contextual-links, for non touch devices only.
if(!Modernizr.touch) {
$trigger.add($links).hover(
function () { $region.addClass('contextual-region-active'); },
function () { $region.removeClass('contextual-region-active'); }
);
}
// Hide the contextual links when user clicks a link or rolls out of the .contextual-region.
$region.bind('mouseleave click', Drupal.contextualLinks.mouseleave);
$region.hover(
function() { $trigger.addClass('contextual-links-trigger-active'); },
function() { $trigger.removeClass('contextual-links-trigger-active'); }
);
// Prepend the trigger.
$wrapper.prepend($trigger);
$('ul.contextual-links', context).once('contextual', function () {
var $this = $(this);
$this.data('drupal-contextual', new Drupal.contextual($this, $this.closest('.contextual-region')));
});
}
};
/**
* Contextual links object.
*/
Drupal.contextual = function($links, $region) {
this.$links = $links;
this.$region = $region;
this.init();
};
/**
* Initiates a contextual links object.
*/
Drupal.contextual.prototype.init = function() {
// Wrap the links to provide positioning and behavior attachment context.
this.$wrapper = $(Drupal.theme.contextualWrapper())
.insertBefore(this.$links)
.append(this.$links);
// Mark the links as hidden. Use aria-role form so that the number of items
// in the list is spoken.
this.$links
.attr({
'hidden': 'hidden',
'role': 'form'
});
// Create and append the contextual links trigger.
var action = Drupal.t('Open');
this.$trigger = $(Drupal.theme.contextualTrigger())
.text(Drupal.t('@action configuration options', {'@action': action}))
// Set the aria-pressed state.
.attr('aria-pressed', false)
.prependTo(this.$wrapper);
// Bind behaviors through delegation.
var highlightRegion = $.proxy(this.highlightRegion, this);
this.$region
.on('click.contextual', '.contextual .trigger', $.proxy(this.triggerClickHandler, this))
.on('mouseenter.contextual', {highlight: true}, highlightRegion)
.on('mouseleave.contextual', {highlight: false}, highlightRegion)
.on('mouseleave.contextual', '.contextual', {show: false}, $.proxy(this.triggerLeaveHandler, this))
.on('focus.contextual', '.contextual-links a, .contextual .trigger', {highlight: true}, highlightRegion)
.on('blur.contextual', '.contextual-links a, .contextual .trigger', {highlight: false}, highlightRegion);
};
/**
* Toggles the highlighting of a contextual region.
*
* @param {Object} event
* jQuery Event object.
*/
Drupal.contextual.prototype.highlightRegion = function(event) {
// Set up a timeout to delay the dismissal of the region highlight state.
if (!event.data.highlight && this.timer === undefined) {
return this.timer = window.setTimeout($.proxy($.fn.trigger, $(event.target), 'mouseleave.contextual'), 100);
}
// Clear the timeout to prevent an infinite loop of mouseleave being
// triggered.
if (this.timer) {
window.clearTimeout(this.timer);
delete this.timer;
}
// Toggle active state of the contextual region based on the highlight value.
this.$region.toggleClass('contextual-region-active', event.data.highlight);
// Hide the links if the contextual region is inactive.
var state = this.$region.hasClass('contextual-region-active');
if (!state) {
this.showLinks(state);
}
};
/**
* Handles click on the contextual links trigger.
*
* @param {Object} event
* jQuery Event object.
*/
Drupal.contextual.prototype.triggerClickHandler = function (event) {
event.preventDefault();
this.showLinks();
};
/**
* Handles mouseleave on the contextual links trigger.
*
* @param {Object} event
* jQuery Event object.
*/
Drupal.contextual.prototype.triggerLeaveHandler = function (event) {
var show = event && event.data && event.data.show;
this.showLinks(show);
};
/**
* Toggles the active state of the contextual links.
*
* @param {Boolean} show
* (optional) True if the links should be shown. False is the links should be
* hidden.
*/
Drupal.contextual.prototype.showLinks = function(show) {
this.$wrapper.toggleClass('contextual-links-active', show);
var isOpen = this.$wrapper.hasClass('contextual-links-active');
var action = (isOpen) ? Drupal.t('Close') : Drupal.t('Open');
this.$trigger
.text(Drupal.t('@action configuration options', {'@action': action}))
// Set the aria-pressed state.
.attr('aria-pressed', isOpen);
// Mark the links as hidden if they are.
if (isOpen) {
this.$links.removeAttr('hidden');
}
else {
this.$links.attr('hidden', 'hidden');
}
};
/**
* Wraps contextual links.
*
* @return {String}
* A string representing a DOM fragment.
*/
Drupal.theme.contextualWrapper = function () {
return '<div class="contextual" />';
};
/**
* Disables outline for the region contextual links are associated with.
* A trigger is an interactive element often bound to a click handler.
*
* @return {String}
* A string representing a DOM fragment.
*/
Drupal.contextualLinks.mouseleave = function () {
$(this)
.find('.contextual-active').removeClass('contextual-active')
.find('.contextual-links').hide();
Drupal.theme.contextualTrigger = function () {
return '<button class="trigger element-invisible element-focusable" type="button"></button>';
};
})(jQuery);
})(jQuery, Drupal);
......@@ -69,8 +69,6 @@ function contextual_element_info() {
'#pre_render' => array('contextual_pre_render_links'),
'#theme' => 'links__contextual',
'#links' => array(),
'#prefix' => '<div class="contextual">',
'#suffix' => '</div>',
'#attributes' => array('class' => array('contextual-links')),
'#attached' => array(
'library' => array(
......
......@@ -3,17 +3,26 @@
* RTL styling for contextual module.
*/
/**
* Contextual links wrappers.
*/
.contextual {
left: 5px;
right: auto;
}
.contextual .contextual-links {
border-radius: 0 4px 4px 4px;
left: 0;
right: auto;
}
.contextual-region .contextual .contextual-links a {
text-align: right;
padding: 0.4em 0.6em 0.4em 0.8em;
/**
* Contextual trigger.
*/
.contextual .trigger {
float: left;
}
/**
* Contextual links.
*/
.contextual .contextual-links {
border-radius: 0 4px 4px 4px;
float: left;
text-align: right;
}
......@@ -6,58 +6,64 @@
/**
* Contextual links wrappers.
*/
.contextual {
position: absolute;
right: 0; /* LTR */
top: 2px;
z-index: 999;
}
.contextual-region-active {
outline: 1px dashed #d6d6d6;
outline-offset: 1px;
}
.contextual {
right: 2px; /* LTR */
top: 2px;
}
/**
* Contextual trigger.
*/
.contextual .trigger {
background: transparent url(images/gear-select.png) no-repeat 2px 0;
background: transparent url("images/gear-select.png") no-repeat 2px 0;
border: 1px solid transparent;
height: 18px;
border-radius: 4px 4px 0 0;
/* Override the .element-focusable height: auto */
height: 18px !important;
float: right; /* LTR */
margin: 0;
outline: none;
overflow: hidden;
padding: 0 2px;
text-indent: 34px;
width: 28px;
position: relative;
width: 34px;
text-indent: -9999px;
z-index: 2;
}
.no-touch .contextual .trigger:hover,
.contextual-active .trigger {
.contextual-links-active .trigger {
background-position: 2px -18px;
}
.contextual-active .trigger {
background-color: #ffffff;
.contextual-links-active .trigger {
background-color: #fff;
border-bottom: none;
border-color: #d6d6d6;
border-radius: 4px 4px 0 0;
position: relative;
z-index: 1;
}
/**
* Contextual links.
*
* The following selectors are heavy to discourage theme overriding.
*/
.contextual .contextual-links {
.contextual-region .contextual .contextual-links {
background-color: #fff;
border: 1px solid #d6d6d6;
border-radius: 4px 0 4px 4px; /* LTR */
clear: both;
float: right; /* LTR */
margin: 0;
padding: 0.25em 0;
position: absolute;
right: 0; /* LTR */
text-align: left;
top: 18px;
position: relative;
text-align: left; /* LTR */
top: -1px;
white-space: nowrap;
z-index: 1;
}
/* Reset the li to prevent accidential overrides by a theme. */
.contextual-region .contextual .contextual-links li {
background-color: #fff;
border: none;
......@@ -65,21 +71,22 @@
list-style-image: none;
margin: 0;
padding: 0;
line-height: 100%;
}
.contextual-region .contextual .contextual-links a {
background-color: #fff;
/* This is an unforetunately necessary use of !important to prevent white
* links on a white background or some similar illegible combination. */
color: #333 !important;
display: block;
font-family: sans-serif;
font-size: small;
line-height: 0.8em;
margin: 0.25em 0;
padding: 0.4em 0.8em 0.4em 0.6em; /* LTR */
padding: 0.4em 0.6em;
}
.contextual-region .contextual .contextual-links a,
.no-touch .contextual-region .contextual .contextual-links a:hover,
.contextual-region .contextual .contextual-links a:active,
.contextual-region .contextual .contextual-links a:focus {
background-color: #fff;
color: #333;
.contextual-region .contextual .contextual-links a:hover {
text-decoration: none;
}
.no-touch .contextual-region .contextual .contextual-links li a:hover {
......
......@@ -47,26 +47,29 @@ function edit_toolbar() {
}
$tab['edit'] = array(
'#type' => 'toolbar_item',
'tab' => array(
'title' => t('Edit'),
'href' => '',
'html' => FALSE,
'attributes' => array(
'class' => array('icon', 'icon-edit', 'edit-nothing-editable-hidden'),
'#type' => 'link',
'#title' => t('Edit'),
'#href' => '',
'#options' => array(
'html' => FALSE,
'attributes' => array(
'id' => 'toolbar-tab-edit',
'class' => array('icon', 'icon-edit', 'edit-nothing-editable-hidden'),
),
),
),
'tray' => array(
'#attached' => array(
'library' => array(
array('edit', 'edit'),
),
'#attached' => array(
'library' => array(
array('edit', 'edit'),
),
),
);
// Include the attachments and settings for all available editors.
$attachments = drupal_container()->get('edit.editor.selector')->getAllEditorAttachments();
$tab['edit']['tray']['#attached'] = NestedArray::mergeDeep($tab['edit']['tray']['#attached'], $attachments);
$tab['edit']['#attached'] = NestedArray::mergeDeep($tab['edit']['#attached'], $attachments);
return $tab;
}
......
......@@ -271,7 +271,7 @@ private function doAdminTests($user) {
// Add forum to the Tools menu.
$edit = array();
$this->drupalPost('admin/structure/menu/manage/tools', $edit, t('Save configuration'));
$this->drupalPost('admin/structure/menu/manage/tools', $edit, t('Save'));
$this->assertResponse(200);
// Edit forum taxonomy.
......
......@@ -21,6 +21,7 @@ class MenuFormController extends EntityFormController {
public function form(array $form, array &$form_state, EntityInterface $menu) {
$form = parent::form($form, $form_state, $menu);
$system_menus = menu_list_system_menus();
$form_state['menu'] = &$menu;
$form['label'] = array(
'#type' => 'textfield',
......@@ -50,6 +51,19 @@ public function form(array $form, array &$form_state, EntityInterface $menu) {
'#title' => t('Description'),
'#default_value' => $menu->description,
);
// Add menu links administration form for existing menus.
if (!$menu->isNew() || isset($system_menus[$menu->id()])) {
// Form API supports constructing and validating self-contained sections
// within forms, but does not allow to handle the form section's submission
// equally separated yet. Therefore, we use a $form_state key to point to
// the parents of the form section.
// @see menu_overview_form_submit()
$form_state['menu_overview_form_parents'] = array('links');
$form['links'] = array();
$form['links'] = menu_overview_form($form['links'], $form_state);
}
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
......@@ -71,6 +85,11 @@ public function form(array $form, array &$form_state, EntityInterface $menu) {
*/
public function save(array $form, array &$form_state) {
$menu = $this->getEntity($form_state);
$system_menus = menu_list_system_menus();
if (!$menu->isNew() || isset($system_menus[$menu->id()])) {
menu_overview_form_submit($form, $form_state);
}
if ($menu->isNew()) {
// Add 'menu-' to the menu name to help avoid name-space conflicts.
......
......@@ -47,13 +47,8 @@ public function getOperations(EntityInterface $entity) {
$operations = parent::getOperations($entity);
$uri = $entity->uri();
$operations['list'] = array(
'title' => t('list links'),
'href' => $uri['path'],
'options' => $uri['options'],
'weight' => 0,
);
$operations['edit']['title'] = t('edit menu');
$operatuins['edit']['href'] = $uri['path'];
$operations['add'] = array(
'title' => t('add link'),
'href' => $uri['path'] . '/add',
......
......@@ -104,14 +104,14 @@ function addCustomMenuCRUD() {
$menu->save();
// Assert the new menu.
$this->drupalGet('admin/structure/menu/manage/' . $menu_name . '/edit');
$this->drupalGet('admin/structure/menu/manage/' . $menu_name);
$this->assertRaw($label, 'Custom menu was added.');
// Edit the menu.
$new_label = $this->randomName(16);
$menu->set('label', $new_label);
$menu->save();
$this->drupalGet('admin/structure/menu/manage/' . $menu_name . '/edit');
$this->drupalGet('admin/structure/menu/manage/' . $menu_name);
$this->assertRaw($new_label, 'Custom menu was edited.');
}
......@@ -242,10 +242,10 @@ function doMenuTests($menu_name) {
$this->disableMenuLink($item1);
$edit = array();
// Note in the UI the 'mlid:x[hidden]' form element maps to enabled, or
// NOT hidden.
$edit['mlid:' . $item1['mlid'] . '[hidden]'] = TRUE;
$this->drupalPost('admin/structure/menu/manage/' . $item1['menu_name'], $edit, t('Save configuration'));
// Note in the UI the 'links[mlid:x][hidden]' form element maps to enabled,
// or NOT hidden.
$edit['links[mlid:' . $item1['mlid'] . '][hidden]'] = TRUE;
$this->drupalPost('admin/structure/menu/manage/' . $item1['menu_name'], $edit, t('Save'));
// Verify in the database.
$this->assertMenuLink($item1['mlid'], array('hidden' => 0));
......
......@@ -7,6 +7,7 @@
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Drupal\system\Plugin\Core\Entity\Menu;
use Drupal\Component\Utility\NestedArray;
/**
* Menu callback which shows an overview page of all the custom menus and their descriptions.
......@@ -47,20 +48,35 @@ function menu_menu_edit(Menu $menu) {
}
/**
* Form for editing an entire menu tree at once.
* Form constructor to edit an entire menu tree at once.
*
* Shows for one menu the menu links accessible to the current user and
* relevant operations.
*
* This form constructor can be integrated as a section into another form. It
* relies on the following keys in $form_state:
* - menu: A loaded menu definition, as returned by menu_load().
* - menu_overview_form_parents: An array containing the parent keys to this
* form.
* Forms integrating this section should call menu_overview_form_submit() from
* their form submit handler.
*/
function menu_overview_form($form, &$form_state, $menu) {
function menu_overview_form($form, &$form_state) {
global $menu_admin;
// Ensure that menu_overview_form_submit() knows the parents of this form
// section.
$form['#tree'] = TRUE;
$form['#theme'] = 'menu_overview_form';
$form_state += array('menu_overview_form_parents' => array());
$form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/menu.admin.css');
$sql = "
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, m.description_callback, m.description_arguments, ml.*
FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
WHERE ml.menu_name = :menu
ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC";
$result = db_query($sql, array(':menu' => $menu->id()), array('fetch' => PDO::FETCH_ASSOC));
$result = db_query($sql, array(':menu' => $form_state['menu']->id()), array('fetch' => PDO::FETCH_ASSOC));
$links = array();
foreach ($result as $item) {
$links[] = $item;
......@@ -74,20 +90,23 @@ function menu_overview_form($form, &$form_state, $menu) {
menu_tree_check_access($tree, $node_links);
$menu_admin = FALSE;
// Inline the "Add link" action so it displays right above the table of
// links. No access check needed, since this form has the same access
// restriction as adding menu items to the menu.
$form['inline_actions'] = array(
'#prefix' => '<ul class="action-links">',
'#suffix' => '</ul>',
);
$form['inline_actions']['add'] = array(
'#theme' => 'menu_local_action',
'#link' => array(
'href' => 'admin/structure/menu/manage/' . $form_state['menu']->id() . '/add',
'title' => t('Add link'),
),
);
$form = array_merge($form, _menu_overview_tree_form($tree, $delta));
$form['#menu'] = $menu;
$form['#empty_text'] = t('There are no menu links yet. <a href="@link">Add link</a>.', array('@link' => url('admin/structure/menu/manage/' . $form_state['menu']->id() .'/add')));
if (element_children($form)) {
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save configuration'),
'#button_type' => 'primary',
);
}
else {
$form['#empty_text'] = t('There are no menu links yet. <a href="@link">Add link</a>.', array('@link' => url('admin/structure/menu/manage/'. $form['#menu']->id() .'/add')));
}
return $form;
}
......@@ -183,15 +202,24 @@ function _menu_overview_tree_form($tree, $delta = 50) {
*
* @see menu_overview_form()
*/
function menu_overview_form_submit($form, &$form_state) {
function menu_overview_form_submit($complete_form, &$form_state) {
// Form API supports constructing and validating self-contained sections
// within forms, but does not allow to handle the form section's submission
// equally separated yet. Therefore, we use a $form_state key to point to
// the parents of the form section.
$parents = $form_state['menu_overview_form_parents'];
$input = NestedArray::getValue($form_state['input'], $parents);
$form = &NestedArray::getValue($complete_form, $parents);
// When dealing with saving menu items, the order in which these items are
// saved is critical. If a changed child item is saved before its parent,
// the child item could be saved with an invalid path past its immediate
// parent. To prevent this, save items in the form in the same order they
// are sent by $_POST, ensuring parents are saved first, then their children.
// are sent, ensuring parents are saved first, then their children.
// See http://drupal.org/node/181126#comment-632270
$order = array_flip(array_keys($form_state['input'])); // Get the $_POST order.
$form = array_intersect_key(array_merge($order, $form), $form); // Update our original form with the new order.
$order = array_flip(array_keys($input));
// Update our original form with the new order.
$form = array_intersect_key(array_merge($order, $form), $form);
$updated_items = array();
$fields = array('weight', 'plid');
......@@ -219,7 +247,6 @@ function menu_overview_form_submit($form, &$form_state) {
$item['customized'] = 1;
menu_link_save($item);
}
drupal_set_message(t('Your configuration has been saved.'));
}
/**
......@@ -272,6 +299,7 @@ function theme_menu_overview_form($variables) {
if (empty($rows)) {
$rows[] = array(array('data' => $form['#empty_text'], 'colspan' => '7'));
}
$output .= drupal_render($form['inline_actions']);
$output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'menu-overview')));
$output .= drupal_render_children($form);
return $output;
......
......@@ -99,36 +99,28 @@ function menu_menu() {
'file' => 'menu.admin.inc',
);
$items['admin/structure/menu/manage/%menu'] = array(
'title' => 'Customize menu',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_overview_form', 4),
'title' => 'Edit menu',
'page callback' => 'menu_menu_edit',
'page arguments' => array(4),
'title callback' => 'entity_page_label',
'title arguments' => array(4),
'access arguments' => array('administer menu'),
'file' => 'menu.admin.inc',
);
$items['admin/structure/menu/manage/%menu/list'] = array(
'title' => 'List links',
'weight' => -10,