core/modules/outside_in/css/outside_in.motion.css | 4 +- core/modules/outside_in/css/outside_in.theme.css | 6 +- core/modules/outside_in/js/outside_in.es6.js | 6 +- core/modules/outside_in/js/outside_in.js | 6 +- core/modules/outside_in/outside_in.module | 51 ++++++++++++---- core/modules/outside_in/outside_in.routing.yml | 1 + core/modules/outside_in/outside_in.services.yml | 5 ++ .../BlockPluginHasOffCanvasFormAccessCheck.php | 29 +++++++++ .../BlockPluginHasOffCanvasFormAccessCheckTest.php | 69 ++++++++++++++++++++++ 9 files changed, 156 insertions(+), 21 deletions(-) diff --git a/core/modules/outside_in/css/outside_in.motion.css b/core/modules/outside_in/css/outside_in.motion.css index 5fae723..1a92c91 100644 --- a/core/modules/outside_in/css/outside_in.motion.css +++ b/core/modules/outside_in/css/outside_in.motion.css @@ -19,8 +19,8 @@ /* Transition the editables on the page, their contextual links and their hover states. */ .dialog-off-canvas__main-canvas .contextual, -.dialog-off-canvas__main-canvas .js-outside-in-edit-mode .outside-in-editable, -.dialog-off-canvas__main-canvas.js-tray-open .js-outside-in-edit-mode .outside-in-editable { +.dialog-off-canvas__main-canvas .js-outside-in-edit-mode .contextual-region.outside-in-editable, +.dialog-off-canvas__main-canvas.js-tray-open .js-outside-in-edit-mode .contextual-region.outside-in-editable { -webkit-transition: all .7s ease; -moz-transition: all .7s ease; transition: all .7s ease; diff --git a/core/modules/outside_in/css/outside_in.theme.css b/core/modules/outside_in/css/outside_in.theme.css index 0db7a3c..01c974b 100644 --- a/core/modules/outside_in/css/outside_in.theme.css +++ b/core/modules/outside_in/css/outside_in.theme.css @@ -60,12 +60,12 @@ } /* Style the editables while in edit mode. */ -.dialog-off-canvas__main-canvas.js-outside-in-edit-mode .outside-in-editable { +.dialog-off-canvas__main-canvas.js-outside-in-edit-mode .contextual-region.outside-in-editable { outline: 1px dashed rgba(0,0,0,0.5); box-shadow: 0 0 0 1px rgba(255,255,255,0.7); } -.dialog-off-canvas__main-canvas.js-outside-in-edit-mode .outside-in-editable:hover, -.dialog-off-canvas__main-canvas.js-outside-in-edit-mode .outside-in-editable.outside-in-active-editable { +.dialog-off-canvas__main-canvas.js-outside-in-edit-mode .contextual-region.outside-in-editable:hover, +.dialog-off-canvas__main-canvas.js-outside-in-edit-mode .contextual-region.outside-in-editable.outside-in-active-editable { background-color: rgba(0,0,0,0.2); } diff --git a/core/modules/outside_in/js/outside_in.es6.js b/core/modules/outside_in/js/outside_in.es6.js index 76bb872..5112eac 100644 --- a/core/modules/outside_in/js/outside_in.es6.js +++ b/core/modules/outside_in/js/outside_in.es6.js @@ -6,7 +6,7 @@ (function ($, Drupal) { 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'; + const itemsToToggleSelector = '[data-off-canvas-main-canvas], #toolbar-bar, .contextual-region[data-drupal-outsidein="editable"] a, .contextual-region[data-drupal-outsidein="editable"] button'; const contextualItemsSelector = '[data-contextual-id] a, [data-contextual-id] button'; const quickEditItemSelector = '[data-quickedit-entity-id]'; @@ -132,7 +132,7 @@ $editButton.text(Drupal.t('Editing')); closeToolbarTrays(); - $editables = $('[data-drupal-outsidein="editable"]').once('outsidein'); + $editables = $('.contextual-region[data-drupal-outsidein="editable"]').once('outsidein'); if ($editables.length) { // Use event capture to prevent clicks on links. document.querySelector('[data-off-canvas-main-canvas]').addEventListener('click', preventClick, true); @@ -166,7 +166,7 @@ } // Disable edit mode. else { - $editables = $('[data-drupal-outsidein="editable"]').removeOnce('outsidein'); + $editables = $('.contextual-region[data-drupal-outsidein="editable"]').removeOnce('outsidein'); if ($editables.length) { document.querySelector('[data-off-canvas-main-canvas]').removeEventListener('click', preventClick, true); $editables.off('.outsidein'); diff --git a/core/modules/outside_in/js/outside_in.js b/core/modules/outside_in/js/outside_in.js index 95cf90e..350ded0 100644 --- a/core/modules/outside_in/js/outside_in.js +++ b/core/modules/outside_in/js/outside_in.js @@ -8,7 +8,7 @@ (function ($, Drupal) { 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'; + var itemsToToggleSelector = '[data-off-canvas-main-canvas], #toolbar-bar, .contextual-region[data-drupal-outsidein="editable"] a, .contextual-region[data-drupal-outsidein="editable"] button'; var contextualItemsSelector = '[data-contextual-id] a, [data-contextual-id] button'; var quickEditItemSelector = '[data-quickedit-entity-id]'; @@ -74,7 +74,7 @@ $editButton.text(Drupal.t('Editing')); closeToolbarTrays(); - $editables = $('[data-drupal-outsidein="editable"]').once('outsidein'); + $editables = $('.contextual-region[data-drupal-outsidein="editable"]').once('outsidein'); if ($editables.length) { document.querySelector('[data-off-canvas-main-canvas]').addEventListener('click', preventClick, true); @@ -97,7 +97,7 @@ }); } } else { - $editables = $('[data-drupal-outsidein="editable"]').removeOnce('outsidein'); + $editables = $('.contextual-region[data-drupal-outsidein="editable"]').removeOnce('outsidein'); if ($editables.length) { document.querySelector('[data-off-canvas-main-canvas]').removeEventListener('click', preventClick, true); $editables.off('.outsidein'); diff --git a/core/modules/outside_in/outside_in.module b/core/modules/outside_in/outside_in.module index 69517b9..665468a 100644 --- a/core/modules/outside_in/outside_in.module +++ b/core/modules/outside_in/outside_in.module @@ -100,8 +100,16 @@ function outside_in_entity_type_build(array &$entity_types) { * Implements hook_preprocess_HOOK() for block templates. */ function outside_in_preprocess_block(&$variables) { - // The main system block does not contain the block contextual links. - if ($variables['plugin_id'] !== 'system_main_block') { + // Only blocks that have an off_canvas form will have a "Quick Edit" link. We + // could wait for the contextual links to be initialized on the client side, + // and then add the class and data- attribute below there (via JavaScript). + // But that means that it will be impossible to show Settings Tray's clickable + // regions immediately when the page loads. When latency is high, this will + // cause flicker. Therefore, for now, we choose to duplicate some logic to + // guarantee a smooth experience. + // This is an implementation detail that may change in the future. + // @see \Drupal\outside_in\Access\BlockPluginHasOffCanvasFormAccessCheck + if (\Drupal::service('plugin.manager.block')->createInstance($variables['plugin_id'])->hasFormClass('off_canvas')) { // Add class and attributes to all blocks to allow Javascript to target. $variables['attributes']['class'][] = 'outside-in-editable'; $variables['attributes']['data-drupal-outsidein'] = 'editable'; @@ -139,17 +147,40 @@ function outside_in_toolbar_alter(&$items) { /** * Implements hook_block_alter(). + * + * Ensures every block plugin definition has an 'off_canvas' form specified. + * + * @see \Drupal\outside_in\Access\BlockPluginHasOffCanvasFormAccessCheck */ function outside_in_block_alter(&$definitions) { - if (!empty($definitions['system_branding_block'])) { - $definitions['system_branding_block']['forms']['off_canvas'] = SystemBrandingOffCanvasForm::class; - } - - // Since menu blocks use derivatives, check the definition ID instead of - // relying on the plugin ID. foreach ($definitions as &$definition) { - if ($definition['id'] === 'system_menu_block') { - $definition['forms']['off_canvas'] = SystemMenuOffCanvasForm::class; + // If the block plugin already defines an 'off_canvas' form, there's nothing + // to do. + if (isset($definition['forms']['off_canvas'])) { + continue; + } + + switch ($definition['id']) { + // Use specialized off-canvas forms when they're available. + // @todo move these into the corresponding block plugin annotations when Settings Tray becomes stable. + case 'system_menu_block': + $definition['forms']['off_canvas'] = SystemMenuOffCanvasForm::class; + break; + case 'system_branding_block': + $definition['forms']['off_canvas'] = SystemBrandingOffCanvasForm::class; + break; + + // No off-canvas form for the page title block, despite it having + // contextual links: it's too confusing that you're editing configuration, + // not content, so the title itself cannot actually be changed. + case 'page_title_block': + $definition['forms']['off_canvas'] = FALSE; + break; + + // Otherwise fall back to the built-in form for the block plugin. + default: + $definition['forms']['off_canvas'] = $definition['class']; + break; } } } diff --git a/core/modules/outside_in/outside_in.routing.yml b/core/modules/outside_in/outside_in.routing.yml index 18a564c..a778727 100644 --- a/core/modules/outside_in/outside_in.routing.yml +++ b/core/modules/outside_in/outside_in.routing.yml @@ -5,3 +5,4 @@ entity.block.off_canvas_form: _title_callback: '\Drupal\outside_in\Block\BlockEntityOffCanvasForm::title' requirements: _permission: 'administer blocks' + _access_block_plugin_has_offcanvas_form: 'TRUE' diff --git a/core/modules/outside_in/outside_in.services.yml b/core/modules/outside_in/outside_in.services.yml index ce82146..5d95c1e 100644 --- a/core/modules/outside_in/outside_in.services.yml +++ b/core/modules/outside_in/outside_in.services.yml @@ -4,3 +4,8 @@ services: arguments: ['@title_resolver', '@renderer'] tags: - { name: render.main_content_renderer, format: drupal_dialog.off_canvas } + + access_check.outside_in.block.off_canvas_form: + class: Drupal\outside_in\Access\BlockPluginHasOffCanvasFormAccessCheck + tags: + - { name: access_check, applies_to: _access_block_plugin_has_offcanvas_form } diff --git a/core/modules/outside_in/src/Access/BlockPluginHasOffCanvasFormAccessCheck.php b/core/modules/outside_in/src/Access/BlockPluginHasOffCanvasFormAccessCheck.php new file mode 100644 index 0000000..e8536f7 --- /dev/null +++ b/core/modules/outside_in/src/Access/BlockPluginHasOffCanvasFormAccessCheck.php @@ -0,0 +1,29 @@ +getPlugin(); + return AccessResult::allowedIf($block_plugin->hasFormClass('off_canvas')); + } + +} diff --git a/core/modules/outside_in/tests/src/Unit/Access/BlockPluginHasOffCanvasFormAccessCheckTest.php b/core/modules/outside_in/tests/src/Unit/Access/BlockPluginHasOffCanvasFormAccessCheckTest.php new file mode 100644 index 0000000..0643688 --- /dev/null +++ b/core/modules/outside_in/tests/src/Unit/Access/BlockPluginHasOffCanvasFormAccessCheckTest.php @@ -0,0 +1,69 @@ +randomMachineName(), $plugin_definition); + $block = $this->prophesize(BlockInterface::class); + $block->getPlugin()->willReturn($block_plugin); + + $access_check = new BlockPluginHasOffCanvasFormAccessCheck(); + $this->assertEquals($expected_access_result, $access_check->access($block->reveal())); + } + + public function providerTestAccess() { + return [ + 'set to class' => [ + [ + 'provider' => 'block_test', + 'forms' => [ + 'off_canvas' => $this->randomMachineName(), + ], + ], + new AccessResultAllowed(), + ], + 'not set' => [ + [ + 'provider' => 'block_test', + ], + new AccessResultNeutral(), + ], + 'set to FALSE' => [ + [ + 'provider' => 'block_test', + 'forms' => [ + 'off_canvas' => FALSE, + ], + ], + new AccessResultNeutral(), + ], + ]; + } + +} + +class TestBlockClass extends BlockBase { + + public function build() { + return []; + } + +}