Commit 7844d041 authored by alexpott's avatar alexpott
Browse files

Issue #2724819 by SKAUGHT, pwolanin, tim.plunkett, tkoleary, tedbow, webchick,...

Issue #2724819 by SKAUGHT, pwolanin, tim.plunkett, tkoleary, tedbow, webchick, yoroy, Wim Leers, brantwynn, Bojhan: Create experimental module for place block on any page feature
parent a1529fe9
......@@ -52,6 +52,7 @@
"drupal/big_pipe": "self.version",
"drupal/block": "self.version",
"drupal/block_content": "self.version",
"drupal/block_place": "self.version",
"drupal/book": "self.version",
"drupal/breakpoint": "self.version",
"drupal/ckeditor": "self.version",
......
name: Place Blocks
type: module
description: 'Allow administrators to place blocks from any Drupal page'
package: Core (Experimental)
version: VERSION
core: 8.x
dependencies:
- block
drupal.block_place:
version: VERSION
css:
theme:
css/block-place.css: {}
drupal.block_place.icons:
version: VERSION
css:
theme:
css/block-place.icons.theme.css: {}
<?php
/**
* @file
* Controls the placement of blocks from all pages.
*/
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
/**
* Implements hook_help().
*/
function block_place_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.block_place':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Place Blocks module module allows you to place blocks from every page. For more information, see the <a href=":blocks-documentation">online documentation for the Place Blocks module</a>.', [':blocks-documentation' => 'https://www.drupal.org/documentation/modules/block_place/']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<p>' . t('Block placement is specific to each theme on your site. This module allows you to place blocks in the context of your content pages') . '</p>';
return $output;
}
}
/**
* Implements hook_toolbar().
*/
function block_place_toolbar() {
// Link to the current page with a query parameter.
$query = \Drupal::request()->query->all();
$wrapper_class = '';
$status_class = '';
$description = '';
if (isset($query['block-place'])) {
$status_class = 'active';
$wrapper_class = 'is-active';
$description = t('Exit Place block mode.');
unset($query['block-place']);
unset($query['destination']);
}
else {
$status_class = 'inactive';
$description = t('Show regions to Place blocks.');
$query['block-place'] = '1';
// Setting destination is both a work-around for the toolbar "Back to site"
// link in escapeAdmin.js and used for the destination after picking a
// block.
$query['destination'] = Url::fromRoute('<current>')->toString();
}
// Remove on Admin routes.
$admin_route = \Drupal::service('router.admin_context')->isAdminRoute();
// Remove on Block Demo page.
$admin_demo = \Drupal::routeMatch()->getRouteName() === 'block.admin_demo';
$access = (\Drupal::currentUser()->hasPermission('administer blocks') && !$admin_route && !$admin_demo);
// The 'Place Block' tab is a simple link, with no corresponding tray.
$items['block_place'] = [
'#cache' => [
'contexts' => ['user.permissions', 'url.query_args'],
],
'#type' => 'toolbar_item',
'tab' => [
'#access' => $access,
'#type' => 'link',
'#title' => t('Place block'),
'#url' => Url::fromRoute('<current>', [], ['query' => $query]),
'#attributes' => [
'title' => $description,
'class' => ['toolbar-icon', 'toolbar-icon-place-block-' . $status_class],
],
],
'#wrapper_attributes' => [
'class' => ['toolbar-tab', 'block-place-toolbar-tab', $wrapper_class],
],
'#weight' => 100,
'#attached' => [
'library' => [
'block_place/drupal.block_place.icons',
],
],
];
return $items;
}
services:
block_place.page_display_variant_subscriber.block:
class: Drupal\block_place\EventSubscriber\BlockPlaceEventSubscriber
arguments: ['@request_stack', '@current_user']
tags:
- { name: event_subscriber }
/**
* @file
* Styling for block_place module regions and buttons during block placement.
*/
/* Borrows styling from Bartik's demo-block.css. */
.block-place-region {
background: #ffff66;
border: 1px dotted #9f9e00;
font: 90% "Lucida Grande", "Lucida Sans Unicode", sans-serif;
margin: 5px;
padding: 5px;
text-align: center;
text-shadow: none;
}
.block-place-region a.button {
white-space: nowrap;
color: #3a3a3a;
}
.region:hover {
outline: 1px dashed #9f9e00;
}
/**
* @file
* Styling for block_place module toolbar icons.
*/
.toolbar .block-place-toolbar-tab.is-active {
background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.25) 20%, transparent 200%);
background-image: linear-gradient(rgba(255, 255, 255, 0.25) 20%, transparent 200%);
}
.toolbar .toolbar-bar .block-place-toolbar-tab {
float: right;
}
[dir="rtl"] .toolbar .toolbar-bar .block-place-toolbar-tab {
float: left;
}
.toolbar-bar .toolbar-icon-place-block-active:before {
background-image: url(../icons/ffffff/place-block.svg);
}
.toolbar-bar .toolbar-icon-place-block-inactive:before {
background-image: url(../icons/bebebe/place-block.svg);
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0" y="0" width="16" height="16" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve"><rect x="1" y="6" fill="#bebebe" width="14" height="9"/><ellipse fill="#bebebe" cx="11.48" cy="3.68" rx="2.23" ry="0.61"/><rect x="9.25" y="3.68" fill="#bebebe" width="4.45" height="3.35"/><ellipse fill="#bebebe" cx="4.48" cy="3.68" rx="2.23" ry="0.61"/><rect x="2.25" y="3.68" fill="#bebebe" width="4.45" height="3.35"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0" y="0" width="16" height="16" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve"><rect x="1" y="6" fill="#ffffff" width="14" height="9"/><ellipse fill="#ffffff" cx="11.48" cy="3.68" rx="2.23" ry="0.61"/><rect x="9.25" y="3.68" fill="#ffffff" width="4.45" height="3.35"/><ellipse fill="#ffffff" cx="4.48" cy="3.68" rx="2.23" ry="0.61"/><rect x="2.25" y="3.68" fill="#ffffff" width="4.45" height="3.35"/></svg>
<?php
namespace Drupal\block_place\EventSubscriber;
use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
use Drupal\Core\Render\RenderEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\Core\Session\AccountInterface;
/**
* @see \Drupal\block_place\Plugin\DisplayVariant\PlaceBlockPageVariant
*/
class BlockPlaceEventSubscriber implements EventSubscriberInterface {
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Constructs a \Drupal\block_place\EventSubscriber\BlockPlaceEventSubscriber object.
*
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack used to retrieve the current request.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(RequestStack $request_stack, AccountInterface $account) {
$this->requestStack = $request_stack;
$this->account = $account;
}
/**
* Selects the block place override of the block page display variant.
*
* @param \Drupal\Core\Render\PageDisplayVariantSelectionEvent $event
* The event to process.
*/
public function onBlockPageDisplayVariantSelected(PageDisplayVariantSelectionEvent $event) {
if ($event->getPluginId() === 'block_page') {
if ($this->requestStack->getCurrentRequest()->query->has('block-place') && $this->account->hasPermission('administer blocks')) {
$event->setPluginId('block_place_page');
}
$event->addCacheContexts(['user.permissions', 'url.query_args']);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Set a very low priority, so that it runs last.
$events[RenderEvents::SELECT_PAGE_DISPLAY_VARIANT][] = ['onBlockPageDisplayVariantSelected', -1000];
return $events;
}
}
<?php
namespace Drupal\block_place\Plugin\DisplayVariant;
use Drupal\block\BlockRepositoryInterface;
use Drupal\block\Plugin\DisplayVariant\BlockPageVariant;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Core\Link;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Allows blocks to be placed directly within a region.
*
* @PageDisplayVariant(
* id = "block_place_page",
* admin_label = @Translation("Page with blocks and place block buttons")
* )
*/
class PlaceBlockPageVariant extends BlockPageVariant {
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a new PlaceBlockPageVariant.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\block\BlockRepositoryInterface $block_repository
* The block repository.
* @param \Drupal\Core\Entity\EntityViewBuilderInterface $block_view_builder
* The block view builder.
* @param string[] $block_list_cache_tags
* The Block entity type list cache tags.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The current request stack.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, BlockRepositoryInterface $block_repository, EntityViewBuilderInterface $block_view_builder, array $block_list_cache_tags, ThemeManagerInterface $theme_manager, RequestStack $request_stack) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $block_repository, $block_view_builder, $block_list_cache_tags);
$this->themeManager = $theme_manager;
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('block.repository'),
$container->get('entity_type.manager')->getViewBuilder('block'),
$container->get('entity_type.manager')->getDefinition('block')->getListCacheTags(),
$container->get('theme.manager'),
$container->get('request_stack')
);
}
/**
* {@inheritdoc}
*/
public function build() {
$build = parent::build();
$active_theme = $this->themeManager->getActiveTheme();
$theme_name = $active_theme->getName();
$visible_regions = $this->getVisibleRegionNames($theme_name);
// Build an array of the region names in the right order.
$build += array_fill_keys(array_keys($visible_regions), []);
foreach ($visible_regions as $region => $region_name) {
$destination = $this->requestStack->getCurrentRequest()->query->get('destination');
$query = [
'region' => $region,
];
if ($destination) {
$query['destination'] = $destination;
}
$title = $this->t('Place block<span class="visually-hidden"> in the %region region</span>', ['%region' => $region_name]);
$operations['block_description'] = [
'#type' => 'inline_template',
'#template' => '<div class="block-place-region">{{ link }}</div>',
'#context' => [
'link' => Link::createFromRoute($title, 'block.admin_library', ['theme' => $theme_name], [
'query' => $query,
'attributes' => [
'title' => $title,
'class' => ['use-ajax', 'button', 'button--small'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
]),
],
]),
],
];
$build[$region] = ['block_place_operations' => $operations] + $build[$region];
}
$build['#attached']['library'][] = 'block_place/drupal.block_place';
return $build;
}
/**
* Returns the human-readable list of regions keyed by machine name.
*
* @param string $theme
* The name of the theme.
*
* @return array
* An array of human-readable region names keyed by machine name.
*/
protected function getVisibleRegionNames($theme) {
return system_region_list($theme, REGIONS_VISIBLE);
}
}
<?php
namespace Drupal\Tests\block_place\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the placing a block.
*
* @group block_place
*/
class BlockPlaceTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = ['block', 'block_place', 'toolbar'];
/**
* Tests placing blocks as an admin.
*/
public function testPlacingBlocksAdmin() {
// Create administrative user.
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'access toolbar',
'administer blocks',
'view the administration theme',
]));
$this->drupalGet(Url::fromRoute('<front>'));
$this->clickLink('Place block');
// Each region should have one link to place a block.
$theme_name = $this->container->get('theme.manager')->getActiveTheme()->getName();
$visible_regions = system_region_list($theme_name, REGIONS_VISIBLE);
$this->assertGreaterThan(0, count($visible_regions));
$default_theme = $this->config('system.theme')->get('default');
$block_library_url = Url::fromRoute('block.admin_library', ['theme' => $default_theme]);
foreach ($visible_regions as $region => $name) {
$block_library_url->setOption('query', ['region' => $region]);
$links = $this->xpath('//a[contains(@href, :href)]', [':href' => $block_library_url->toString()]);
$this->assertEquals(1, count($links));
list(, $query_string) = explode('?', $links[0]->getAttribute('href'), 2);
parse_str($query_string, $query_parts);
$this->assertNotEmpty($query_parts['destination']);
// Get the text inside the div->a->span->em.
$demo_block = $this->xpath('//div[@class="block-place-region"]/a[text()="Place block"]//em[text()="' . $name . '"]');
$this->assertEquals(1, count($demo_block));
}
}
/**
* Tests placing blocks as an unprivileged user.
*/
public function testPlacingBlocksUnprivileged() {
// Create a user who cannot administer blocks.
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'access toolbar',
'view the administration theme',
]));
$this->drupalGet(Url::fromRoute('<front>'));
$links = $this->xpath('//a[text()=:label]', [':label' => 'Place block']);
$this->assertEmpty($links);
$this->drupalGet(Url::fromRoute('block.admin_library', ['theme' => 'classy']));
$this->assertSession()->statusCodeEquals(403);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment