diff --git a/core/modules/block_place/block_place.install b/core/modules/block_place/block_place.install new file mode 100644 index 0000000000..c51492ff67 --- /dev/null +++ b/core/modules/block_place/block_place.install @@ -0,0 +1,25 @@ +deleteAll(); +} + diff --git a/core/modules/block_place/block_place.libraries.yml b/core/modules/block_place/block_place.libraries.yml index 0671252032..87af7de5a3 100644 --- a/core/modules/block_place/block_place.libraries.yml +++ b/core/modules/block_place/block_place.libraries.yml @@ -9,3 +9,12 @@ drupal.block_place.icons: css: theme: css/block-place.icons.theme.css: {} + +drupal.block_place.js: + version: VERSION + js: + js/block_place.js: {} + dependencies: + - core/jquery + - core/drupal + diff --git a/core/modules/block_place/block_place.links.contextual.yml b/core/modules/block_place/block_place.links.contextual.yml new file mode 100644 index 0000000000..9c482cf154 --- /dev/null +++ b/core/modules/block_place/block_place.links.contextual.yml @@ -0,0 +1,4 @@ +block_place.sort: + title: 'Sort blocks' + route_name: 'block_place.sort' + group: 'block' diff --git a/core/modules/block_place/block_place.module b/core/modules/block_place/block_place.module index 7fe86a6c7f..c9dfccc6a9 100644 --- a/core/modules/block_place/block_place.module +++ b/core/modules/block_place/block_place.module @@ -7,6 +7,7 @@ use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; +use Drupal\block\Entity\Block; /** * Implements hook_help(). @@ -77,8 +78,78 @@ function block_place_toolbar() { '#attached' => [ 'library' => [ 'block_place/drupal.block_place.icons', + // This JS library only needs to be attached here so that the new + // contextual link works with Ajax. Remove + // in https://www.drupal.org/node/2764931. + 'block_place/drupal.block_place.js', ], - ], + ], ]; return $items; } + +/** + * Implements hook_page_top(). + * + * Add block place library if dialog should be open to sort blocks. + */ +function block_place_page_top(array &$page_top) { + if ($region_sort = \Drupal::request()->query->get('region_sort')) { + $page_top['#attached']['library'][] = 'block_place/drupal.block_place.js'; + $destination = \Drupal::destination()->get(); + $destination = explode('?', $destination)[0]; + $theme = \Drupal::theme()->getActiveTheme(); + $url = Url::fromRoute('block_place.sort', ['region' => $region_sort, 'theme' => $theme->getName()], ['query' => ['destination' => $destination]]); + $page_top['#attached']['drupalSettings']['block_place'] = [ + 'dialog_url' => $url->setAbsolute()->toString(), + // @todo Always use 'off_canvas' in https://www.drupal.org/node/2784443. + 'dialog_type' => \Drupal::moduleHandler()->moduleExists('outside_in') ? 'dialog_off_canvas' : 'modal', + ]; + } +} + +/** + * Implements hook_contextual_links_view_alter(). + */ +function block_place_contextual_links_view_alter(&$element, $items) { + if (isset($element['#links']['block-placesort']) && isset($items['block_configure']['route_parameters']['block'])) { + + // Add arguments to sort blocks url. + /** @var \Drupal\block\Entity\Block $block */ + $block = Block::load($items['block_configure']['route_parameters']['block']); + /** @var \Drupal\Core\Url $url */ + $url = &$element['#links']['block-placesort']['url']; + $url->setRouteParameters( + [ + 'theme' => $block->getTheme(), + 'region' => $block->getRegion(), + ] + ); + + // @todo Always use 'off_canvas' in https://www.drupal.org/node/2784443. + $attributes = [ + 'class' => ['use-ajax'], + ]; + if (\Drupal::moduleHandler()->moduleExists('outside_in')) { + $attributes['data-dialog-type'] = 'dialog'; + $attributes['data-dialog-renderer'] = 'off_canvas'; + } + else { + $attributes['data-dialog-type'] = 'modal'; + } + $element['#links']['block-placesort']['attributes'] = $attributes; + } +} + + +/** + * Implements hook_block_view_alter(). + */ +function block_place_block_view_alter(array &$build) { + // Force a new 'data-contextual-id' attribute on blocks when this module is + // enabled so as not to reuse stale data cached client-side. + // @todo Remove when https://www.drupal.org/node/2773591 is fixed. + $build['#contextual_links']['block_place'] = [ + 'route_parameters' => [], + ]; +} diff --git a/core/modules/block_place/block_place.routing.yml b/core/modules/block_place/block_place.routing.yml new file mode 100644 index 0000000000..402f25980e --- /dev/null +++ b/core/modules/block_place/block_place.routing.yml @@ -0,0 +1,15 @@ +block_place.admin_library: + path: 'admin/structure/block-place/library/{theme}' + defaults: + _controller: '\Drupal\block_place\Controller\PlaceBlockLibraryController::listBlocks' + _title: 'Place block' + requirements: + _access_theme: 'TRUE' + _permission: 'administer blocks' +block_place.sort: + path: 'admin/structure/block-place-sort/{theme}/{region}' + defaults: + _form: '\Drupal\block_place\Form\BlockRegionSorterForm' + _title: 'Order Blocks' + requirements: + _permission: 'administer blocks' diff --git a/core/modules/block_place/js/block_place.es6.js b/core/modules/block_place/js/block_place.es6.js new file mode 100644 index 0000000000..92f259549d --- /dev/null +++ b/core/modules/block_place/js/block_place.es6.js @@ -0,0 +1,31 @@ +/** + * @file + * Block Place behaviors. + */ + +(function ($, window, Drupal, drupalSettings) { + Drupal.behaviors.blockPlace = { + attach: function (context, settings) { + // If drupalSettings.block_place is set open open dialog. + if (drupalSettings.hasOwnProperty('block_place') && drupalSettings.block_place.hasOwnProperty('dialog_url')) { + $(window).once('block_sort').each(function () { + const blockSort = Drupal.ajax({ + dialog: {}, + dialogType: drupalSettings.block_place.dialog_type, + selector: '.ckeditor-dialog-loading-link', + url: drupalSettings.block_place.dialog_url, + progress: {type: 'throbber' }, + }); + blockSort.execute(); + }); + } + }, + }; + + // Make sure contextual links work with Ajax. + // Remove in https://www.drupal.org/node/2764931. + $(document).once('contextual-ajax').on('drupalContextualLinkAdded', function (event, data) { + Drupal.attachBehaviors(data.$el[0]); + }); + +})(jQuery, window, Drupal, drupalSettings); diff --git a/core/modules/block_place/js/block_place.js b/core/modules/block_place/js/block_place.js new file mode 100644 index 0000000000..5162837201 --- /dev/null +++ b/core/modules/block_place/js/block_place.js @@ -0,0 +1,29 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, window, Drupal, drupalSettings) { + Drupal.behaviors.blockPlace = { + attach: function attach(context, settings) { + if (drupalSettings.hasOwnProperty('block_place') && drupalSettings.block_place.hasOwnProperty('dialog_url')) { + $(window).once('block_sort').each(function () { + var blockSort = Drupal.ajax({ + dialog: {}, + dialogType: drupalSettings.block_place.dialog_type, + selector: '.ckeditor-dialog-loading-link', + url: drupalSettings.block_place.dialog_url, + progress: { type: 'throbber' } + }); + blockSort.execute(); + }); + } + } + }; + + $(document).once('contextual-ajax').on('drupalContextualLinkAdded', function (event, data) { + Drupal.attachBehaviors(data.$el[0]); + }); +})(jQuery, window, Drupal, drupalSettings); \ No newline at end of file diff --git a/core/modules/block_place/src/Controller/PlaceBlockLibraryController.php b/core/modules/block_place/src/Controller/PlaceBlockLibraryController.php new file mode 100644 index 0000000000..b44f365b5a --- /dev/null +++ b/core/modules/block_place/src/Controller/PlaceBlockLibraryController.php @@ -0,0 +1,46 @@ +moduleHandler()->moduleExists('outside_in')) { + $data_dialog_attributes = [ + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', + 'data-dialog-options' => Json::encode([ + 'width' => 425, + ]), + ]; + } + else { + $data_dialog_attributes = []; + } + foreach ($build['blocks']['#rows'] as &$row) { + if (isset($row['operations']['data']['#links']['add'])) { + $row['operations']['data']['#links']['add']['attributes'] = $data_dialog_attributes + $row['operations']['data']['#links']['add']['attributes']; + $row['operations']['data']['#links']['add']['query']['destination'] = \Drupal::destination()->get(); + } + } + } + return $build; + } + +} diff --git a/core/modules/block_place/src/Form/BlockRegionSorterForm.php b/core/modules/block_place/src/Form/BlockRegionSorterForm.php new file mode 100644 index 0000000000..b47dd9aaac --- /dev/null +++ b/core/modules/block_place/src/Form/BlockRegionSorterForm.php @@ -0,0 +1,114 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $theme = NULL, $region = NULL) { + $blocks = $this->entityTypeManager->getStorage('block')->loadByProperties(['region' => $region, 'theme' => $theme]); + // Make sure the blocks are in the correct order. + usort($blocks, function (Block $blocka, Block $blockb) { + return $blocka->getWeight() - $blockb->getWeight(); + }); + $form['#tree'] = TRUE; + $form['blocks'] = [ + '#type' => 'table', + '#header' => [$this->t('Block'), $this->t('Weight')], + '#empty' => $this->t('There are no blocks in this regions'), + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'block-table-order-weight', + ], + ], + '#attributes' => [ + 'id' => 'block-place-sort-table', + ], + ]; + + /** @var \Drupal\block\Entity\Block $block */ + foreach ($blocks as $block) { + $form['blocks'][$block->id()]['#weight'] = $block->getWeight(); + $form['blocks'][$block->id()]['#attributes']['class'][] = 'draggable'; + $form['blocks'][$block->id()]['label'] = [ + '#plain_text' => $block->label(), + ]; + + $form['blocks'][$block->id()]['weight'] = [ + '#type' => 'weight', + '#title' => $this->t('Weight for @title', ['@title' => $block->label()]), + '#title_display' => 'invisible', + '#default_value' => $block->getWeight(), + // Classify the weight element for #tabledrag. + '#attributes' => ['class' => ['block-table-order-weight']], + ]; + } + + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save changes'), + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'block_place_sort'; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + foreach ($form_state->getValue('blocks') as $block_id => $block_weight) { + /** @var \Drupal\block\Entity\Block $block */ + $block = $this->entityTypeManager->getStorage('block')->load($block_id); + $block->setWeight($block_weight['weight']); + $block->save(); + } + drupal_set_message('The block order has been saved'); + } + +} diff --git a/core/modules/block_place/src/Plugin/DisplayVariant/PlaceBlockPageVariant.php b/core/modules/block_place/src/Plugin/DisplayVariant/PlaceBlockPageVariant.php index d05c7f172f..74fe544310 100644 --- a/core/modules/block_place/src/Plugin/DisplayVariant/PlaceBlockPageVariant.php +++ b/core/modules/block_place/src/Plugin/DisplayVariant/PlaceBlockPageVariant.php @@ -6,9 +6,10 @@ use Drupal\block\Plugin\DisplayVariant\BlockPageVariant; use Drupal\Component\Serialization\Json; use Drupal\Core\Entity\EntityViewBuilderInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Link; use Drupal\Core\Routing\RedirectDestinationInterface; use Drupal\Core\Theme\ThemeManagerInterface; -use Drupal\Core\Link; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -36,6 +37,13 @@ class PlaceBlockPageVariant extends BlockPageVariant { protected $redirectDestination; /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** * Constructs a new PlaceBlockPageVariant. * * @param array $configuration @@ -54,12 +62,15 @@ class PlaceBlockPageVariant extends BlockPageVariant { * The theme manager. * @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination * The redirect destination. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, BlockRepositoryInterface $block_repository, EntityViewBuilderInterface $block_view_builder, array $block_list_cache_tags, ThemeManagerInterface $theme_manager, RedirectDestinationInterface $redirect_destination) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, BlockRepositoryInterface $block_repository, EntityViewBuilderInterface $block_view_builder, array $block_list_cache_tags, ThemeManagerInterface $theme_manager, RedirectDestinationInterface $redirect_destination, ModuleHandlerInterface $module_handler) { parent::__construct($configuration, $plugin_id, $plugin_definition, $block_repository, $block_view_builder, $block_list_cache_tags); $this->themeManager = $theme_manager; $this->redirectDestination = $redirect_destination; + $this->moduleHandler = $module_handler; } /** @@ -74,7 +85,8 @@ public static function create(ContainerInterface $container, array $configuratio $container->get('entity_type.manager')->getViewBuilder('block'), $container->get('entity_type.manager')->getDefinition('block')->getListCacheTags(), $container->get('theme.manager'), - $container->get('redirect.destination') + $container->get('redirect.destination'), + $container->get('module_handler') ); } @@ -97,23 +109,43 @@ public function build() { 'region' => $region, ]; if ($destination) { - $query['destination'] = $destination; + $query['destination'] = $destination . "?region_sort=$region"; } $title = $this->t('Place block in the %region region', ['%region' => $region_name]); + // @todo Remove module exists check when off_canvas library moved into + // core.services.yml. https://www.drupal.org/node/2784443 + if ($this->moduleHandler->moduleExists('outside_in')) { + $place_block_route = 'block_place.admin_library'; + $data_dialog_attributes = [ + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', + 'data-dialog-options' => Json::encode([ + 'width' => 350, + ]), + ]; + } + else { + $place_block_route = 'block.admin_library'; + $data_dialog_attributes = [ + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ]; + } + + $place_block_route = 'block_place.admin_library'; + $operations['block_description'] = [ '#type' => 'inline_template', '#template' => '
{{ link }}
', '#context' => [ - 'link' => Link::createFromRoute($title, 'block.admin_library', ['theme' => $theme_name], [ + 'link' => Link::createFromRoute($title, $place_block_route, ['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, - ]), - ], + ] + $data_dialog_attributes, ]), ], ]; diff --git a/core/modules/block_place/tests/src/Functional/BlockPlaceTest.php b/core/modules/block_place/tests/src/Functional/BlockPlaceTest.php index 8e89048acd..d7e965a8d8 100644 --- a/core/modules/block_place/tests/src/Functional/BlockPlaceTest.php +++ b/core/modules/block_place/tests/src/Functional/BlockPlaceTest.php @@ -39,7 +39,7 @@ public function testPlacingBlocksAdmin() { $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]); + $block_library_url = Url::fromRoute('block_place.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()]);