diff --git a/core/modules/block_place/block_place.install b/core/modules/block_place/block_place.install new file mode 100644 index 0000000..ca9cdb6 --- /dev/null +++ b/core/modules/block_place/block_place.install @@ -0,0 +1,24 @@ +deleteAll(); +} diff --git a/core/modules/block_place/block_place.libraries.yml b/core/modules/block_place/block_place.libraries.yml index 0671252..87af7de 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 0000000..9c482cf --- /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 7fe86a6..4757a91 100644 --- a/core/modules/block_place/block_place.module +++ b/core/modules/block_place/block_place.module @@ -5,8 +5,10 @@ * Controls the placement of blocks from all pages. */ +use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; +use Drupal\block\Entity\Block; /** * Implements hook_help(). @@ -77,8 +79,83 @@ function block_place_toolbar() { '#attached' => [ 'library' => [ 'block_place/drupal.block_place.icons', + // @todo 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_attachments(). + * + * Add block place library if dialog should be open to sort blocks. + */ +function block_place_page_attachments(array &$attachments) { + $query = \Drupal::request()->query; + if ($region_sort = $query->get('block-place-region-sort')) { + $attachments['#attached']['library'][] = 'block_place/drupal.block_place.js'; + $attachments['#cache']['contexts'] = 'url.query_args:block-place-region-sort'; + + // Remove block-place-region-sort from the query string. + $destination_parts = UrlHelper::parse(\Drupal::destination()->get()); + unset($destination_parts['query']['block-place-region-sort']); + $destination = $destination_parts['path'] . ($destination_parts['query'] ? ('?' . UrlHelper::buildQuery($destination_parts['query'])) : ''); + + $theme = \Drupal::theme()->getActiveTheme(); + $url = Url::fromRoute('block_place.sort', ['region' => $region_sort, 'theme' => $theme->getName()], ['query' => \Drupal::destination()->getAsArray()]); + + $attachments['#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', + ]; + } + // Save a setting to flag pages where block_place mode is active. + $attachments['#attached']['drupalSettings']['block_place']['isInBlockPlaceMode'] = ($region_sort || $query->get('block-place')); +} + +/** + * 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\BlockInterface $block */ + $block = Block::load($items['block_configure']['route_parameters']['block']); + $element['#links']['block-placesort']['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 0000000..5b94600 --- /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 0000000..eca6005 --- /dev/null +++ b/core/modules/block_place/js/block_place.es6.js @@ -0,0 +1,35 @@ +/** + * @file + * Block Place behaviors. + */ + +(function ($, window, Drupal, drupalSettings) { + Drupal.blockPlace = { + isBlockPlaceMode: () => { + const queryString = decodeURI(window.location.search); + return (/block-place=1/i.test(queryString) || /block-place-region-sort=/i.test(queryString)); + }, + }; + + Drupal.behaviors.blockPlace = { + attach(context, settings) { + // If drupalSettings.block_place is set open dialog. + if (drupalSettings.hasOwnProperty('block_place') && drupalSettings.block_place.hasOwnProperty('dialog_url')) { + $(window).once('block_sort').each(() => { + Drupal.ajax({ + dialog: {}, + dialogType: drupalSettings.block_place.dialog_type, + url: drupalSettings.block_place.dialog_url, + progress: { type: 'throbber' }, + }).execute(); + }); + } + }, + }; + + // Make sure contextual links work with Ajax. + // @todo Remove in https://www.drupal.org/node/2764931. + $(document).once('contextual-ajax').on('drupalContextualLinkAdded', (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 0000000..633ec85 --- /dev/null +++ b/core/modules/block_place/js/block_place.js @@ -0,0 +1,35 @@ +/** +* 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.blockPlace = { + isBlockPlaceMode: function isBlockPlaceMode() { + var queryString = decodeURI(window.location.search); + return (/block-place=1/i.test(queryString) || /block-place-region-sort=/i.test(queryString) + ); + } + }; + + 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 () { + Drupal.ajax({ + dialog: {}, + dialogType: drupalSettings.block_place.dialog_type, + url: drupalSettings.block_place.dialog_url, + progress: { type: 'throbber' } + }).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 0000000..67e684e --- /dev/null +++ b/core/modules/block_place/src/Controller/PlaceBlockLibraryController.php @@ -0,0 +1,44 @@ +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 0000000..b42f649 --- /dev/null +++ b/core/modules/block_place/src/Form/BlockRegionSorterForm.php @@ -0,0 +1,111 @@ +blockStorage = $block_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager')->getStorage('block') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $theme = NULL, $region = NULL) { + $blocks = $this->blockStorage->loadByProperties(['region' => $region, 'theme' => $theme]); + // Make sure the blocks are in the correct order. + uasort($assignment, 'Drupal\block\Entity\Block::sort'); + $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\BlockInterface[] $blocks */ + 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) { + /** @var \Drupal\block\BlockInterface[] $blocks */ + $blocks = $this->blockStorage->loadMultiple(array_keys($form_state->getValue('blocks'))); + foreach ($blocks as $block_id => $block) { + $weight = $form_state->getValue(['blocks', $block_id, 'weight']); + $block->setWeight($weight)->save(); + } + drupal_set_message('The block region sorter 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 d05c7f1..72a0c2e 100644 --- a/core/modules/block_place/src/Plugin/DisplayVariant/PlaceBlockPageVariant.php +++ b/core/modules/block_place/src/Plugin/DisplayVariant/PlaceBlockPageVariant.php @@ -5,10 +5,13 @@ use Drupal\block\BlockRepositoryInterface; use Drupal\block\Plugin\DisplayVariant\BlockPageVariant; use Drupal\Component\Serialization\Json; +use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Entity\EntityStorageInterface; 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 +39,20 @@ class PlaceBlockPageVariant extends BlockPageVariant { protected $redirectDestination; /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The block storage handler. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $blockStorage; + + /** * Constructs a new PlaceBlockPageVariant. * * @param array $configuration @@ -54,12 +71,18 @@ 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. + * @param \Drupal\Core\Entity\EntityStorageInterface $block_storage + * The block storage 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, EntityStorageInterface $block_storage) { 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; + $this->blockStorage = $block_storage; } /** @@ -74,7 +97,9 @@ 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'), + $container->get('entity_type.manager')->getStorage('block') ); } @@ -96,24 +121,49 @@ public function build() { $query = [ 'region' => $region, ]; + + if ($destination) { - $query['destination'] = $destination; + if ($this->blockStorage->loadByProperties(['theme' => $theme_name, 'region' => $region])) { + $destination_parts = UrlHelper::parse($destination); + $destination_parts['query']['block-place-region-sort'] = $region; + $query['destination'] = $destination_parts['path'] . '?' . UrlHelper::buildQuery($destination_parts['query']); + } + else { + $query['destination'] = $destination; + } } $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')) { + $data_dialog_attributes = [ + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', + 'data-dialog-options' => Json::encode([ + 'width' => 350, + ]), + ]; + } + else { + $data_dialog_attributes = [ + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ]; + } + $operations['block_description'] = [ '#type' => 'inline_template', '#template' => '
{{ link }}
', '#context' => [ - 'link' => Link::createFromRoute($title, 'block.admin_library', ['theme' => $theme_name], [ + 'link' => Link::createFromRoute($title, 'block_place.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, - ]), - ], + ] + $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 8e89048..d7e965a 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()]); diff --git a/core/modules/block_place/tests/src/FunctionalJavascript/BlockPlaceTest.php b/core/modules/block_place/tests/src/FunctionalJavascript/BlockPlaceTest.php new file mode 100644 index 0000000..32c0f6e --- /dev/null +++ b/core/modules/block_place/tests/src/FunctionalJavascript/BlockPlaceTest.php @@ -0,0 +1,65 @@ +drupalLogin($this->drupalCreateUser([ + 'access administration pages', + 'access toolbar', + 'administer blocks', + 'view the administration theme', + ])); + } + + + public function testPlaceBlock() { + $this->drupalGet(Url::fromRoute('')); + $this->clickLink('Place block'); + // @todo Finish test. + return; + + // 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_place.admin_library', ['theme' => $default_theme]); + foreach ($visible_regions as $region => $name) { + //print $region . ' ' . $name; + } + $page = $this->getSession()->getPage(); + $page->clickLink('Place block'); + //$page->find('css', 'a[title="Place block in the Left sidebar region"]')->click(); + + //$page->clickLink('Place block in the Left sidebar region'); + $assert_session = $this->assertSession(); + $this->getSession()->wait(1000); + /** @var Element $link */ + foreach ($page->findAll('css', 'a') as $link) { + print $link->getText(); + } + //$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ui-dialog')); + + } + +} diff --git a/core/modules/outside_in/js/outside_in.es6.js b/core/modules/outside_in/js/outside_in.es6.js index 7271024..3201b59 100644 --- a/core/modules/outside_in/js/outside_in.es6.js +++ b/core/modules/outside_in/js/outside_in.es6.js @@ -5,7 +5,7 @@ * @private */ -(function ($, Drupal) { +(function ($, Drupal, drupalSettings) { const blockConfigureSelector = '[data-outside-in-edit]'; const toggleEditSelector = '[data-drupal-outsidein="toggle"]'; const itemsToToggleSelector = '[data-off-canvas-main-canvas], #toolbar-bar, [data-drupal-outsidein="editable"] a, [data-drupal-outsidein="editable"] button'; @@ -164,7 +164,10 @@ $('body').once('outside_in.edit_mode_init').each(() => { const editMode = localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false'; if (editMode) { - setEditModeState(true); + // Do not turn on edit mode when in block place mode. + if (!drupalSettings.hasOwnProperty('block_place') || !drupalSettings.block_place.isInBlockPlaceMode) { + setEditModeState(true); + } } }); @@ -247,4 +250,4 @@ } }, }); -}(jQuery, Drupal)); +}(jQuery, Drupal, drupalSettings)); diff --git a/core/modules/outside_in/js/outside_in.js b/core/modules/outside_in/js/outside_in.js index 30fe581..022f5d7 100644 --- a/core/modules/outside_in/js/outside_in.js +++ b/core/modules/outside_in/js/outside_in.js @@ -5,7 +5,7 @@ * @preserve **/ -(function ($, Drupal) { +(function ($, Drupal, drupalSettings) { var blockConfigureSelector = '[data-outside-in-edit]'; var toggleEditSelector = '[data-drupal-outsidein="toggle"]'; var itemsToToggleSelector = '[data-off-canvas-main-canvas], #toolbar-bar, [data-drupal-outsidein="editable"] a, [data-drupal-outsidein="editable"] button'; @@ -97,7 +97,9 @@ $('body').once('outside_in.edit_mode_init').each(function () { var editMode = localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false'; if (editMode) { - setEditModeState(true); + if (!drupalSettings.hasOwnProperty('block_place') || !drupalSettings.block_place.isInBlockPlaceMode) { + setEditModeState(true); + } } }); @@ -151,4 +153,4 @@ } } }); -})(jQuery, Drupal); \ No newline at end of file +})(jQuery, Drupal, drupalSettings); \ No newline at end of file