Issue #2512456 by tim.plunkett, legolasbo, MattA, dawehner, googletorp, darol100, Bojhan, webchick, andypost, ivanstegic, larowlan, lauriii, LewisNyman, tkoleary, rickvug, eliza411, lunk_rat, nickrosencrans, stpaultim, Mark LaCroix: Implement the new block layout design to emphasize the primary interaction of placing a block
parent
2378f939b2
commit
6b95f9a850
|
@ -58,9 +58,6 @@ function block_theme() {
|
|||
'block' => array(
|
||||
'render element' => 'elements',
|
||||
),
|
||||
'block_list' => array(
|
||||
'render element' => 'form',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,15 @@ block.admin_display_theme:
|
|||
_access_theme: 'TRUE'
|
||||
_permission: 'administer blocks'
|
||||
|
||||
block.admin_library:
|
||||
path: 'admin/structure/block/library/{theme}'
|
||||
defaults:
|
||||
_controller: '\Drupal\block\Controller\BlockLibraryController::listBlocks'
|
||||
_title: 'Place block'
|
||||
requirements:
|
||||
_access_theme: 'TRUE'
|
||||
_permission: 'administer blocks'
|
||||
|
||||
block.admin_add:
|
||||
path: '/admin/structure/block/add/{plugin_id}/{theme}'
|
||||
defaults:
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
/* Block listing page */
|
||||
.region-title .button {
|
||||
margin-left: 1em; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .region-title .button {
|
||||
margin-left: 0;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
/* Block demo mode */
|
||||
.block-region {
|
||||
background-color: #ff6;
|
||||
margin-top: 4px;
|
||||
|
@ -22,87 +32,10 @@ a.block-demo-backlink:hover {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.layout-region {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.block-list-secondary {
|
||||
border: 1px solid #bfbfbf;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
.block-list {
|
||||
padding: 0 0.75em;
|
||||
margin: 0;
|
||||
}
|
||||
.block-list li {
|
||||
list-style: none;
|
||||
padding: 0.1em 0;
|
||||
}
|
||||
.block-list a:before {
|
||||
content: '+ ';
|
||||
}
|
||||
.block-list-secondary .form-type-search {
|
||||
padding: 0 1em;
|
||||
}
|
||||
/* Configure block form - Block description */
|
||||
.block-form .form-item-settings-admin-label label {
|
||||
display: inline;
|
||||
}
|
||||
.block-form .form-item-settings-admin-label label:after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
/* Wide screens */
|
||||
@media
|
||||
screen and (min-width: 780px),
|
||||
(orientation: landscape) and (min-device-height: 780px) {
|
||||
|
||||
.block-list-primary {
|
||||
float: left; /* LTR */
|
||||
width: 75%;
|
||||
padding-right: 2em;
|
||||
}
|
||||
[dir="rtl"] .block-list-primary {
|
||||
float: right;
|
||||
padding-left: 2em;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.block-list-secondary {
|
||||
float: right; /* LTR */
|
||||
width: 25%;
|
||||
}
|
||||
[dir="rtl"] .block-list-secondary {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* @todo File an issue to add a standard class to all text-like inputs */
|
||||
.block-list-secondary .form-autocomplete,
|
||||
.block-list-secondary .form-text,
|
||||
.block-list-secondary .form-tel,
|
||||
.block-list-secondary .form-email,
|
||||
.block-list-secondary .form-url,
|
||||
.block-list-secondary .form-search,
|
||||
.block-list-secondary .form-number,
|
||||
.block-list-secondary .form-color,
|
||||
.block-list-secondary textarea {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The vertical toolbar mode gets triggered for narrow screens, which throws off
|
||||
* the intent of media queries written for the viewport width. When the vertical
|
||||
* toolbar is on, we need to suppress layout for the original media width + the
|
||||
* toolbar width (240px). In this case, 240px + 780px.
|
||||
*/
|
||||
@media
|
||||
screen and (max-width: 1020px) {
|
||||
|
||||
.toolbar-vertical.toolbar-tray-open .block-list-primary,
|
||||
.toolbar-vertical.toolbar-tray-open .block-list-secondary {
|
||||
float: none;
|
||||
width: auto;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,66 +19,38 @@
|
|||
Drupal.behaviors.blockFilterByText = {
|
||||
attach: function (context, settings) {
|
||||
var $input = $('input.block-filter-text').once('block-filter-text');
|
||||
var $element = $($input.attr('data-element'));
|
||||
var $blocks;
|
||||
var $details;
|
||||
var $table = $($input.attr('data-element'));
|
||||
var $filter_rows;
|
||||
|
||||
/**
|
||||
* Hides the `<details>` element for a category if it has no visible blocks.
|
||||
*
|
||||
* @param {number} index
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
function hideCategoryDetails(index, element) {
|
||||
var $catDetails = $(element);
|
||||
$catDetails.toggle($catDetails.find('li:visible').length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the block list.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
*/
|
||||
function filterBlockList(e) {
|
||||
var query = $(e.target).val().toLowerCase();
|
||||
|
||||
/**
|
||||
* Shows or hides the block entry based on the query.
|
||||
*
|
||||
* @param {number} index
|
||||
* @param {HTMLElement} block
|
||||
* @param {number} index The index of the block.
|
||||
* @param {HTMLElement} label The label of the block.
|
||||
*/
|
||||
function showBlockEntry(index, block) {
|
||||
var $block = $(block);
|
||||
var $sources = $block.find('.block-filter-text-source');
|
||||
var textMatch = $sources.text().toLowerCase().indexOf(query) !== -1;
|
||||
$block.toggle(textMatch);
|
||||
function toggleBlockEntry(index, label) {
|
||||
var $label = $(label);
|
||||
var $row = $label.parent().parent();
|
||||
var textMatch = $label.text().toLowerCase().indexOf(query) !== -1;
|
||||
$row.toggle(textMatch);
|
||||
}
|
||||
|
||||
// Filter if the length of the query is at least 2 characters.
|
||||
if (query.length >= 2) {
|
||||
$blocks.each(showBlockEntry);
|
||||
|
||||
// Note that we first open all <details> to be able to use ':visible'.
|
||||
// Mark the <details> elements that were closed before filtering, so
|
||||
// they can be reclosed when filtering is removed.
|
||||
$details.not('[open]').attr('data-drupal-block-state', 'forced-open');
|
||||
// Hide the category <details> if they don't have any visible rows.
|
||||
$details.attr('open', 'open').each(hideCategoryDetails);
|
||||
$filter_rows.each(toggleBlockEntry);
|
||||
}
|
||||
else {
|
||||
$blocks.show();
|
||||
$details.show();
|
||||
// Return <details> elements that had been closed before filtering
|
||||
// to a closed state.
|
||||
$details.filter('[data-drupal-block-state="forced-open"]').removeAttr('open data-drupal-block-state');
|
||||
$filter_rows.each(function (index) {
|
||||
$(this).parent().parent().show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ($element.length) {
|
||||
$details = $element.find('details');
|
||||
$blocks = $details.find('li');
|
||||
|
||||
if ($table.length) {
|
||||
$filter_rows = $table.find('div.block-filter-text-source');
|
||||
$input.on('keyup', filterBlockList);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,11 +170,13 @@ class BlockForm extends EntityForm {
|
|||
}
|
||||
|
||||
// Region settings.
|
||||
$entity_region = $entity->getRegion();
|
||||
$region = $entity->isNew() ? $this->getRequest()->query->get('region', $entity_region) : $entity_region;
|
||||
$form['region'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Region'),
|
||||
'#description' => $this->t('Select the region where this block should be displayed.'),
|
||||
'#default_value' => $entity->getRegion(),
|
||||
'#default_value' => $region,
|
||||
'#empty_value' => BlockInterface::BLOCK_REGION_NONE,
|
||||
'#options' => system_region_list($theme, REGIONS_VISIBLE),
|
||||
'#prefix' => '<div id="edit-block-region-wrapper">',
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
namespace Drupal\block;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Block\BlockManagerInterface;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
|
||||
|
@ -44,13 +43,6 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* The block manager.
|
||||
*
|
||||
* @var \Drupal\Core\Block\BlockManagerInterface
|
||||
*/
|
||||
protected $blockManager;
|
||||
|
||||
/**
|
||||
* The theme manager.
|
||||
*
|
||||
|
@ -77,17 +69,14 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
* The entity type definition.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
|
||||
* The entity storage class.
|
||||
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
|
||||
* The block manager.
|
||||
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
|
||||
* The theme manager.
|
||||
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
|
||||
* The form builder.
|
||||
*/
|
||||
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, BlockManagerInterface $block_manager, ThemeManagerInterface $theme_manager, FormBuilderInterface $form_builder) {
|
||||
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, ThemeManagerInterface $theme_manager, FormBuilderInterface $form_builder) {
|
||||
parent::__construct($entity_type, $storage);
|
||||
|
||||
$this->blockManager = $block_manager;
|
||||
$this->themeManager = $theme_manager;
|
||||
$this->formBuilder = $form_builder;
|
||||
}
|
||||
|
@ -99,7 +88,6 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
return new static(
|
||||
$entity_type,
|
||||
$container->get('entity.manager')->getStorage($entity_type->id()),
|
||||
$container->get('plugin.manager.block'),
|
||||
$container->get('theme.manager'),
|
||||
$container->get('form_builder')
|
||||
);
|
||||
|
@ -135,7 +123,6 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$form['#theme'] = array('block_list');
|
||||
$form['#attached']['library'][] = 'core/drupal.tableheader';
|
||||
$form['#attached']['library'][] = 'block/drupal.block';
|
||||
$form['#attached']['library'][] = 'block/drupal.block.admin';
|
||||
|
@ -143,7 +130,6 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
|
||||
// Build the form tree.
|
||||
$form['blocks'] = $this->buildBlocksForm();
|
||||
$form['place_blocks'] = $this->buildPlaceBlocksForm();
|
||||
|
||||
$form['actions'] = array(
|
||||
'#tree' => FALSE,
|
||||
|
@ -206,7 +192,7 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
|
||||
// Loop over each region and build blocks.
|
||||
$regions = $this->systemRegionList($this->getThemeName(), REGIONS_VISIBLE);
|
||||
$block_regions_with_disabled = $regions + array(BlockInterface::BLOCK_REGION_NONE => BlockInterface::BLOCK_REGION_NONE);
|
||||
$block_regions_with_disabled = $regions + array(BlockInterface::BLOCK_REGION_NONE => $this->t('Disabled', array(), array('context' => 'Plural')));
|
||||
foreach ($block_regions_with_disabled as $region => $title) {
|
||||
$form['#tabledrag'][] = array(
|
||||
'action' => 'match',
|
||||
|
@ -229,10 +215,20 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
),
|
||||
);
|
||||
$form['region-' . $region]['title'] = array(
|
||||
'#markup' => $region != BlockInterface::BLOCK_REGION_NONE ? $title : $this->t('Disabled', array(), array('context' => 'Plural')),
|
||||
'#prefix' => $region != BlockInterface::BLOCK_REGION_NONE ? $title : $block_regions_with_disabled[$region],
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Place block <span class="visually-hidden">in the %region region</span>', ['%region' => $block_regions_with_disabled[$region]]),
|
||||
'#url' => Url::fromRoute('block.admin_library', ['theme' => $this->getThemeName()], ['query' => ['region' => $region]]),
|
||||
'#wrapper_attributes' => array(
|
||||
'colspan' => 5,
|
||||
),
|
||||
'#attributes' => [
|
||||
'class' => ['use-ajax', 'button', 'button--small'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 700,
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
||||
$form['region-' . $region . '-message'] = array(
|
||||
|
@ -312,78 +308,6 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
|
|||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the "Place Blocks" portion of the form.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function buildPlaceBlocksForm() {
|
||||
$form['title'] = array(
|
||||
'#type' => 'container',
|
||||
'#markup' => '<h3>' . $this->t('Place blocks') . '</h3>',
|
||||
'#attributes' => array(
|
||||
'class' => array(
|
||||
'entity-meta__header',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$form['filter'] = array(
|
||||
'#type' => 'search',
|
||||
'#title' => $this->t('Filter'),
|
||||
'#title_display' => 'invisible',
|
||||
'#size' => 30,
|
||||
'#placeholder' => $this->t('Filter by block name'),
|
||||
'#attributes' => array(
|
||||
'class' => array('block-filter-text'),
|
||||
'data-element' => '.entity-meta',
|
||||
'title' => $this->t('Enter a part of the block name to filter by.'),
|
||||
),
|
||||
);
|
||||
|
||||
$form['list']['#type'] = 'container';
|
||||
$form['list']['#attributes']['class'][] = 'entity-meta';
|
||||
|
||||
// Only add blocks which work without any available context.
|
||||
$definitions = $this->blockManager->getDefinitionsForContexts();
|
||||
$sorted_definitions = $this->blockManager->getSortedDefinitions($definitions);
|
||||
foreach ($sorted_definitions as $plugin_id => $plugin_definition) {
|
||||
$category = SafeMarkup::checkPlain($plugin_definition['category']);
|
||||
$category_key = 'category-' . $category;
|
||||
if (!isset($form['list'][$category_key])) {
|
||||
$form['list'][$category_key] = array(
|
||||
'#type' => 'details',
|
||||
'#title' => $category,
|
||||
'#open' => TRUE,
|
||||
'content' => array(
|
||||
'#theme' => 'links',
|
||||
'#links' => array(),
|
||||
'#attributes' => array(
|
||||
'class' => array(
|
||||
'block-list',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
$form['list'][$category_key]['content']['#links'][$plugin_id] = array(
|
||||
'title' => $plugin_definition['admin_label'],
|
||||
'url' => Url::fromRoute('block.admin_add', [
|
||||
'plugin_id' => $plugin_id,
|
||||
'theme' => $this->theme
|
||||
]),
|
||||
'attributes' => array(
|
||||
'class' => array('use-ajax', 'block-filter-text-source'),
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode(array(
|
||||
'width' => 700,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the theme used for this block listing.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\block\Controller\BlockLibraryController.
|
||||
*/
|
||||
|
||||
namespace Drupal\block\Controller;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Block\BlockManagerInterface;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
||||
use Drupal\Core\Menu\LocalActionManagerInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Provides a list of block plugins to be added to the layout.
|
||||
*/
|
||||
class BlockLibraryController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* The block manager.
|
||||
*
|
||||
* @var \Drupal\Core\Block\BlockManagerInterface
|
||||
*/
|
||||
protected $blockManager;
|
||||
|
||||
/**
|
||||
* The route match.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RouteMatchInterface
|
||||
*/
|
||||
protected $routeMatch;
|
||||
|
||||
/**
|
||||
* The local action manager.
|
||||
*
|
||||
* @var \Drupal\Core\Menu\LocalActionManagerInterface
|
||||
*/
|
||||
protected $localActionManager;
|
||||
|
||||
/**
|
||||
* Constructs a BlockLibraryController object.
|
||||
*
|
||||
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
|
||||
* The block manager.
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The current route match.
|
||||
* @param \Drupal\Core\Menu\LocalActionManagerInterface $local_action_manager
|
||||
* The local action manager.
|
||||
*/
|
||||
public function __construct(BlockManagerInterface $block_manager, RouteMatchInterface $route_match, LocalActionManagerInterface $local_action_manager) {
|
||||
$this->blockManager = $block_manager;
|
||||
$this->routeMatch = $route_match;
|
||||
$this->localActionManager = $local_action_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('plugin.manager.block'),
|
||||
$container->get('current_route_match'),
|
||||
$container->get('plugin.manager.menu.local_action')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a list of blocks that can be added to a theme's layout.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The current request.
|
||||
* @param string $theme
|
||||
* Theme key of the block list.
|
||||
*
|
||||
* @return array
|
||||
* A render array as expected by the renderer.
|
||||
*/
|
||||
public function listBlocks(Request $request, $theme) {
|
||||
// Since modals do not render any other part of the page, we need to render
|
||||
// them manually as part of this listing.
|
||||
if ($request->query->get(MainContentViewSubscriber::WRAPPER_FORMAT) === 'drupal_modal') {
|
||||
$build['local_actions'] = $this->buildLocalActions();
|
||||
}
|
||||
|
||||
$headers = [
|
||||
['data' => $this->t('Block')],
|
||||
['data' => $this->t('Category')],
|
||||
['data' => $this->t('Operations')],
|
||||
];
|
||||
|
||||
// Only add blocks which work without any available context.
|
||||
$definitions = $this->blockManager->getDefinitionsForContexts();
|
||||
// Order by category, and then by admin label.
|
||||
$definitions = $this->blockManager->getSortedDefinitions($definitions);
|
||||
|
||||
$region = $request->query->get('region');
|
||||
$rows = [];
|
||||
foreach ($definitions as $plugin_id => $plugin_definition) {
|
||||
$row = [];
|
||||
$row['title']['data'] = [
|
||||
'#markup' => $plugin_definition['admin_label'],
|
||||
'#prefix' => '<div class="block-filter-text-source">',
|
||||
'#suffix' => '</div>',
|
||||
];
|
||||
$row['category']['data'] = SafeMarkup::checkPlain($plugin_definition['category']);
|
||||
$links['add'] = [
|
||||
'title' => $this->t('Place block'),
|
||||
'url' => Url::fromRoute('block.admin_add', ['plugin_id' => $plugin_id, 'theme' => $theme]),
|
||||
'attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 700,
|
||||
]),
|
||||
],
|
||||
];
|
||||
if ($region) {
|
||||
$links['add']['query']['region'] = $region;
|
||||
}
|
||||
$row['operations']['data'] = [
|
||||
'#type' => 'operations',
|
||||
'#links' => $links,
|
||||
];
|
||||
$rows[] = $row;
|
||||
}
|
||||
|
||||
$build['#attached']['library'][] = 'block/drupal.block.admin';
|
||||
|
||||
$build['filter'] = [
|
||||
'#type' => 'search',
|
||||
'#title' => $this->t('Filter'),
|
||||
'#title_display' => 'invisible',
|
||||
'#size' => 30,
|
||||
'#placeholder' => $this->t('Filter by block name'),
|
||||
'#attributes' => [
|
||||
'class' => ['block-filter-text'],
|
||||
'data-element' => '.block-add-table',
|
||||
'title' => $this->t('Enter a part of the block name to filter by.'),
|
||||
],
|
||||
];
|
||||
|
||||
$build['blocks'] = [
|
||||
'#type' => 'table',
|
||||
'#header' => $headers,
|
||||
'#rows' => $rows,
|
||||
'#empty' => $this->t('No blocks available.'),
|
||||
'#attributes' => [
|
||||
'class' => ['block-add-table'],
|
||||
],
|
||||
];
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the local actions for this listing.
|
||||
*
|
||||
* @return array
|
||||
* An array of local actions for this listing.
|
||||
*/
|
||||
protected function buildLocalActions() {
|
||||
$build = $this->localActionManager->getActionsForRoute($this->routeMatch->getRouteName());
|
||||
// Without this workaround, the action links will be rendered as <li> with
|
||||
// no wrapping <ul> element.
|
||||
if (!empty($build)) {
|
||||
$build['#prefix'] = '<ul class="action-links">';
|
||||
$build['#suffix'] = '</ul>';
|
||||
}
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
|
@ -69,7 +69,7 @@ class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface {
|
|||
}
|
||||
// Default task!
|
||||
if ($default_theme == $theme_name) {
|
||||
$this->derivatives[$theme_name]['route_name'] = 'block.admin_display';
|
||||
$this->derivatives[$theme_name]['route_name'] = $base_plugin_definition['parent_id'];
|
||||
// Emulate default logic because without the base plugin id we can't
|
||||
// change the base_route.
|
||||
$this->derivatives[$theme_name]['weight'] = -10;
|
||||
|
|
|
@ -62,6 +62,7 @@ class BlockLanguageCacheTest extends WebTestBase {
|
|||
// Create the block cache for all languages.
|
||||
foreach ($this->langcodes as $langcode) {
|
||||
$this->drupalGet('admin/structure/block', array('language' => $langcode));
|
||||
$this->clickLinkPartialName('Place block');
|
||||
}
|
||||
|
||||
// Create a menu in the default language.
|
||||
|
@ -73,6 +74,7 @@ class BlockLanguageCacheTest extends WebTestBase {
|
|||
// Check that the block is listed for all languages.
|
||||
foreach ($this->langcodes as $langcode) {
|
||||
$this->drupalGet('admin/structure/block', array('language' => $langcode));
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->assertText($edit['label']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\block\Tests\BlockTitleXSSTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\block\Tests;
|
||||
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* Tests block XSS in title.
|
||||
*
|
||||
* @group block
|
||||
*/
|
||||
class BlockTitleXSSTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('block', 'block_test');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->drupalPlaceBlock('test_xss_title', array('label' => '<script>alert("XSS label");</script>'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test XSS in title.
|
||||
*/
|
||||
function testXSSInTitle() {
|
||||
\Drupal::state()->set('block_test.content', $this->randomMachineName());
|
||||
$this->drupalGet('');
|
||||
$this->assertNoRaw('<script>alert("XSS label");</script>', 'The block title was properly sanitized when rendered.');
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser(array('administer blocks', 'access administration pages')));
|
||||
$default_theme = $this->config('system.theme')->get('default');
|
||||
$this->drupalGet('admin/structure/block/list/' . $default_theme);
|
||||
$this->assertNoRaw("<script>alert('XSS subject');</script>", 'The block title was properly sanitized in Block Plugin UI Admin page.');
|
||||
}
|
||||
|
||||
}
|
|
@ -139,14 +139,15 @@ class BlockUiTest extends WebTestBase {
|
|||
*/
|
||||
public function testCandidateBlockList() {
|
||||
$arguments = array(
|
||||
':ul_class' => 'block-list',
|
||||
':li_class' => 'test-block-instantiation',
|
||||
':title' => 'Display message',
|
||||
':category' => 'Block test',
|
||||
':href' => 'admin/structure/block/add/test_block_instantiation/classy',
|
||||
':text' => 'Display message',
|
||||
);
|
||||
$pattern = '//tr[.//td/div[text()=:title] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]';
|
||||
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$elements = $this->xpath('//details[@id="edit-category-block-test"]//ul[contains(@class, :ul_class)]/li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$elements = $this->xpath($pattern, $arguments);
|
||||
$this->assertTrue(!empty($elements), 'The test block appears in the category for its module.');
|
||||
|
||||
// Trigger the custom category addition in block_test_block_alter().
|
||||
|
@ -154,7 +155,9 @@ class BlockUiTest extends WebTestBase {
|
|||
$this->container->get('plugin.manager.block')->clearCachedDefinitions();
|
||||
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$elements = $this->xpath('//details[@id="edit-category-custom-category"]//ul[contains(@class, :ul_class)]/li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$arguments[':category'] = 'Custom category';
|
||||
$elements = $this->xpath($pattern, $arguments);
|
||||
$this->assertTrue(!empty($elements), 'The test block appears in a custom category controlled by block_test_block_alter().');
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,34 @@ class BlockXssTest extends WebTestBase {
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['block', 'block_content', 'menu_ui', 'views'];
|
||||
public static $modules = ['block', 'block_content', 'block_test', 'menu_ui', 'views'];
|
||||
|
||||
/**
|
||||
* Test XSS in title.
|
||||
*/
|
||||
public function testXssInTitle() {
|
||||
$this->drupalPlaceBlock('test_xss_title', ['label' => '<script>alert("XSS label");</script>']);
|
||||
|
||||
\Drupal::state()->set('block_test.content', $this->randomMachineName());
|
||||
$this->drupalGet('');
|
||||
$this->assertNoRaw('<script>alert("XSS label");</script>', 'The block title was properly sanitized when rendered.');
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser(['administer blocks', 'access administration pages']));
|
||||
$default_theme = $this->config('system.theme')->get('default');
|
||||
$this->drupalGet('admin/structure/block/list/' . $default_theme);
|
||||
$this->assertNoRaw("<script>alert('XSS subject');</script>", 'The block title was properly sanitized in Block Plugin UI Admin page.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests XSS in category.
|
||||
*/
|
||||
public function testXssInCategory() {
|
||||
$this->drupalPlaceBlock('test_xss_title');
|
||||
$this->drupalLogin($this->drupalCreateUser(['administer blocks', 'access administration pages']));
|
||||
$this->drupalGet(Url::fromRoute('block.admin_display'));
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->assertNoRaw("<script>alert('XSS category');</script>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests various modules that provide blocks for XSS.
|
||||
|
@ -51,8 +78,9 @@ class BlockXssTest extends WebTestBase {
|
|||
$view->save();
|
||||
|
||||
$this->drupalGet(Url::fromRoute('block.admin_display'));
|
||||
$this->clickLink('<script>alert("view");</script>');
|
||||
$this->assertRaw('<script>alert("view");</script>');
|
||||
$this->clickLinkPartialName('Place block');
|
||||
// The block admin label is automatically XSS admin filtered.
|
||||
$this->assertRaw('alert("view");');
|
||||
$this->assertNoRaw('<script>alert("view");</script>');
|
||||
}
|
||||
|
||||
|
@ -66,8 +94,9 @@ class BlockXssTest extends WebTestBase {
|
|||
])->save();
|
||||
|
||||
$this->drupalGet(Url::fromRoute('block.admin_display'));
|
||||
$this->clickLink('<script>alert("menu");</script>');
|
||||
$this->assertRaw('<script>alert("menu");</script>');
|
||||
$this->clickLinkPartialName('Place block');
|
||||
// The block admin label is automatically XSS admin filtered.
|
||||
$this->assertRaw('alert("menu");');
|
||||
$this->assertNoRaw('<script>alert("menu");</script>');
|
||||
}
|
||||
|
||||
|
@ -86,8 +115,9 @@ class BlockXssTest extends WebTestBase {
|
|||
])->save();
|
||||
|
||||
$this->drupalGet(Url::fromRoute('block.admin_display'));
|
||||
$this->clickLink('<script>alert("block_content");</script>');
|
||||
$this->assertRaw('<script>alert("block_content");</script>');
|
||||
$this->clickLinkPartialName('Place block');
|
||||
// The block admin label is automatically XSS admin filtered.
|
||||
$this->assertRaw('alert("block_content");');
|
||||
$this->assertNoRaw('<script>alert("block_content");</script>');
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
namespace Drupal\block\Tests\Views;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\views\Views;
|
||||
use Drupal\views\Tests\ViewTestBase;
|
||||
use Drupal\views\Tests\ViewTestData;
|
||||
|
@ -60,19 +58,20 @@ class DisplayBlockTest extends ViewTestBase {
|
|||
$edit['block[style][row_plugin]'] = 'fields';
|
||||
$this->drupalPostForm('admin/structure/views/add', $edit, t('Save and edit'));
|
||||
|
||||
$pattern = '//tr[.//td[text()=:category] and .//td//a[contains(@href, :href)]]';
|
||||
|
||||
// Test that the block was given a default category corresponding to its
|
||||
// base table.
|
||||
$arguments = array(
|
||||
':id' => 'edit-category-lists-views',
|
||||
':li_class' => 'views-block' . Html::getClass($edit['id']) . '-block-1',
|
||||
':href' => \Drupal::Url('block.admin_add', array(
|
||||
'plugin_id' => 'views_block:' . $edit['id'] . '-block_1',
|
||||
'theme' => 'classy',
|
||||
)),
|
||||
':text' => $edit['label'],
|
||||
':category' => t('Lists (Views)'),
|
||||
);
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$elements = $this->xpath('//details[@id=:id]//li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$elements = $this->xpath($pattern, $arguments);
|
||||
$this->assertTrue(!empty($elements), 'The test block appears in the category for its base table.');
|
||||
|
||||
// Duplicate the block before changing the category.
|
||||
|
@ -81,10 +80,9 @@ class DisplayBlockTest extends ViewTestBase {
|
|||
|
||||
// Change the block category to a random string.
|
||||
$this->drupalGet('admin/structure/views/view/' . $edit['id'] . '/edit/block_1');
|
||||
$label = t('Lists (Views)');
|
||||
$link = $this->xpath('//a[@id="views-block-1-block-category" and normalize-space(text())=:label]', array(':label' => $label));
|
||||
$link = $this->xpath('//a[@id="views-block-1-block-category" and normalize-space(text())=:category]', $arguments);
|
||||
$this->assertTrue(!empty($link));
|
||||
$this->clickLink($label);
|
||||
$this->clickLink(t('Lists (Views)'));
|
||||
$category = $this->randomString();
|
||||
$this->drupalPostForm(NULL, array('block_category' => $category), t('Apply'));
|
||||
|
||||
|
@ -95,34 +93,30 @@ class DisplayBlockTest extends ViewTestBase {
|
|||
$this->drupalPostForm(NULL, array(), t('Save'));
|
||||
|
||||
// Test that the blocks are listed under the correct categories.
|
||||
$category_id = Html::getUniqueId('edit-category-' . SafeMarkup::checkPlain($category));
|
||||
$arguments[':id'] = $category_id;
|
||||
$arguments[':category'] = $category;
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$elements = $this->xpath('//details[@id=:id]//li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$elements = $this->xpath($pattern, $arguments);
|
||||
$this->assertTrue(!empty($elements), 'The test block appears in the custom category.');
|
||||
|
||||
$arguments = array(
|
||||
':id' => 'edit-category-lists-views',
|
||||
':li_class' => 'views-block' . Html::getClass($edit['id']) . '-block-2',
|
||||
':href' => \Drupal::Url('block.admin_add', array(
|
||||
'plugin_id' => 'views_block:' . $edit['id'] . '-block_2',
|
||||
'theme' => 'classy',
|
||||
)),
|
||||
':text' => $edit['label'],
|
||||
':category' => t('Lists (Views)'),
|
||||
);
|
||||
$elements = $this->xpath('//details[@id=:id]//li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
|
||||
$elements = $this->xpath($pattern, $arguments);
|
||||
$this->assertTrue(!empty($elements), 'The first duplicated test block remains in the original category.');
|
||||
|
||||
$arguments = array(
|
||||
':id' => $category_id,
|
||||
':li_class' => 'views-block' . Html::getClass($edit['id']) . '-block-3',
|
||||
':href' => \Drupal::Url('block.admin_add', array(
|
||||
'plugin_id' => 'views_block:' . $edit['id'] . '-block_3',
|
||||
'theme' => 'classy',
|
||||
)),
|
||||
':text' => $edit['label'],
|
||||
':category' => $category,
|
||||
);
|
||||
$elements = $this->xpath('//details[@id=:id]//li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
|
||||
$elements = $this->xpath($pattern, $arguments);
|
||||
$this->assertTrue(!empty($elements), 'The second duplicated test block appears in the custom category.');
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ block_content_add_action:
|
|||
route_name: block_content.add_page
|
||||
title: 'Add custom block'
|
||||
appears_on:
|
||||
- block.admin_display
|
||||
- block.admin_display_theme
|
||||
- block.admin_library
|
||||
- entity.block_content.collection
|
||||
class: \Drupal\block_content\Plugin\Menu\LocalAction\BlockContentAddLocalAction
|
||||
|
|
|
@ -189,6 +189,7 @@ class BlockContentTypeTest extends BlockContentTestBase {
|
|||
// block configure form.
|
||||
$path = $theme == $default_theme ? 'admin/structure/block' : "admin/structure/block/list/$theme";
|
||||
$this->drupalGet($path);
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->clickLink(t('Add custom block'));
|
||||
// The seven theme has markup inside the link, we cannot use clickLink().
|
||||
if ($default_theme == 'seven') {
|
||||
|
|
|
@ -219,6 +219,7 @@ class MenuTest extends MenuWebTestBase {
|
|||
|
||||
// Confirm that the custom menu block is available.
|
||||
$this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->assertText($label);
|
||||
|
||||
// Enable the block.
|
||||
|
@ -532,6 +533,7 @@ class MenuTest extends MenuWebTestBase {
|
|||
// Make sure menu shows up with new name in block addition.
|
||||
$default_theme = $this->config('system.theme')->get('default');
|
||||
$this->drupalget('admin/structure/block/list/' . $default_theme);
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->assertText($edit['label']);
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ class SearchBlockTest extends SearchTestBase {
|
|||
|
||||
// Test availability of the search block in the admin "Place blocks" list.
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->assertLinkByHref('/admin/structure/block/add/search_form_block/classy', 0,
|
||||
'Did not find the search block in block candidate list.');
|
||||
|
||||
|
|
|
@ -2324,7 +2324,7 @@ abstract class WebTestBase extends TestBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Follows a link by name.
|
||||
* Follows a link by complete name.
|
||||
*
|
||||
* Will click the first link found with this link text by default, or a later
|
||||
* one if an index is given. Match is case sensitive with normalized space.
|
||||
|
@ -2332,17 +2332,54 @@ abstract class WebTestBase extends TestBase {
|
|||
*
|
||||
* If the link is discovered and clicked, the test passes. Fail otherwise.
|
||||
*
|
||||
* @param $label
|
||||
* @param string $label
|
||||
* Text between the anchor tags.
|
||||
* @param $index
|
||||
* @param int $index
|
||||
* Link position counting from zero.
|
||||
*
|
||||
* @return
|
||||
* @return string|bool
|
||||
* Page contents on success, or FALSE on failure.
|
||||
*/
|
||||
protected function clickLink($label, $index = 0) {
|
||||
return $this->clickLinkHelper($label, $index, '//a[normalize-space()=:label]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Follows a link by partial name.
|
||||
*
|
||||
*
|
||||
* If the link is discovered and clicked, the test passes. Fail otherwise.
|
||||
*
|
||||
* @param string $label
|
||||
* Text between the anchor tags, uses starts-with().
|
||||
* @param int $index
|
||||
* Link position counting from zero.
|
||||
*
|
||||
* @return string|bool
|
||||
* Page contents on success, or FALSE on failure.
|
||||
*
|
||||
* @see ::clickLink()
|
||||
*/
|
||||
protected function clickLinkPartialName($label, $index = 0) {
|
||||
return $this->clickLinkHelper($label, $index, '//a[starts-with(normalize-space(), :label)]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a helper for ::clickLink() and ::clickLinkPartialName().
|
||||
*
|
||||
* @param string $label
|
||||
* Text between the anchor tags, uses starts-with().
|
||||
* @param int $index
|
||||
* Link position counting from zero.
|
||||
* @param string $pattern
|
||||
* A pattern to use for the XPath.
|
||||
*
|
||||
* @return bool|string
|
||||
* Page contents on success, or FALSE on failure.
|
||||
*/
|
||||
protected function clickLinkHelper($label, $index, $pattern) {
|
||||
$url_before = $this->getUrl();
|
||||
$urls = $this->xpath('//a[normalize-space()=:label]', array(':label' => $label));
|
||||
$urls = $this->xpath($pattern, array(':label' => $label));
|
||||
if (isset($urls[$index])) {
|
||||
$url_target = $this->getAbsoluteUrl($urls[$index]['href']);
|
||||
$this->pass(SafeMarkup::format('Clicked link %label (@url_target) from @url_before', array('%label' => $label, '@url_target' => $url_target, '@url_before' => $url_before)), 'Browser');
|
||||
|
|
|
@ -133,6 +133,7 @@ class BasicTest extends WizardTestBase {
|
|||
|
||||
// Confirm that the block is available in the block administration UI.
|
||||
$this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->assertText($view3['label']);
|
||||
|
||||
// Place the block.
|
||||
|
|
|
@ -71,6 +71,7 @@ class ItemsPerPageTest extends WizardTestBase {
|
|||
|
||||
// Confirm that the block is listed in the block administration UI.
|
||||
$this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->assertText($view['label']);
|
||||
|
||||
// Place the block, visit a page that displays the block, and check that the
|
||||
|
|
|
@ -50,6 +50,7 @@ class OverrideDisplaysTest extends UITestBase {
|
|||
|
||||
// Confirm that the view block is available in the block administration UI.
|
||||
$this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->assertText($view['label']);
|
||||
|
||||
// Place the block.
|
||||
|
@ -109,6 +110,7 @@ class OverrideDisplaysTest extends UITestBase {
|
|||
|
||||
// Confirm that the block is available in the block administration UI.
|
||||
$this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
|
||||
$this->clickLinkPartialName('Place block');
|
||||
$this->assertText($view['label']);
|
||||
|
||||
// Put the block into the first sidebar region, and make sure it will not
|
||||
|
|
Loading…
Reference in New Issue