Commit 785ca2b2 authored by webchick's avatar webchick

Issue #1751606 follow-up by sun, swentel: Various fixes for content creation page.

parent 478a624a
......@@ -3336,6 +3336,52 @@ function form_process_actions($element, &$form_state) {
return $element;
}
/**
* #pre_render callback for #type 'actions'.
*
* This callback iterates over all child elements of the #type 'actions'
* container to look for elements with a #dropbutton property, so as to group
* those elements into dropbuttons. As such, it works similar to #group, but is
* specialized for dropbuttons.
*
* The value of #dropbutton denotes the dropbutton to group the child element
* into. For example, two different values of 'foo' and 'bar' on child elements
* would generate two separate dropbuttons, which each contain the corresponding
* buttons.
*
* @param array $element
* The #type 'actions' element to process.
*
* @return array
* The processed #type 'actions' element, including individual buttons grouped
* into new #type 'dropbutton' elements.
*/
function form_pre_render_actions_dropbutton(array $element) {
$dropbuttons = array();
foreach (element_children($element, TRUE) as $key) {
if (isset($element[$key]['#dropbutton'])) {
$dropbutton = $element[$key]['#dropbutton'];
// If there is no dropbutton for this button group yet, create one.
if (!isset($dropbuttons[$dropbutton])) {
$dropbuttons[$dropbutton] = array(
'#type' => 'dropbutton',
);
}
// Add this button to the corresponding dropbutton.
// @todo Change #type 'dropbutton' to be based on theme_item_list()
// instead of theme_links() to avoid this preemptive rendering.
$button = drupal_render($element[$key]);
$dropbuttons[$dropbutton]['#links'][$key] = array(
'title' => $button,
'html' => TRUE,
);
}
}
// @todo For now, all dropbuttons appear first. Consider to invent a more
// fancy sorting/injection algorithm here.
return $dropbuttons + $element;
}
/**
* #process callback for #pattern form element property.
*
......
......@@ -1802,11 +1802,11 @@ function theme_links($variables) {
// Merge in default array properties into $link.
$link += array(
'html' => FALSE,
'attributes' => array(),
);
$item = '<span' . new Attribute($link['attributes']) . '>';
$item .= ($link['html'] ? $link['title'] : check_plain($link['title']));
$item .= '</span>';
$item = ($link['html'] ? $link['title'] : check_plain($link['title']));
if (isset($link['attributes'])) {
$item = '<span' . new Attribute($link['attributes']) . '>' . $item . '</span>';
}
}
$output .= '<li' . new Attribute(array('class' => $class)) . '>';
......@@ -1834,24 +1834,6 @@ function theme_dropbutton_wrapper($variables) {
}
}
/**
* Returns HTML for wrapping a dropbutton list.
*
* Use this function if the dropbutton contains submit buttons. These elements
* need to have a #prefix and #suffix element that wraps those into an <li>
* element.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the dropbutton list. Properties used: #children.
*/
function theme_dropbutton_list_wrapper($variables) {
if (!empty($variables['element']['#children'])) {
return '<ul class="dropbutton">' . $variables['element']['#children'] . '</ul>';
}
}
/**
* Returns HTML for an image.
*
......@@ -3163,9 +3145,6 @@ function drupal_common_theme() {
'dropbutton_wrapper' => array(
'render element' => 'element',
),
'dropbutton_list_wrapper' => array(
'render element' => 'element',
),
'image' => array(
// HTML 4 and XHTML 1.0 always require an alt attribute. The HTML 5 draft
// allows the alt attribute to be omitted in some cases. Therefore,
......
......@@ -22,13 +22,21 @@
.js .dropbutton-widget {
max-width: 100%;
}
@media screen and (max-width:600px) {
.js .dropbutton-wrapper {
width: 100%;
}
}
/* Splitbuttons */
.form-actions .dropbutton-wrapper {
float: left;
}
.js .form-actions .dropbutton-widget {
position: static;
}
.js .dropbutton-widget {
position: absolute;
}
......@@ -79,7 +87,7 @@
text-indent: 110%;
top: 0;
white-space: nowrap;
width: 2.08em;
width: 2em;
}
.dropbutton-toggle button {
background: none;
......
......@@ -275,99 +275,70 @@ public function form(array $form, array &$form_state, EntityInterface $node) {
}
/**
* Overrides Drupal\entity\EntityFormController::actionsElement().
* Overrides Drupal\Core\Entity\EntityFormController::actions().
*/
protected function actionsElement(array $form, array &$form_state) {
$element = parent::actionsElement($form, $form_state);
protected function actions(array $form, array &$form_state) {
$element = parent::actions($form, $form_state);
$node = $this->getEntity($form_state);
$preview_mode = variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL);
// Because some of the 'links' are actually submit buttons, we have to
// manually wrap each item in <li> and the whole list in <ul>. The
// <ul> is added with a #theme_wrappers function.
$element['operations'] = array(
'#type' => 'operations',
'#subtype' => 'node',
'#attached' => array (
'css' => array(
drupal_get_path('module', 'node') . '/node.admin.css',
),
),
);
$element['operations']['actions'] = array(
'#theme_wrappers' => array('dropbutton_list_wrapper')
);
// Depending on the state of the node (published or unpublished) and
// whether the current user has the permission to change the status, the
// labels and order of the buttons will vary.
if (user_access('administer nodes')) {
$element['operations']['actions']['publish'] = array(
'#type' => 'submit',
'#value' => t('Save and publish'),
'#submit' => array(array($this, 'publish'), array($this, 'submit'), array($this, 'save')),
'#validate' => array(array($this, 'validate')),
'#button_type' => $node->status ? 'primary' : '',
'#weight' => 0,
'#prefix' => '<li class="publish">',
'#suffix' => '</li>',
);
$element['operations']['actions']['unpublish'] = array(
'#type' => 'submit',
'#value' => t('Save as unpublished'),
'#submit' => array(array($this, 'unpublish'), array($this, 'submit'), array($this, 'save')),
'#validate' => array(array($this, 'validate')),
'#button_type' => empty($node->status) ? 'primary' : '',
'#weight' => $node->status ? 1 : -1,
'#prefix' => '<li class="unpublish">',
"#suffix" => '</li>',
);
$element['submit']['#access'] = $preview_mode != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview']));
if (!empty($node->nid)) {
if ($node->status) {
$publish_label = t('Save and keep published');
$unpublish_label = t('Save and unpublish');
}
else {
$publish_label = t('Save and publish');
$unpublish_label = t('Save and keep unpublished');
}
$element['operations']['actions']['publish']['#value'] = $publish_label;
$element['operations']['actions']['unpublish']['#value'] = $unpublish_label;
// If saving is an option, privileged users get dedicated form submit
// buttons to adjust the publishing status while saving in one go.
// @todo This adjustment makes it close to impossible for contributed
// modules to integrate with "the Save operation" of this form. Modules
// need a way to plug themselves into 1) the ::submit() step, and
// 2) the ::save() step, both decoupled from the pressed form button.
if ($element['submit']['#access'] && user_access('administer nodes')) {
// isNew | prev status » default & publish label & unpublish label
// 1 | 1 » publish & Save and publish & Save as unpublished
// 1 | 0 » unpublish & Save and publish & Save as unpublished
// 0 | 1 » publish & Save and keep published & Save and unpublish
// 0 | 0 » unpublish & Save and keep unpublished & Save and publish
// Add a "Publish" button.
$element['publish'] = $element['submit'];
$element['publish']['#dropbutton'] = 'save';
if ($node->isNew()) {
$element['publish']['#value'] = t('Save and publish');
}
}
// The user has no permission to change the status of the node. Just
// show a save button without the 'publish' or 'unpublish' callback in
// the #submit definition.
else {
$element['operations']['actions']['save'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#submit' => array(array($this, 'submit'), array($this, 'save')),
'#validate' => array(array($this, 'validate')),
'#button_type' => 'primary',
'#weight' => 1,
'#prefix' => '<li class="save">',
"#suffix" => '</li>',
);
}
unset($element['submit']);
else {
$element['publish']['#value'] = $node->status ? t('Save and keep published') : t('Save and publish');
}
$element['publish']['#weight'] = 0;
array_unshift($element['publish']['#submit'], array($this, 'publish'));
// Add a "Unpublish" button.
$element['unpublish'] = $element['submit'];
$element['unpublish']['#dropbutton'] = 'save';
if ($node->isNew()) {
$element['unpublish']['#value'] = t('Save as unpublished');
}
else {
$element['unpublish']['#value'] = !$node->status ? t('Save and keep unpublished') : t('Save and unpublish');
}
$element['unpublish']['#weight'] = 10;
array_unshift($element['unpublish']['#submit'], array($this, 'unpublish'));
return $element;
}
// If already published, the 'publish' button is primary.
if ($node->status) {
unset($element['unpublish']['#button_type']);
}
// Otherwise, the 'unpublish' button is primary and should come first.
else {
unset($element['publish']['#button_type']);
$element['unpublish']['#weight'] = -10;
}
/*
* Overrides Drupal\Core\Entity\EntityFormController::actions().
*/
protected function actions(array $form, array &$form_state) {
$element = parent::actions($form, $form_state);
$node = $this->getEntity($form_state);
$preview_mode = variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL);
// Remove the "Save" button.
$element['submit']['#access'] = FALSE;
}
$element['preview'] = array(
'#access' => $preview_mode != DRUPAL_DISABLED,
'#value' => t('Preview'),
'#weight' => 20,
'#validate' => array(
array($this, 'validate'),
),
......@@ -377,8 +348,8 @@ protected function actions(array $form, array &$form_state) {
),
);
$element['submit']['#access'] = $preview_mode != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview']));
$element['delete']['#access'] = node_access('delete', $node);
$element['delete']['#weight'] = 100;
return $element;
}
......@@ -473,7 +444,7 @@ public function preview(array $form, array &$form_state) {
*/
public function publish(array $form, array &$form_state) {
$node = $this->getEntity($form_state);
$node->status = TRUE;
$node->status = 1;
return $node;
}
......@@ -487,7 +458,7 @@ public function publish(array $form, array &$form_state) {
*/
public function unpublish(array $form, array &$form_state) {
$node = $this->getEntity($form_state);
$node->status = FALSE;
$node->status = 0;
return $node;
}
......
<?php
/**
* @file
* Contains Drupal\node\Tests\NodeLoadHooksTest.
*/
namespace Drupal\node\Tests;
/**
* Tests the node form buttons.
*/
class NodeFormButtonsTest extends NodeTestBase {
protected $web_user;
protected $admin_user;
public static function getInfo() {
return array(
'name' => 'Node form buttons',
'description' => 'Test all the different buttons on the node form.',
'group' => 'Node',
);
}
function setUp() {
parent::setUp();
// Create a user that has no access to change the state of the node.
$this->web_user = $this->drupalCreateUser(array('create article content', 'edit own article content'));
// Create a user that has access to change the state of the node.
$this->admin_user = $this->drupalCreateUser(array('administer nodes', 'bypass node access'));
}
/**
* Tests that the right buttons are displayed for saving nodes.
*/
function testNodeFormButtons() {
// Login as administrative user.
$this->drupalLogin($this->admin_user);
// Verify the buttons on a node add form.
$this->drupalGet('node/add/article');
$this->assertButtons(array(t('Save and publish'), t('Save as unpublished')));
// Save the node and assert it's published after clicking
// 'Save and publish'.
$edit = array('title' => $this->randomString());
$this->drupalPost('node/add/article', $edit, t('Save and publish'));
// Get the node.
$node_1 = node_load(1);
$this->assertEqual(1, $node_1->status, 'Node is published');
// Verify the buttons on a node edit form.
$this->drupalGet('node/' . $node_1->nid . '/edit');
$this->assertButtons(array(t('Save and keep published'), t('Save and unpublish')));
// Save the node and verify it's still published after clicking
// 'Save and keep published'.
$this->drupalPost(NULL, $edit, t('Save and keep published'));
$node = node_load(1, TRUE);
$this->assertEqual(1, $node_1->status, 'Node is published');
// Save the node and verify it's unpublished after clicking
// 'Save and unpublish'.
$this->drupalPost('node/' . $node_1->nid . '/edit', $edit, t('Save and unpublish'));
$node_1 = node_load(1, TRUE);
$this->assertEqual(0, $node_1->status, 'Node is unpublished');
// Verify the buttons on an unpublished node edit screen.
$this->drupalGet('node/' . $node_1->nid . '/edit');
$this->assertButtons(array(t('Save and keep unpublished'), t('Save and publish')));
// Create a node as a normal user.
$this->drupalLogout();
$this->drupalLogin($this->web_user);
// Verify the buttons for a normal user.
$this->drupalGet('node/add/article');
$this->assertButtons(array(t('Save')), FALSE);
// Create the node.
$edit = array('title' => $this->randomString());
$this->drupalPost('node/add/article', $edit, t('Save'));
$node_2 = node_load(2);
$this->assertEqual(1, $node_2->status, 'Node is published');
// Login as an administrator and unpublish the node that just
// was created by the normal user.
$this->drupalLogout();
$this->drupalLogin($this->admin_user);
$this->drupalPost('node/' . $node_2->nid . '/edit', array(), t('Save and unpublish'));
$node_2 = node_load(2, TRUE);
$this->assertEqual(0, $node_2->status, 'Node is unpublished');
// Login again as the normal user, save the node and verify
// it's still unpublished.
$this->drupalLogout();
$this->drupalLogin($this->web_user);
$this->drupalPost('node/' . $node_2->nid . '/edit', array(), t('Save'));
$node_2 = node_load(2, TRUE);
$this->assertEqual(0, $node_2->status, 'Node is still unpublished');
$this->drupalLogout();
// Set article content type default to unpublished. This will change the
// the initial order of buttons and/or status of the node when creating
// a node.
variable_set('node_options_article', array('promote'));
$this->refreshVariables();
// Verify the buttons on a node add form for an administrator.
$this->drupalLogin($this->admin_user);
$this->drupalGet('node/add/article');
$this->assertButtons(array(t('Save as unpublished'), t('Save and publish')));
// Verify the node is unpublished by default for a normal user.
$this->drupalLogout();
$this->drupalLogin($this->web_user);
$edit = array('title' => $this->randomString());
$this->drupalPost('node/add/article', $edit, t('Save'));
$node_3 = node_load(3);
$this->assertEqual(0, $node_3->status, 'Node is unpublished');
}
/**
* Assert method to verify the buttons in the dropdown element.
*
* @param array $buttons
* A collection of buttons to assert for on the page.
* @param bool $dropbutton
* Whether to check if the buttons are in a dropbutton widget or not.
*/
public function assertButtons($buttons, $dropbutton = TRUE) {
// Try to find a Save button.
$save_button = $this->xpath('//input[@type="submit"][@value="Save"]');
// Verify that the number of buttons passed as parameters is
// available in the dropbutton widget.
if ($dropbutton) {
$i = 0;
$count = count($buttons);
// Assert there is no save button.
$this->assertTrue(empty($save_button));
// Dropbutton elements.
$elements = $this->xpath('//div[@class="dropbutton-wrapper"]//input[@type="submit"]');
$this->assertEqual($count, count($elements));
foreach ($elements as $element) {
$value = isset($element['value']) ? (string) $element['value'] : '';
$this->assertEqual($buttons[$i], $value);
$i++;
}
}
else {
// Assert there is a save button.
$this->assertTrue(!empty($save_button));
$this->assertNoRaw('dropbutton-wrapper');
}
}
}
......@@ -9,130 +9,3 @@
.revision-current {
background: #ffc;
}
/**
* Node form dropbuttons.
*/
.form-actions .dropbutton-wrapper {
float: left;
margin-right: 1em;
}
.form-actions .dropbutton-wrapper .dropbutton-widget {
position: static;
}
.form-actions .dropbutton-wrapper li a,
.form-actions .dropbutton-wrapper input {
padding: 5px 17px 6px 17px;
margin-bottom: 0em;
border: medium;
border-radius: 0;
background: none;
}
.form-actions .dropbutton-wrapper input:hover {
background: none;
border: none;
}
.form-actions .button {
background: #fefefe;
background-image: -webkit-linear-gradient(top, #fefefe, #e0e0e0);
background-image: -moz-linear-gradient(top, #fefefe, #e0e0e0);
background-image: -o-linear-gradient(top, #fefefe, #e0e0e0);
background-image: linear-gradient(to bottom, #fefefe, #e0e0e0);
border: 1px solid #c8c8c8;
border-radius: 3px;
text-decoration: none;
padding: 6px 17px 6px 17px;
margin-left: 0;
}
.form-actions .button:focus,
.form-actions .button:hover {
background: #fefefe;
background-image: -webkit-linear-gradient(top, #fefefe, #eaeaea);
background-image: -moz-linear-gradient(top, #fefefe, #eaeaea);
background-image: -o-linear-gradient(top, #fefefe, #eaeaea);
background-image: linear-gradient(to bottom, #fefefe, #eaeaea);
-webkit-box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
color: #2e2e2e;
text-decoration: none;
}
.form-actions .button:active {
border: 1px solid #c8c8c8;
background: #fefefe;
background-image: -webkit-linear-gradient(top, #eaeaea, #fefefe);
background-image: -moz-linear-gradient(top, #eaeaea, #fefefe);
background-image: -o-linear-gradient(top, #eaeaea, #fefefe);
background-image: linear-gradient(to bottom, #eaeaea, #fefefe);
-webkit-box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
color: #2e2e2e;
text-decoration: none;
text-shadow: none;
}
/* Delete button */
.form-actions .button-danger {
color: #c72100;
background: none;
border: none;
float: right;
margin-right: 0;
margin-left: 0;
padding-right: 0;
padding-left: 0;
}
.form-actions .button-danger:hover,
.form-actions .button-danger:focus {
color: #ff2a00;
background: none;
border: none;
text-decoration: underline;
}
.form-actions .button-danger:active {
color: #ff2a00;
background: none;
border: none;
text-decoration: underline;
}
/**
* Form edit action theming
*/
.js .form-actions .dropbutton-widget {
background-color: #50a0e9;
background-image: -moz-linear-gradient(-90deg, #50a0e9, #4481dc);
background-image: -o-linear-gradient(-90deg, #50a0e9, #4481dc);
background-image: -webkit-linear-gradient(-90deg, #50a0e9, #4481dc);
background-image: linear-gradient(180deg, #50a0e9, #4481dc);
border-radius: 3px;
border: 1px solid #3974ae;
}
.js .form-actions .dropbutton-widget .dropbutton li {
border-top: 1px solid rgba(255, 255, 255, 0.5);
border-top-left-radius: 3px;
}
.js .form-actions .dropbutton-widget .dropbutton .dropbutton-toggle {
border-top-left-radius: 0px;
border-top-right-radius: 3px;
top: 1px;
}
.js .form-actions .dropbutton-widget .dropbutton .secondary-action {
border-top: 1px solid rgba(255, 255, 255, 0.3);
border-top-left-radius: 0px;
}
.js .form-actions .dropbutton-widget .button {
color: #ffffff;
text-shadow: 1px 1px 1px rgba(31, 83, 131, 0.8);
}
.js .form-actions .dropbutton-multiple.open .dropbutton-action:hover {
background-color: #50a0e9;
}
......@@ -434,9 +434,6 @@ function node_admin_nodes() {
'#title' => t('Update options'),
'#attributes' => array('class' => array('container-inline')),
'#access' => $admin_access,
'#attached' => array (
'css' => array(drupal_get_path('module', 'node') . '/css/node-admin.theme.css'),
),
);
$options = array();
foreach (module_invoke_all('node_operations') as $operation => $array) {
......
......@@ -164,7 +164,7 @@ function testLinks() {
$expected_links = '';
$expected_links .= '<ul id="somelinks">';
$expected_links .= '<li class="a-link odd first"><a href="' . url('a/link') . '">' . check_plain('A <link>') . '</a></li>';
$expected_links .= '<li class="plain-text even"><span>' . check_plain('Plain "text"') . '</span></li>';
$expected_links .= '<li class="plain-text even">' . check_plain('Plain "text"') . '</li>';
$expected_links .= '<li class="front-page odd last active"><a href="' . url('<front>') . '" class="active">' . check_plain('Front page') . '</a></li>';
$expected_links .= '</ul>';
......@@ -185,6 +185,22 @@ function testLinks() {
$expected_heading = '<h3 id="heading">Links heading</h3>';
$expected = $expected_heading . $expected_links;
$this->assertThemeOutput('links', $variables, $expected);
// Verify that passing attributes for the links work.
$variables['links']['a link']['attributes'] = array(
'class' => array('a/class'),
);
$variables['links']['plain text']['attributes'] = array(
'class' => array('a/class'),
);
$expected_links = '';
$expected_links .= '<ul id="somelinks">';
$expected_links .= '<li class="a-link odd first"><a href="' . url('a/link') . '" class="a/class">' . check_plain('A <link>') . '</a></li>';
$expected_links .= '<li class="plain-text even"><span class="a/class">' . check_plain('Plain "text"') . '</span></li>';
$expected_links .= '<li class="front-page odd last active"><a href="' . url('<front>') . '" class="active">' . check_plain('Front page') . '</a></li>';
$expected_links .= '</ul>';
$expected = $expected_heading . $expected_links;
$this->assertThemeOutput('links', $variables, $expected);
}
/**
......
......@@ -576,7 +576,7 @@ function system_element_info() {
'#theme_wrappers' => array('container'),
);
$types['actions'] = array(
'#process' => array('form_process_actions', 'form_process_container'),
'#process' => array('form_pre_render_actions_dropbutton', 'form_process_actions', 'form_process_container'),
'#weight' => 100,
'#theme_wrappers' => array('container'),
);
......
......@@ -1537,3 +1537,114 @@ details.fieldset-no-legend {
.entity-meta details .summary {
display: none; /* Hide JS summaries. @todo Rethink summaries. */
}
/**
* Node form dropbuttons.
*/
.form-actions .dropbutton-wrapper li a,
.form-actions .dropbutton-wrapper input {
padding: 5px 17px 6px 17px;
margin-bottom: 0em;
border: medium;
border-radius: 0;
background: none;
}
.form-actions .dropbutton-wrapper input:hover {
background: none;
border: none;
}
.form-actions .button {
background: #fefefe;
background-image: -webkit-linear-gradient(top, #fefefe, #e0e0e0);
background-image: -moz-linear-gradient(top, #fefefe, #e0e0e0);