Commit d6ba81b6 authored by Dries's avatar Dries

Issue #2058321 by tim.plunkett, pameeela: Move the 'place block' UI into the block listing.

parent cd15b235
......@@ -16,7 +16,9 @@
function block_admin_demo($theme = NULL) {
return array(
'#attached' => array(
'css' => array(drupal_get_path('module', 'block') . '/css/block.admin.css'),
'library' => array(
array('block', 'drupal.block.admin'),
),
),
);
}
......
......@@ -61,7 +61,7 @@ function block_help($path, $arg) {
$output .= '<dd>' . t('Blocks can be configured to be visible only on certain pages, only to users of certain roles, or only on pages displaying certain <a href="@content-type">content types</a>. Some dynamic blocks, such as those generated by modules, will be displayed only on certain pages.', array('@content-type' => url('admin/structure/types'), '@user' => url('user'))) . '</dd>';
if (module_exists('custom_block')) {
$output .= '<dt>' . t('Creating custom blocks') . '</dt>';
$output .= '<dd>' . t('Users with the <em>Administer blocks</em> permission can <a href="@block-add">add custom blocks</a>, which are then listed on the <a href="@blocks">Blocks administration page</a>. Once created, custom blocks behave just like default and module-generated blocks.', array('@blocks' => url('admin/structure/block'), '@block-add' => url('admin/structure/block/list/' . Drupal::config('system.theme')->get('default') . '/add/custom_blocks'))) . '</dd>';
$output .= '<dd>' . t('Users with the <em>Administer blocks</em> permission can add custom blocks, which are then listed on the <a href="@blocks">Blocks administration page</a>. Once created, custom blocks behave just like default and module-generated blocks.', array('@blocks' => url('admin/structure/block'))) . '</dd>';
}
$output .= '</dl>';
return $output;
......@@ -74,7 +74,7 @@ function block_help($path, $arg) {
$demo_theme = Drupal::config('system.theme')->get('default');
}
$themes = list_themes();
$output = '<p>' . t('This page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the <em>Save blocks</em> button at the bottom of the page. Click the <em>configure</em> link next to each block to configure its specific title and visibility settings.') . '</p>';
$output = '<p>' . t('This page provides a drag-and-drop interface for adding a block to a region, and for controlling the order of blocks within regions. To add a block to a region, or to configure its specific title and visibility settings, click the block title under <em>Place blocks</em>. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the <em>Save blocks</em> button at the bottom of the page.') . '</p>';
$output .= '<p>' . l(t('Demonstrate block regions (@theme)', array('@theme' => $themes[$demo_theme]->info['name'])), 'admin/structure/block/demo/' . $demo_theme) . '</p>';
return $output;
}
......@@ -147,11 +147,6 @@ function block_menu() {
'type' => $key == $default_theme ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
'route_name' => "block_admin_display.$key",
);
$items["admin/structure/block/list/$key/add"] = array(
'title' => 'Place blocks',
'type' => MENU_LOCAL_ACTION,
'route_name' => "block_plugin_ui.$key",
);
$items["admin/structure/block/demo/$key"] = array(
'title' => check_plain($theme->info['name']),
'page callback' => 'block_admin_demo',
......@@ -636,6 +631,20 @@ function block_library_info() {
array('system', 'drupal'),
),
);
$libraries['drupal.block.admin'] = array(
'title' => 'Block admin',
'version' => VERSION,
'js' => array(
drupal_get_path('module', 'block') . '/js/block.admin.js' => array(),
),
'css' => array(
drupal_get_path('module', 'block') . '/css/block.admin.css' => array(),
),
'dependencies' => array(
array('system', 'jquery'),
array('system', 'drupal'),
),
);
return $libraries;
}
......@@ -26,10 +26,3 @@ block_admin_add:
_content: '\Drupal\block\Controller\BlockAddController::blockAddConfigureForm'
requirements:
_permission: 'administer blocks'
block_autocomplete:
pattern: '/block/autocomplete'
defaults:
_controller: '\Drupal\block\Controller\BlockAutocompleteController::autocomplete'
requirements:
_permission: 'administer blocks'
......@@ -31,3 +31,87 @@ a.block-demo-backlink:visited {
a.block-demo-backlink:hover {
text-decoration: underline;
}
.block-list-region {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.block-list-right {
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-right .form-type-search {
padding: 0 1em;
}
/* Wide screens */
@media
screen and (min-width: 780px),
(orientation: landscape) and (min-device-height: 780px) {
.block-list-left {
float: left; /* LTR */
width: 75%;
padding-right: 2em;
}
[dir="rtl"] .block-list-left {
float: right;
padding-left: 2em;
padding-right: 0;
}
.block-list-right {
float: right; /* LTR */
width: 25%;
}
[dir="rtl"] .block-list-right {
float: left;
}
/* @todo File an issue to add a standard class to all text-like inputs */
.block-list-right .form-autocomplete,
.block-list-right .form-text,
.block-list-right .form-tel,
.block-list-right .form-email,
.block-list-right .form-url,
.block-list-right .form-search,
.block-list-right .form-number,
.block-list-right .form-color,
.block-list-right textarea {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
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),
(orientation: landscape) and (max-device-height: 1020px) {
.toolbar-vertical .block-list-left,
.toolbar-vertical .block-list-right {
float: none;
width: auto;
padding-right: 0;
}
}
......@@ -3,3 +3,9 @@ custom_block_type_add:
title: 'Add custom block type'
appears_on:
- custom_block_type_list
custom_block_add_action:
route_name: custom_block_add_page
title: 'Add custom block'
appears_on:
- block_admin_display
......@@ -31,19 +31,6 @@ function custom_block_help($path, $arg) {
* Implements hook_menu_local_tasks().
*/
function custom_block_menu_local_tasks(&$data, $router_item, $root_path) {
// Add the "Add custom block" action link to the theme-specific block library
// listing page.
// @todo This should just be $root_path == 'admin/structure/block/list/%/add'
// but block_menu() registers static router paths instead of dynamic ones.
if (preg_match('@^admin/structure/block/list/(.*)/add$@', $root_path)) {
$item = menu_get_item('block/add');
if ($item['access']) {
$data['actions']['block/add'] = array(
'#theme' => 'menu_local_action',
'#link' => $item,
);
}
}
if ($router_item['route_name'] == 'custom_block_list') {
// @todo Move to a LocalAction plugin when https://drupal.org/node/2045267
// allows local actions to work with query strings.
......@@ -59,6 +46,19 @@ function custom_block_menu_local_tasks(&$data, $router_item, $root_path) {
}
}
/**
* Implements hook_menu_local_actions_alter().
*/
function custom_block_menu_local_actions_alter(&$actions) {
if (isset($actions['local_action_static:custom_block_add_action'])) {
foreach (list_themes() as $theme => $theme_info) {
if ($theme_info->status) {
$actions['local_action_static:custom_block_add_action']['appears_on'][] = "block_admin_display.$theme";
}
}
}
}
/**
* Implements hook_menu().
*/
......@@ -246,27 +246,6 @@ function custom_block_add_body_field($block_type_id, $label = 'Block body') {
return $instance;
}
/**
* Implements hook_form_FORM_ID_alter() for block_plugin_ui().
*/
function custom_block_form_block_plugin_ui_alter(&$form, $form_state) {
foreach ($form['left']['plugin_library']['#rows'] as $plugin_id => &$row) {
// @todo Clean up when http://drupal.org/node/1874498 lands.
if (strpos($plugin_id, ':') === FALSE) {
continue;
}
list($base, $derivative) = explode(':', $plugin_id);
if ($base !== 'custom_block') {
continue;
}
$custom_block = entity_load_by_uuid('custom_block', $derivative);
$row['1']['data']['#links']['edit'] = array(
'title' => t('Edit'),
'href' => 'block/' . $custom_block->id(),
);
}
}
/**
* Implements hook_admin_paths().
*/
......
......@@ -7,15 +7,20 @@
namespace Drupal\block;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Utility\Json;
use Drupal\Core\Config\Entity\ConfigEntityListController;
use Drupal\block\Plugin\Core\Entity\Block;
use Drupal\Core\Entity\EntityControllerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the block list controller.
*/
class BlockListController extends ConfigEntityListController implements FormInterface {
class BlockListController extends ConfigEntityListController implements FormInterface, EntityControllerInterface {
/**
* The regions containing the blocks.
......@@ -31,6 +36,46 @@ class BlockListController extends ConfigEntityListController implements FormInte
*/
protected $theme;
/**
* The block manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $blockManager;
/**
* Constructs a new BlockListController object.
*
* @param string $entity_type
* The type of entity to be listed.
* @param array $entity_info
* An array of entity info for the entity type.
* @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage
* The entity storage controller class.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke hooks on.
* @param \Drupal\Component\Plugin\PluginManagerInterface $block_manager
* The block manager.
*/
public function __construct($entity_type, array $entity_info, EntityStorageControllerInterface $storage, ModuleHandlerInterface $module_handler, PluginManagerInterface $block_manager) {
parent::__construct($entity_type, $entity_info, $storage, $module_handler);
$this->blockManager = $block_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) {
return new static(
$entity_type,
$entity_info,
$container->get('plugin.manager.entity')->getStorageController($entity_type),
$container->get('module_handler'),
$container->get('plugin.manager.block')
);
}
/**
* Overrides \Drupal\Core\Config\Entity\ConfigEntityListController::load().
*/
......@@ -75,14 +120,20 @@ public function getFormID() {
*/
public function buildForm(array $form, array &$form_state) {
$entities = $this->load();
$form['#attached']['css'][] = drupal_get_path('module', 'block') . '/css/block.admin.css';
$form['#attached']['library'][] = array('system', 'drupal.tableheader');
$form['#attached']['library'][] = array('block', 'drupal.block');
$form['#attached']['library'][] = array('block', 'drupal.block.admin');
$form['#attributes']['class'][] = 'clearfix';
// Add a last region for disabled blocks.
$block_regions_with_disabled = $this->regions + array(BLOCK_REGION_NONE => BLOCK_REGION_NONE);
$form['left']['#type'] = 'container';
$form['left']['#attributes']['class'] = array(
'block-list-region',
'block-list-left',
);
$form['block_regions'] = array(
$form['left']['block_regions'] = array(
'#type' => 'value',
'#value' => $block_regions_with_disabled,
);
......@@ -93,11 +144,11 @@ public function buildForm(array $form, array &$form_state) {
$weight_delta = round(count($entities) / 2);
// Build the form tree.
$form['edited_theme'] = array(
$form['left']['edited_theme'] = array(
'#type' => 'value',
'#value' => $this->theme,
);
$form['blocks'] = array(
$form['left']['blocks'] = array(
'#type' => 'table',
'#header' => array(
t('Block'),
......@@ -123,7 +174,7 @@ public function buildForm(array $form, array &$form_state) {
// Loop over each region and build blocks.
foreach ($block_regions_with_disabled as $region => $title) {
$form['blocks']['#tabledrag'][] = array(
$form['left']['blocks']['#tabledrag'][] = array(
'match',
'sibling',
'block-region-select',
......@@ -131,27 +182,27 @@ public function buildForm(array $form, array &$form_state) {
NULL,
FALSE,
);
$form['blocks']['#tabledrag'][] = array(
$form['left']['blocks']['#tabledrag'][] = array(
'order',
'sibling',
'block-weight',
'block-weight-' . $region,
);
$form['blocks'][$region] = array(
$form['left']['blocks'][$region] = array(
'#attributes' => array(
'class' => array('region-title', 'region-title-' . $region, 'odd'),
'no_striping' => TRUE,
),
);
$form['blocks'][$region]['title'] = array(
$form['left']['blocks'][$region]['title'] = array(
'#markup' => $region != BLOCK_REGION_NONE ? $title : t('Disabled'),
'#wrapper_attributes' => array(
'colspan' => 5,
),
);
$form['blocks'][$region . '-message'] = array(
$form['left']['blocks'][$region . '-message'] = array(
'#attributes' => array(
'class' => array(
'region-message',
......@@ -160,7 +211,7 @@ public function buildForm(array $form, array &$form_state) {
),
),
);
$form['blocks'][$region . '-message']['message'] = array(
$form['left']['blocks'][$region . '-message']['message'] = array(
'#markup' => '<em>' . t('No blocks in this region') . '</em>',
'#wrapper_attributes' => array(
'colspan' => 5,
......@@ -171,19 +222,19 @@ public function buildForm(array $form, array &$form_state) {
foreach ($blocks[$region] as $info) {
$entity_id = $info['entity_id'];
$form['blocks'][$entity_id] = array(
$form['left']['blocks'][$entity_id] = array(
'#attributes' => array(
'class' => array('draggable'),
),
);
$form['blocks'][$entity_id]['info'] = array(
$form['left']['blocks'][$entity_id]['info'] = array(
'#markup' => check_plain($info['admin_label']),
'#wrapper_attributes' => array(
'class' => array('block'),
),
);
$form['blocks'][$entity_id]['region-theme']['region'] = array(
$form['left']['blocks'][$entity_id]['region-theme']['region'] = array(
'#type' => 'select',
'#default_value' => $region,
'#empty_value' => BLOCK_REGION_NONE,
......@@ -195,12 +246,12 @@ public function buildForm(array $form, array &$form_state) {
),
'#parents' => array('blocks', $entity_id, 'region'),
);
$form['blocks'][$entity_id]['region-theme']['theme'] = array(
$form['left']['blocks'][$entity_id]['region-theme']['theme'] = array(
'#type' => 'hidden',
'#value' => $this->theme,
'#parents' => array('blocks', $entity_id, 'theme'),
);
$form['blocks'][$entity_id]['weight'] = array(
$form['left']['blocks'][$entity_id]['weight'] = array(
'#type' => 'weight',
'#default_value' => $info['weight'],
'#delta' => $weight_delta,
......@@ -210,25 +261,94 @@ public function buildForm(array $form, array &$form_state) {
'class' => array('block-weight', 'block-weight-' . $region),
),
);
$form['blocks'][$entity_id]['operations'] = $this->buildOperations($info['entity']);
$form['left']['blocks'][$entity_id]['operations'] = $this->buildOperations($info['entity']);
}
}
}
// Do not allow disabling the main system content block when it is present.
if (isset($form['blocks']['system_main']['region'])) {
$form['blocks']['system_main']['region']['#required'] = TRUE;
if (isset($form['left']['blocks']['system_main']['region'])) {
$form['left']['blocks']['system_main']['region']['#required'] = TRUE;
}
$form['actions'] = array(
$form['left']['actions'] = array(
'#tree' => FALSE,
'#type' => 'actions',
);
$form['actions']['submit'] = array(
$form['left']['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save blocks'),
'#button_type' => 'primary',
);
$form['right']['#type'] = 'container';
$form['right']['#attributes']['class'] = array(
'block-list-region',
'block-list-right',
);
$form['right']['list']['#type'] = 'container';
$form['right']['list']['#attributes']['class'][] = 'entity-meta';
$form['right']['list']['title'] = array(
'#type' => 'container',
'#children' => '<h3>' . t('Place blocks') . '</h3>',
'#attributes' => array(
'class' => array(
'entity-meta-header',
),
),
);
$form['right']['list']['search'] = array(
'#type' => 'search',
'#title' => t('Filter'),
'#title_display' => 'invisible',
'#size' => 30,
'#placeholder' => t('Filter by block name'),
'#attributes' => array(
'class' => array('block-filter-text'),
'data-element' => '.entity-meta',
'title' => t('Enter a part of the block name to filter by.'),
),
);
// Sort the plugins first by category, then by label.
$plugins = $this->blockManager->getDefinitions();
uasort($plugins, function ($a, $b) {
if ($a['category'] != $b['category']) {
return strnatcasecmp($a['category'], $b['category']);
}
return strnatcasecmp($a['admin_label'], $b['admin_label']);
});
foreach ($plugins as $plugin_id => $plugin_definition) {
$category = $plugin_definition['category'];
if (!isset($form['right']['list'][$category])) {
$form['right']['list'][$category] = array(
'#type' => 'details',
'#title' => $category,
'content' => array(
'#theme' => 'links',
'#links' => array(),
'#attributes' => array(
'class' => array(
'block-list',
),
),
),
);
}
$form['right']['list'][$category]['content']['#links'][$plugin_id] = array(
'title' => $plugin_definition['admin_label'],
'href' => 'admin/structure/block/add/' . $plugin_id . '/' . $this->theme,
'attributes' => array(
'class' => array('use-ajax', 'block-filter-text-source'),
'data-accepts' => 'application/vnd.drupal-modal',
'data-dialog-options' => Json::encode(array(
'width' => 700,
)),
),
);
}
return $form;
}
......
<?php
/**
* @file
* Contains \Drupal\block\Controller\BlockAutocompleteController.
*/
namespace Drupal\block\Controller;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Utility\Tags;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Controller\ControllerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an autocomplete for blocks.
*/
class BlockAutocompleteController implements ControllerInterface {
/**
* The block plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $manager;
/**
* Constructs a new BlockAutocompleteController object.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
* The block plugin manager.
*/
public function __construct(PluginManagerInterface $manager) {
$this->manager = $manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.block')
);
}
/**
* Autocompletes a block plugin ID.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The matched plugins as JSON.
*/
public function autocomplete(Request $request) {
$string_typed = $request->query->get('q');
// The passed string may be comma or space separated.
$string_typed = Tags::explode($string_typed);
// Take the last result and lowercase it.
$string = Unicode::strtolower(array_pop($string_typed));
$matches = array();
if ($string) {
$titles = array();
// Gather all block plugins and their admin titles.
foreach($this->manager->getDefinitions() as $plugin_id => $plugin) {
$titles[$plugin_id] = $plugin['admin_label'];
}
// Find any matching block plugin IDs.
$matches = preg_grep("/\b". $string . "/i", $titles);
}
return new JsonResponse($matches);
}
}