Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -0,0 +1,23 @@
/**
* @file
* Styling for Settings Tray module.
*/
/*
* Position the edit toolbar tab.
* @todo Move changes into contextual module when Settings Tray is not
* experimental: https://www.drupal.org/node/2784591.
*/
.toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab {
float: left;
}
[dir="rtl"] .toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab {
float: right;
}
.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode a,
.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode input {
pointer-events: none;
}
.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode .contextual-links a {
pointer-events: inherit;
}

View file

@ -0,0 +1,19 @@
/**
* @file
* Motion effects for Settings Tray module.
*
* Motion effects are in a separate file so that they can be easily turned off
* to improve performance if desired.
*/
/* Transition the edit icon in the toolbar. */
#toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before {
transition: all 0.7s ease;
}
/* 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-settings-tray-edit-mode .settings-tray-editable,
.dialog-off-canvas-main-canvas.js-off-canvas-dialog-open .js-settings-tray-edit-mode .settings-tray-editable {
transition: all 0.7s ease;
}

View file

@ -0,0 +1,70 @@
/**
* @file
* Visual styling for Settings Tray module.
*/
/* @todo remove the @imports when we find a better way to load these styles last.
* https://www.drupal.org/node/1945262.
*/
/* Style the edit mode toolbar and tabs. */
#toolbar-bar.js-settings-tray-edit-mode {
background-image: linear-gradient(to bottom, #0a7bc1, #0a6eb4);
}
.js-settings-tray-edit-mode .toolbar-item:not(.toolbar-icon-edit) {
color: #999;
}
.js-settings-tray-edit-mode .toolbar-item:not(.toolbar-icon-edit) .is-active {
color: #333;
}
/* Style both the edit and editing states of the contextual links toggle tab. */
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item,
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active,
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:focus {
background-color: #0066a1;
background-image: linear-gradient(to bottom, #0066a1, #005b98);
color: #eee;
text-shadow: none;
font-weight: bold;
outline: none;
}
/* Make the hover of the inactive state the same as the active state. */
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:hover,
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active {
background-image: linear-gradient(to bottom, #0a7bc1, #0a6eb4);
color: #fff;
}
/* Make the hover of the active state the same as the inactive state. */
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover {
background-color: #0066a1;
background-image: linear-gradient(to bottom, #0066a1, #005b98);
color: #fff;
}
/* Make the inactive icon grey. */
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:before {
background-image: url(../../../misc/icons/bebebe/pencil.svg);
}
/* Make the active icon white. */
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:before {
background-image: url(../../../misc/icons/ffffff/pencil.svg);
}
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:hover:before {
background-image: url(../../../misc/icons/ffffff/pencil.svg);
}
.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:hover > .toolbar-icon-edit:before {
background-image: url(../../../misc/icons/ffffff/pencil.svg);
}
.toolbar-tab > .button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before {
background-image: url(../../../misc/icons/ffffff/pencil.svg);
}
/* Style the editables while in edit mode. */
.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode .settings-tray-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-settings-tray-edit-mode .settings-tray-editable:hover,
.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode .settings-tray-editable.settings-tray-active-editable {
background-color: rgba(0, 0, 0, 0.2);
}

View file

@ -0,0 +1,66 @@
/**
* @file
* Visual styling for the toolbar when Settings Tray module is enabled.
*/
/* @todo Move this into toolbar when module is not experimental:
* https://www.drupal.org/node/2784593.
*/
/* Style the edit mode toolbar and tabs. */
#toolbar-bar.js-settings-tray-edit-mode {
background-color: #fff;
}
#toolbar-bar.js-settings-tray-edit-mode .toolbar-item {
color: #999;
}
#toolbar-bar.js-settings-tray-edit-mode .toolbar-item .is-active {
color: #333;
}
/* Style both the edit and editing states of the contextual links toggle tab. */
.toolbar-icon-edit.toolbar-item {
background-color: #0066a1;
background-image: linear-gradient(to bottom, #0066a1, #005b98);
color: #eee;
text-shadow: 0 1px hsla(0, 0%, 0%, 0.5);
font-weight: 700;
-webkit-font-smoothing: antialiased;
}
.toolbar-icon-edit.toolbar-item.is-active {
background-color: #0a7bc1;
background-image: linear-gradient(to bottom, #0a7bc1, #0a6eb4);
color: #fff;
text-shadow: 0 1px hsla(0, 0%, 0%, 0.5);
font-weight: 700;
-webkit-font-smoothing: antialiased;
}
.toolbar-tab:hover > .toolbar-icon-edit,
.toolbar-icon-edit:focus .toolbar-item {
background-color: #0a7bc1;
background-image: linear-gradient(to bottom, #0a7bc1, #0a6eb4);
border-color: #1e5c90;
color: #fff;
outline: none;
}
.toolbar-icon.toolbar-icon-edit.toolbar-item:before,
button.toolbar-icon.toolbar-icon-edit.toolbar-item:before {
background-image: url(../../../misc/icons/bebebe/pencil.svg);
}
.toolbar-icon.toolbar-icon-edit.toolbar-item:before:hover,
button.toolbar-icon.toolbar-icon-edit.toolbar-item:before:focus {
background-image: url(../../../misc/icons/ffffff/pencil.svg);
}
.toolbar-icon.toolbar-icon-edit.toolbar-item:hover > .toolbar-icon-edit:before {
background-image: url(../../../misc/icons/ffffff/pencil.svg);
}
#toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before {
background-image: url(../../../misc/icons/ffffff/pencil.svg);
}
#toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active {
color: #fff;
}
#toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover {
background-image: linear-gradient(to bottom, #0a6fb4, #0a65aa);
}

View file

@ -0,0 +1,309 @@
/**
* @file
* Drupal's Settings Tray library.
*
* @private
*/
(($, Drupal) => {
const blockConfigureSelector = '[data-settings-tray-edit]';
const toggleEditSelector = '[data-drupal-settingstray="toggle"]';
const itemsToToggleSelector =
'[data-off-canvas-main-canvas], #toolbar-bar, [data-drupal-settingstray="editable"] a, [data-drupal-settingstray="editable"] button';
const contextualItemsSelector =
'[data-contextual-id] a, [data-contextual-id] button';
const quickEditItemSelector = '[data-quickedit-entity-id]';
/**
* Prevent default click events except contextual links.
*
* In edit mode the default action of click events is suppressed.
*
* @param {jQuery.Event} event
* The click event.
*/
function preventClick(event) {
// Do not prevent contextual links.
if ($(event.target).closest('.contextual-links').length) {
return;
}
event.preventDefault();
}
/**
* Close any active toolbar tray before entering edit mode.
*/
function closeToolbarTrays() {
$(Drupal.toolbar.models.toolbarModel.get('activeTab')).trigger('click');
}
/**
* Disables the QuickEdit module editor if open.
*/
function disableQuickEdit() {
$('.quickedit-toolbar button.action-cancel').trigger('click');
}
/**
* Closes/removes off-canvas.
*/
function closeOffCanvas() {
$('.ui-dialog-off-canvas .ui-dialog-titlebar-close').trigger('click');
}
/**
* Gets all items that should be toggled with class during edit mode.
*
* @return {jQuery}
* Items that should be toggled.
*/
function getItemsToToggle() {
return $(itemsToToggleSelector).not(contextualItemsSelector);
}
/**
* Helper to switch edit mode state.
*
* @param {boolean} editMode
* True enable edit mode, false disable edit mode.
*/
function setEditModeState(editMode) {
if (!document.querySelector('[data-off-canvas-main-canvas]')) {
throw new Error(
'data-off-canvas-main-canvas is missing from settings-tray-page-wrapper.html.twig',
);
}
editMode = !!editMode;
const $editButton = $(toggleEditSelector);
let $editables;
// Turn on edit mode.
if (editMode) {
$editButton.text(Drupal.t('Editing'));
closeToolbarTrays();
$editables = $('[data-drupal-settingstray="editable"]').once(
'settingstray',
);
if ($editables.length) {
// Use event capture to prevent clicks on links.
document
.querySelector('[data-off-canvas-main-canvas]')
.addEventListener('click', preventClick, true);
/**
* When a click occurs try and find the settings-tray edit link
* and click it.
*/
$editables.not(contextualItemsSelector).on('click.settingstray', e => {
// Contextual links are allowed to function in Edit mode.
if (
$(e.target).closest('.contextual').length ||
!localStorage.getItem('Drupal.contextualToolbar.isViewing')
) {
return;
}
$(e.currentTarget)
.find(blockConfigureSelector)
.trigger('click');
disableQuickEdit();
});
$(quickEditItemSelector)
.not(contextualItemsSelector)
.on('click.settingstray', e => {
/**
* For all non-contextual links or the contextual QuickEdit link
* close the off-canvas dialog.
*/
if (
!$(e.target)
.parent()
.hasClass('contextual') ||
$(e.target)
.parent()
.hasClass('quickedit')
) {
closeOffCanvas();
}
// Do not trigger if target is quick edit link to avoid loop.
if (
$(e.target)
.parent()
.hasClass('contextual') ||
$(e.target)
.parent()
.hasClass('quickedit')
) {
return;
}
$(e.currentTarget)
.find('li.quickedit a')
.trigger('click');
});
}
}
// Disable edit mode.
else {
$editables = $('[data-drupal-settingstray="editable"]').removeOnce(
'settingstray',
);
if ($editables.length) {
document
.querySelector('[data-off-canvas-main-canvas]')
.removeEventListener('click', preventClick, true);
$editables.off('.settingstray');
$(quickEditItemSelector).off('.settingstray');
}
$editButton.text(Drupal.t('Edit'));
closeOffCanvas();
disableQuickEdit();
}
getItemsToToggle().toggleClass('js-settings-tray-edit-mode', editMode);
$('.edit-mode-inactive').toggleClass('visually-hidden', editMode);
}
/**
* Helper to check the state of the settings-tray mode.
*
* @todo don't use a class for this.
*
* @return {boolean}
* State of the settings-tray edit mode.
*/
function isInEditMode() {
return $('#toolbar-bar').hasClass('js-settings-tray-edit-mode');
}
/**
* Helper to toggle Edit mode.
*/
function toggleEditMode() {
setEditModeState(!isInEditMode());
}
/**
* Prepares Ajax links to work with off-canvas and Settings Tray module.
*/
function prepareAjaxLinks() {
// Find all Ajax instances that use the 'off_canvas' renderer.
Drupal.ajax.instances
/**
* If there is an element and the renderer is 'off_canvas' then we want
* to add our changes.
*/
.filter(
instance =>
instance &&
$(instance.element).attr('data-dialog-renderer') === 'off_canvas',
)
/**
* Loop through all Ajax instances that use the 'off_canvas' renderer to
* set active editable ID.
*/
.forEach(instance => {
// Check to make sure existing dialogOptions aren't overridden.
if (!instance.options.data.hasOwnProperty('dialogOptions')) {
instance.options.data.dialogOptions = {};
}
instance.options.data.dialogOptions.settingsTrayActiveEditableId = $(
instance.element,
)
.parents('.settings-tray-editable')
.attr('id');
instance.progress = { type: 'fullscreen' };
});
}
/**
* Reacts to contextual links being added.
*
* @param {jQuery.Event} event
* The `drupalContextualLinkAdded` event.
* @param {object} data
* An object containing the data relevant to the event.
*
* @listens event:drupalContextualLinkAdded
*/
$(document).on('drupalContextualLinkAdded', (event, data) => {
/**
* When contextual links are add we need to set extra properties on the
* instances in Drupal.ajax.instances for them to work with Edit Mode.
*/
prepareAjaxLinks();
// When the first contextual link is added to the page set Edit Mode.
$('body')
.once('settings_tray.edit_mode_init')
.each(() => {
const editMode =
localStorage.getItem('Drupal.contextualToolbar.isViewing') ===
'false';
if (editMode) {
setEditModeState(true);
}
});
/**
* Bind a listener to all 'Quick edit' links for blocks. Click "Edit"
* button in toolbar to force Contextual Edit which starts Settings Tray
* edit mode also.
*/
data.$el.find(blockConfigureSelector).on('click.settingstray', () => {
if (!isInEditMode()) {
$(toggleEditSelector)
.trigger('click')
.trigger('click.settings_tray');
}
/**
* Always disable QuickEdit regardless of whether "EditMode" was just
* enabled.
*/
disableQuickEdit();
});
});
$(document).on('keyup.settingstray', e => {
if (isInEditMode() && e.keyCode === 27) {
Drupal.announce(Drupal.t('Exited edit mode.'));
toggleEditMode();
}
});
/**
* Toggle the js-settings-tray-edit-mode class on items that we want to
* disable while in edit mode.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Toggle the js-settings-tray-edit-mode class.
*/
Drupal.behaviors.toggleEditMode = {
attach() {
$(toggleEditSelector)
.once('settingstray')
.on('click.settingstray', toggleEditMode);
},
};
// Manage Active editable class on opening and closing of the dialog.
$(window).on({
'dialog:beforecreate': (event, dialog, $element, settings) => {
if ($element.is('#drupal-off-canvas')) {
$('body .settings-tray-active-editable').removeClass(
'settings-tray-active-editable',
);
const $activeElement = $(`#${settings.settingsTrayActiveEditableId}`);
if ($activeElement.length) {
$activeElement.addClass('settings-tray-active-editable');
}
}
},
'dialog:beforeclose': (event, dialog, $element) => {
if ($element.is('#drupal-off-canvas')) {
$('body .settings-tray-active-editable').removeClass(
'settings-tray-active-editable',
);
}
},
});
})(jQuery, Drupal);

View file

@ -0,0 +1,156 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, Drupal) {
var blockConfigureSelector = '[data-settings-tray-edit]';
var toggleEditSelector = '[data-drupal-settingstray="toggle"]';
var itemsToToggleSelector = '[data-off-canvas-main-canvas], #toolbar-bar, [data-drupal-settingstray="editable"] a, [data-drupal-settingstray="editable"] button';
var contextualItemsSelector = '[data-contextual-id] a, [data-contextual-id] button';
var quickEditItemSelector = '[data-quickedit-entity-id]';
function preventClick(event) {
if ($(event.target).closest('.contextual-links').length) {
return;
}
event.preventDefault();
}
function closeToolbarTrays() {
$(Drupal.toolbar.models.toolbarModel.get('activeTab')).trigger('click');
}
function disableQuickEdit() {
$('.quickedit-toolbar button.action-cancel').trigger('click');
}
function closeOffCanvas() {
$('.ui-dialog-off-canvas .ui-dialog-titlebar-close').trigger('click');
}
function getItemsToToggle() {
return $(itemsToToggleSelector).not(contextualItemsSelector);
}
function setEditModeState(editMode) {
if (!document.querySelector('[data-off-canvas-main-canvas]')) {
throw new Error('data-off-canvas-main-canvas is missing from settings-tray-page-wrapper.html.twig');
}
editMode = !!editMode;
var $editButton = $(toggleEditSelector);
var $editables = void 0;
if (editMode) {
$editButton.text(Drupal.t('Editing'));
closeToolbarTrays();
$editables = $('[data-drupal-settingstray="editable"]').once('settingstray');
if ($editables.length) {
document.querySelector('[data-off-canvas-main-canvas]').addEventListener('click', preventClick, true);
$editables.not(contextualItemsSelector).on('click.settingstray', function (e) {
if ($(e.target).closest('.contextual').length || !localStorage.getItem('Drupal.contextualToolbar.isViewing')) {
return;
}
$(e.currentTarget).find(blockConfigureSelector).trigger('click');
disableQuickEdit();
});
$(quickEditItemSelector).not(contextualItemsSelector).on('click.settingstray', function (e) {
if (!$(e.target).parent().hasClass('contextual') || $(e.target).parent().hasClass('quickedit')) {
closeOffCanvas();
}
if ($(e.target).parent().hasClass('contextual') || $(e.target).parent().hasClass('quickedit')) {
return;
}
$(e.currentTarget).find('li.quickedit a').trigger('click');
});
}
} else {
$editables = $('[data-drupal-settingstray="editable"]').removeOnce('settingstray');
if ($editables.length) {
document.querySelector('[data-off-canvas-main-canvas]').removeEventListener('click', preventClick, true);
$editables.off('.settingstray');
$(quickEditItemSelector).off('.settingstray');
}
$editButton.text(Drupal.t('Edit'));
closeOffCanvas();
disableQuickEdit();
}
getItemsToToggle().toggleClass('js-settings-tray-edit-mode', editMode);
$('.edit-mode-inactive').toggleClass('visually-hidden', editMode);
}
function isInEditMode() {
return $('#toolbar-bar').hasClass('js-settings-tray-edit-mode');
}
function toggleEditMode() {
setEditModeState(!isInEditMode());
}
function prepareAjaxLinks() {
Drupal.ajax.instances.filter(function (instance) {
return instance && $(instance.element).attr('data-dialog-renderer') === 'off_canvas';
}).forEach(function (instance) {
if (!instance.options.data.hasOwnProperty('dialogOptions')) {
instance.options.data.dialogOptions = {};
}
instance.options.data.dialogOptions.settingsTrayActiveEditableId = $(instance.element).parents('.settings-tray-editable').attr('id');
instance.progress = { type: 'fullscreen' };
});
}
$(document).on('drupalContextualLinkAdded', function (event, data) {
prepareAjaxLinks();
$('body').once('settings_tray.edit_mode_init').each(function () {
var editMode = localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false';
if (editMode) {
setEditModeState(true);
}
});
data.$el.find(blockConfigureSelector).on('click.settingstray', function () {
if (!isInEditMode()) {
$(toggleEditSelector).trigger('click').trigger('click.settings_tray');
}
disableQuickEdit();
});
});
$(document).on('keyup.settingstray', function (e) {
if (isInEditMode() && e.keyCode === 27) {
Drupal.announce(Drupal.t('Exited edit mode.'));
toggleEditMode();
}
});
Drupal.behaviors.toggleEditMode = {
attach: function attach() {
$(toggleEditSelector).once('settingstray').on('click.settingstray', toggleEditMode);
}
};
$(window).on({
'dialog:beforecreate': function dialogBeforecreate(event, dialog, $element, settings) {
if ($element.is('#drupal-off-canvas')) {
$('body .settings-tray-active-editable').removeClass('settings-tray-active-editable');
var $activeElement = $('#' + settings.settingsTrayActiveEditableId);
if ($activeElement.length) {
$activeElement.addClass('settings-tray-active-editable');
}
}
},
'dialog:beforeclose': function dialogBeforeclose(event, dialog, $element) {
if ($element.is('#drupal-off-canvas')) {
$('body .settings-tray-active-editable').removeClass('settings-tray-active-editable');
}
}
});
})(jQuery, Drupal);

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Documentation for Settings Tray API.
*/
/**
* @defgroup settings_tray Settings Tray API
* @{
* Settings Tray API
*
* @section sec_overview Overview and terminology
*
* The Settings Tray module allows blocks to be configured in a sidebar form
* without leaving the page. For example:
*
* - For every block, one can configure whether to display the block title or
* not, and optionally override it (block configuration).
* - For menu blocks, one can configure which menu levels to display (block
* configuration) but also the menu itself (menu configuration).
* - For the site branding block, one can change which branding elements to
* display (block configuration), but also the site name and slogan (simple
* configuration).
*
* Block visibility conditions are not included the sidebar form.
*
* @section sec_api The API: the form in the Settings Tray
*
* By default, the Settings Tray shows any block's built-in form in the
* off-canvas dialog.
*
* @see core/misc/dialog/off-canvas.es6.js
*
* However, many blocks would benefit from a tailored form which either:
* - limits the form items displayed in the Settings Tray to only items that
* affect the content of the rendered block
* - adds additional form items to edit configuration that is rendered by the
* block. See \Drupal\settings_tray\Form\SystemBrandingOffCanvasForm which
* adds site name and slogan configuration.
*
* These can be used to provide a better experience, so that the Settings Tray
* only displays what the user will expect to change when editing the block.
*
* Each block plugin can specify which form to use in the Settings Tray dialog
* in its plugin annotation:
* @code
* forms = {
* "settings_tray" = "\Drupal\some_module\Form\MyBlockOffCanvasForm",
* },
* @endcode
*
* In some cases, a block's content is not configurable (for example, the title,
* main content, and help blocks). Such blocks can opt out of providing a
* settings_tray form:
* @code
* forms = {
* "settings_tray" = FALSE,
* },
* @endcode
*
* Finally, blocks that do not specify a settings_tray form using the annotation
* above will automatically have it set to their plugin class. For example, the
* "Powered by Drupal" block plugin
* (\Drupal\system\Plugin\Block\SystemPoweredByBlock) automatically gets this
* added to its annotation:
* @code
* forms = {
* "settings_tray" = "\Drupal\system\Plugin\Block\SystemPoweredByBlock",
* },
* @endcode
*
* Therefore, the entire Settings Tray API is just this annotation: it controls
* what the Settings Tray does for a given block.
*
* @see settings_tray_block_alter()
* @see \Drupal\Tests\settings_tray\Functional\SettingsTrayBlockTest::testPossibleAnnotations()
*
* @}
*/

View file

@ -0,0 +1,10 @@
name: 'Settings Tray'
type: module
description: 'Provides a sidebar to configure blocks on the page.'
package: Core
version: VERSION
core: 8.x
dependencies:
- drupal:block
- drupal:toolbar
- drupal:contextual

View file

@ -0,0 +1,24 @@
<?php
/**
* @file
* Install, update and uninstall functions for the Settings Tray module.
*/
use Drupal\Core\Cache\Cache;
/**
* Implements hook_install().
*/
function settings_tray_install() {
// This module affects the rendering of blocks and of the page.
// @todo Remove in https://www.drupal.org/node/2783791.
Cache::invalidateTags(['rendered']);
// \Drupal\Core\Menu\ContextualLinkManager caches per-group definitions
// without associating the cache tag that would allow them to be cleared
// by its clearCachedDefinitions() implementation that is automatically
// invoked when modules are installed.
// @todo Remove when that is fixed in https://www.drupal.org/node/2773591.
\Drupal::service('cache.discovery')->deleteAll();
}

View file

@ -0,0 +1,19 @@
drupal.settings_tray:
version: VERSION
js:
js/settings_tray.js: {}
css:
component:
css/settings_tray.module.css: {}
css/settings_tray.motion.css: {}
css/settings_tray.toolbar.css: {}
theme:
# @todo Set the group higher than CSS_AGGREGATE_THEME so that it overrides
# both jQuery UI and Classy's dialog.css, remove in
# https://www.drupal.org/node/1945262.
css/settings_tray.theme.css: { group: 200 }
dependencies:
- core/jquery
- core/drupal
- core/jquery.once
- core/drupal.ajax

View file

@ -0,0 +1,10 @@
settings_tray.block_configure:
title: 'Quick edit'
route_name: 'entity.block.settings_tray_form'
group: 'block'
options:
attributes:
class: ['use-ajax']
data-dialog-type: dialog
data-dialog-renderer: off_canvas
data-settings-tray-edit: true

View file

@ -0,0 +1,187 @@
<?php
/**
* @file
* Allows configuring blocks and other configuration from the site front-end.
*/
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\block\entity\Block;
use Drupal\block\BlockInterface;
use Drupal\settings_tray\Block\BlockEntitySettingTrayForm;
/**
* Implements hook_help().
*/
function settings_tray_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.settings_tray':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Settings Tray module allows users with the <a href=":administer_block_permission">Administer blocks</a> and <a href=":contextual_permission">Use contextual links</a> permissions to edit blocks without visiting a separate page. For more information, see the <a href=":handbook_url">online documentation for the Settings Tray module</a>.', [':handbook_url' => 'https://www.drupal.org/documentation/modules/settings_tray', ':administer_block_permission' => \Drupal::url('user.admin_permissions', [], ['fragment' => 'module-block']), ':contextual_permission' => \Drupal::url('user.admin_permissions', [], ['fragment' => 'module-contextual'])]) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Editing blocks in place') . '</dt>';
$output .= '<dd>';
$output .= '<p>' . t('To edit blocks in place, either click the <strong>Edit</strong> button in the toolbar and then click on the block, or choose "Quick edit" from the block\'s contextual link. (See the <a href=":contextual">Contextual Links module help</a> for more information about how to use contextual links.)', [':contextual' => \Drupal::url('help.page', ['name' => 'contextual'])]) . '</p>';
$output .= '<p>' . t('The Settings Tray for the block will open in a sidebar, with a compact form for configuring what the block shows.') . '</p>';
$output .= '<p>' . t('Save the form and the changes will be immediately visible on the page.') . '</p>';
$output .= '</dd>';
$output .= '</dl>';
return ['#markup' => $output];
}
}
/**
* Implements hook_contextual_links_view_alter().
*
* Change Configure Blocks into off_canvas links.
*/
function settings_tray_contextual_links_view_alter(&$element, $items) {
if (isset($element['#links']['settings-trayblock-configure'])) {
// Place settings_tray link first.
$settings_tray_link = $element['#links']['settings-trayblock-configure'];
unset($element['#links']['settings-trayblock-configure']);
$element['#links'] = ['settings-trayblock-configure' => $settings_tray_link] + $element['#links'];
// If this is content block change title to avoid duplicate "Quick Edit".
if (isset($element['#links']['block-contentblock-edit'])) {
$element['#links']['settings-trayblock-configure']['title'] = t('Quick edit settings');
}
$element['#attached']['library'][] = 'core/drupal.dialog.off_canvas';
}
}
/**
* Checks if a block has overrides.
*
* @param \Drupal\block\BlockInterface $block
* The block to check for overrides.
*
* @return bool
* TRUE if the block has overrides otherwise FALSE.
*
* @internal
*/
function _settings_tray_has_block_overrides(BlockInterface $block) {
// @todo Replace the following with $block->hasOverrides() in https://www.drupal.org/project/drupal/issues/2910353
// and remove this function.
return \Drupal::config($block->getEntityType()->getConfigPrefix() . '.' . $block->id())->hasOverrides();
}
/**
* Implements hook_block_view_alter().
*/
function settings_tray_block_view_alter(array &$build) {
if (isset($build['#contextual_links']['block'])) {
// Ensure that contextual links vary by whether the block has config overrides
// or not.
// @see _contextual_links_to_id()
$build['#contextual_links']['block']['metadata']['has_overrides'] = _settings_tray_has_block_overrides($build['#block']) ? 1 : 0;
}
// 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']['settings_tray'] = [
'route_parameters' => [],
];
}
/**
* Implements hook_entity_type_build().
*/
function settings_tray_entity_type_build(array &$entity_types) {
/* @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
$entity_types['block']
->setFormClass('settings_tray', BlockEntitySettingTrayForm::class)
->setLinkTemplate('settings_tray-form', '/admin/structure/block/manage/{block}/settings-tray');
}
/**
* Implements hook_preprocess_HOOK() for block templates.
*/
function settings_tray_preprocess_block(&$variables) {
// Only blocks that have a settings_tray form and have no configuration
// overrides 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 would mean that it
// would be impossible to show Settings Tray's clickable regions immediately
// when the page loads. When latency is high, this will cause flicker.
// @see \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck
/** @var \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck $access_checker */
$access_checker = \Drupal::service('access_check.settings_tray.block.settings_tray_form');
/** @var \Drupal\Core\Block\BlockManagerInterface $block_plugin_manager */
$block_plugin_manager = \Drupal::service('plugin.manager.block');
/** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */
$block_plugin = $block_plugin_manager->createInstance($variables['plugin_id']);
if (isset($variables['elements']['#contextual_links']['block']['route_parameters']['block'])) {
$block = Block::load($variables['elements']['#contextual_links']['block']['route_parameters']['block']);
if ($access_checker->accessBlockPlugin($block_plugin)->isAllowed() && !_settings_tray_has_block_overrides($block)) {
// Add class and attributes to all blocks to allow Javascript to target.
$variables['attributes']['class'][] = 'settings-tray-editable';
$variables['attributes']['data-drupal-settingstray'] = 'editable';
}
}
}
/**
* Implements hook_toolbar_alter().
*
* Alters the 'contextual' toolbar tab if it exists (meaning the user is allowed
* to use contextual links) and if they can administer blocks.
*
* @todo Remove the "administer blocks" requirement in
* https://www.drupal.org/node/2822965.
*
* @see contextual_toolbar()
*/
function settings_tray_toolbar_alter(&$items) {
$items['contextual']['#cache']['contexts'][] = 'user.permissions';
if (isset($items['contextual']['tab']) && \Drupal::currentUser()->hasPermission('administer blocks')) {
$items['contextual']['#weight'] = -1000;
$items['contextual']['#attached']['library'][] = 'settings_tray/drupal.settings_tray';
$items['contextual']['tab']['#attributes']['data-drupal-settingstray'] = 'toggle';
// Set a class on items to mark whether they should be active in edit mode.
// @todo Create a dynamic method for modules to set their own items.
// https://www.drupal.org/node/2784589.
$edit_mode_items = ['contextual', 'block_place'];
foreach ($items as $key => $item) {
if (!in_array($key, $edit_mode_items) && (!isset($items[$key]['#wrapper_attributes']['class']) || !in_array('hidden', $items[$key]['#wrapper_attributes']['class']))) {
$items[$key]['#wrapper_attributes']['class'][] = 'edit-mode-inactive';
}
}
}
}
/**
* Implements hook_block_alter().
*
* Ensures every block plugin definition has an 'settings_tray' form specified.
*
* @see \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck
*/
function settings_tray_block_alter(&$definitions) {
foreach ($definitions as &$definition) {
// If a block plugin does not define its own 'settings_tray' form, use the
// plugin class itself.
if (!isset($definition['forms']['settings_tray'])) {
$definition['forms']['settings_tray'] = $definition['class'];
}
}
}
/**
* Implements hook_css_alter().
*/
function settings_tray_css_alter(&$css, AttachedAssetsInterface $assets) {
// @todo Remove once conditional ordering is introduced in
// https://www.drupal.org/node/1945262.
$path = drupal_get_path('module', 'settings_tray') . '/css/settings_tray.theme.css';
if (isset($css[$path])) {
// Use 200 to come after CSS_AGGREGATE_THEME.
$css[$path]['group'] = 200;
}
}

View file

@ -0,0 +1,16 @@
entity.block.settings_tray_form:
path: '/admin/structure/block/manage/{block}/settings-tray'
defaults:
_entity_form: 'block.settings_tray'
_title_callback: '\Drupal\settings_tray\Block\BlockEntitySettingTrayForm::title'
requirements:
_permission: 'administer blocks'
_access_block_plugin_has_settings_tray_form: 'TRUE'
_access_block_has_overrides_settings_tray_form: 'TRUE'
# Deprecated.
# @see entity.block.settings_tray_form
# @see \Drupal\settings_tray\RouteProcessor\BlockEntityOffCanvasFormRouteProcessorBC
# @todo Remove in Drupal 9.0.0.
entity.block.off_canvas_form:
path: ''

View file

@ -0,0 +1,18 @@
services:
access_check.settings_tray.block.has_overrides:
class: Drupal\settings_tray\Access\BlockHasOverridesAccessCheck
tags:
- { name: access_check, applies_to: _access_block_has_overrides_settings_tray_form }
access_check.settings_tray.block.settings_tray_form:
class: Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck
tags:
- { name: access_check, applies_to: _access_block_plugin_has_settings_tray_form }
# BC layers.
# @todo Remove in Drupal 9.0.0.
settings_tray.route_processor_off_canvas_form_bc:
class: \Drupal\settings_tray\RouteProcessor\BlockEntityOffCanvasFormRouteProcessorBC
arguments: ['@router.route_provider']
public: false
tags:
- { name: route_processor_outbound }

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\settings_tray\Access;
use Drupal\block\BlockInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
/**
* Determines whether the requested block has a 'settings_tray' form.
*
* @internal
*/
class BlockHasOverridesAccessCheck implements AccessInterface {
/**
* Checks access for accessing a block's 'settings_tray' form.
*
* @param \Drupal\block\BlockInterface $block
* The block whose 'settings_tray' form is being accessed.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(BlockInterface $block) {
return AccessResult::allowedIf(!_settings_tray_has_block_overrides($block));
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace Drupal\settings_tray\Access;
use Drupal\block\BlockInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\Core\Routing\Access\AccessInterface;
/**
* Determines whether the requested block has a 'settings_tray' form.
*
* @internal
*/
class BlockPluginHasSettingsTrayFormAccessCheck implements AccessInterface {
/**
* Checks access for accessing a block's 'settings_tray' form.
*
* @param \Drupal\block\BlockInterface $block
* The block whose 'settings_tray' form is being accessed.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(BlockInterface $block) {
/** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */
$block_plugin = $block->getPlugin();
return $this->accessBlockPlugin($block_plugin);
}
/**
* Checks access for accessing a block plugin's 'settings_tray' form.
*
* @param \Drupal\Core\Block\BlockPluginInterface $block_plugin
* The block plugin whose 'settings_tray' form is being accessed.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*
* @see settings_tray_preprocess_block()
*/
public function accessBlockPlugin(BlockPluginInterface $block_plugin) {
return AccessResult::allowedIf($block_plugin instanceof PluginWithFormsInterface && $block_plugin->hasFormClass('settings_tray'));
}
}

View file

@ -0,0 +1,175 @@
<?php
namespace Drupal\settings_tray\Block;
use Drupal\block\BlockForm;
use Drupal\block\BlockInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\Core\Url;
/**
* Provides form for block instance forms when used in the off-canvas dialog.
*
* This form removes advanced sections of regular block form such as the
* visibility settings, machine ID and region.
*
* @internal
*/
class BlockEntitySettingTrayForm extends BlockForm {
use AjaxFormHelperTrait;
/**
* Provides a title callback to get the block's admin label.
*
* @param \Drupal\block\BlockInterface $block
* The block entity.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The title.
*/
public function title(BlockInterface $block) {
// @todo Wrap "Configure " in <span class="visually-hidden"></span> once
// https://www.drupal.org/node/2359901 is fixed.
return $this->t('Configure @block', ['@block' => $block->getPlugin()->getPluginDefinition()['admin_label']]);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
// Create link to full block form.
$query = [];
if ($destination = $this->getRequest()->query->get('destination')) {
$query['destination'] = $destination;
}
$form['advanced_link'] = [
'#type' => 'link',
'#title' => $this->t('Advanced block options'),
'#url' => $this->entity->toUrl('edit-form', ['query' => $query]),
'#weight' => 1000,
];
// Remove the ID and region elements.
unset($form['id'], $form['region'], $form['settings']['admin_label']);
if (isset($form['settings']['label_display']) && isset($form['settings']['label'])) {
// Only show the label input if the label will be shown on the page.
$form['settings']['label_display']['#weight'] = -100;
$form['settings']['label']['#states']['visible'] = [
':input[name="settings[label_display]"]' => ['checked' => TRUE],
];
// Relabel to "Block title" because on the front-end this may be confused
// with page title.
$form['settings']['label']['#title'] = $this->t("Block title");
$form['settings']['label_display']['#title'] = $this->t("Display block title");
}
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Save @block', ['@block' => $this->entity->getPlugin()->getPluginDefinition()['admin_label']]);
$actions['delete']['#access'] = FALSE;
return $actions;
}
/**
* {@inheritdoc}
*/
protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) {
// Do not display the visibility.
return [];
}
/**
* {@inheritdoc}
*/
protected function validateVisibility(array $form, FormStateInterface $form_state) {
// Intentionally empty.
}
/**
* {@inheritdoc}
*/
protected function submitVisibility(array $form, FormStateInterface $form_state) {
// Intentionally empty.
}
/**
* {@inheritdoc}
*/
protected function getPluginForm(BlockPluginInterface $block) {
if ($block instanceof PluginWithFormsInterface) {
return $this->pluginFormFactory->createInstance($block, 'settings_tray', 'configure');
}
return $block;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$form['actions']['submit']['#ajax'] = [
'callback' => '::ajaxSubmit',
];
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
// static::ajaxSubmit() requires data-drupal-selector to be the same between
// the various Ajax requests. A bug in \Drupal\Core\Form\FormBuilder
// prevents that from happening unless $form['#id'] is also the same.
// Normally, #id is set to a unique HTML ID via Html::getUniqueId(), but
// here we bypass that in order to work around the data-drupal-selector bug.
// This is okay so long as we assume that this form only ever occurs once on
// a page.
// @todo Remove this workaround once https://www.drupal.org/node/2897377 is
// fixed.
$form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']);
return $form;
}
/**
* {@inheritdoc}
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
if ($redirect_url = $this->getRedirectUrl()) {
$command = new RedirectCommand($redirect_url->setAbsolute()->toString());
}
else {
// Settings Tray always provides a destination.
throw new \Exception("No destination provided by Settings Tray form");
}
$response = new AjaxResponse();
return $response->addCommand($command);
}
/**
* Gets the form's redirect URL from 'destination' provide in the request.
*
* @return \Drupal\Core\Url|null
* The redirect URL or NULL if dialog should just be closed.
*/
protected function getRedirectUrl() {
// \Drupal\Core\Routing\RedirectDestination::get() cannot be used directly
// because it will use <current> if 'destination' is not in the query
// string.
if ($this->getRequest()->query->has('destination') && $destination = $this->getRedirectDestination()->get()) {
return Url::fromUserInput('/' . $destination);
}
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Drupal\settings_tray\RouteProcessor;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\Routing\Route;
/**
* Processes the Block entity off-canvas form BC route.
*
* @internal
*/
class BlockEntityOffCanvasFormRouteProcessorBC implements OutboundRouteProcessorInterface {
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* Constructs a BlockEntityOffCanvasFormRouteProcessorBC object.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
*/
public function __construct(RouteProviderInterface $route_provider) {
$this->routeProvider = $route_provider;
}
/**
* {@inheritdoc}
*/
public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
if ($route_name === 'entity.block.off_canvas_form') {
$redirected_route_name = 'entity.block.settings_tray_form';
@trigger_error(sprintf("The '%s' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the '%s' route instead.", $route_name, $redirected_route_name), E_USER_DEPRECATED);
static::overwriteRoute($route, $this->routeProvider->getRouteByName($redirected_route_name));
}
}
/**
* Overwrites one route's metadata with the other's.
*
* @param \Symfony\Component\Routing\Route $target_route
* The route whose metadata to overwrite.
* @param \Symfony\Component\Routing\Route $source_route
* The route whose metadata to read from.
*
* @see \Symfony\Component\Routing\Route
*/
protected static function overwriteRoute(Route $target_route, Route $source_route) {
$target_route->setPath($source_route->getPath());
$target_route->setDefaults($source_route->getDefaults());
$target_route->setRequirements($source_route->getRequirements());
$target_route->setOptions($source_route->getOptions());
$target_route->setHost($source_route->getHost());
$target_route->setSchemes($source_route->getSchemes());
$target_route->setMethods($source_route->getMethods());
}
}

View file

@ -0,0 +1,7 @@
name: 'Configuration override test for Settings Tray'
type: module
package: Testing
version: VERSION
core: 8.x
dependencies:
- drupal:settings_tray

View file

@ -0,0 +1,5 @@
services:
settings_tray_override_test.overrider:
class: Drupal\settings_tray_override_test\ConfigOverrider
tags:
- { name: config.factory.override }

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\settings_tray_override_test;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryOverrideInterface;
use Drupal\Core\Config\StorageInterface;
/**
* Provides an overridden block for Settings Tray testing.
*
* @see \Drupal\Tests\settings_tray\FunctionalJavascript\SettingsTrayBlockFormTest::testOverriddenDisabled()
*/
class ConfigOverrider implements ConfigFactoryOverrideInterface {
/**
* {@inheritdoc}
*/
public function loadOverrides($names) {
$overrides = [];
if (in_array('block.block.overridden_block', $names)) {
if (\Drupal::state()->get('settings_tray_override_test.block')) {
$overrides = $overrides + ['block.block.overridden_block' => ['settings' => ['label' => 'Now this will be the label.']]];
}
}
if (in_array('system.site', $names)) {
if (\Drupal::state()->get('settings_tray_override_test.site_name')) {
$overrides = $overrides + ['system.site' => ['name' => 'Llama Fan Club']];
}
}
if (in_array('system.menu.main', $names)) {
if (\Drupal::state()->get('settings_tray_override_test.menu')) {
$overrides = $overrides + ['system.menu.main' => ['label' => 'Labely label']];
}
}
return $overrides;
}
/**
* {@inheritdoc}
*/
public function getCacheSuffix() {
return 'ConfigOverrider';
}
/**
* {@inheritdoc}
*/
public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata($name) {
return new CacheableMetadata();
}
}

View file

@ -0,0 +1,9 @@
name: 'Settings Tray Test'
type: module
description: 'Provides Settings Tray test functionality.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- drupal:block
- drupal:settings_tray

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\settings_tray_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormBase;
/**
* @see \Drupal\settings_tray_test\Plugin\Block\SettingsTrayFormAnnotationIsClassBlock
*/
class SettingsTrayFormAnnotationIsClassBlockForm extends PluginFormBase {
/**
* The block plugin.
*
* @var \Drupal\Core\Block\BlockPluginInterface
*/
protected $plugin;
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = $this->plugin->buildConfigurationForm($form, $form_state);
$form['some_setting'] = [
'#type' => 'select',
'#title' => t('Some setting'),
'#options' => [
'a' => 'A',
'b' => 'B',
],
'#required' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\settings_tray_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Block that explicitly provides a "settings_tray" form class.
*
* @Block(
* id = "settings_tray_test_class",
* admin_label = "Settings Tray test block: forms[settings_tray]=class",
* forms = {
* "settings_tray" = "\Drupal\settings_tray_test\Form\SettingsTrayFormAnnotationIsClassBlockForm",
* },
* )
*/
class SettingsTrayFormAnnotationIsClassBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return ['#markup' => '<span>class</span>'];
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\settings_tray_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Block that explicitly provides no "settings_tray" form, thus opting out.
*
* @Block(
* id = "settings_tray_test_false",
* admin_label = "Settings Tray test block: forms[settings_tray]=FALSE",
* forms = {
* "settings_tray" = FALSE,
* },
* )
*/
class SettingsTrayFormAnnotationIsFalseBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return ['#markup' => '<span>FALSE</span>'];
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\settings_tray_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Block that does nothing explicit for Settings Tray.
*
* @Block(
* id = "settings_tray_test_none",
* admin_label = "Settings Tray test block: forms[settings_tray] is not specified",
* )
*/
class SettingsTrayFormAnnotationNoneBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return ['#markup' => '<span>none</span>'];
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\settings_tray_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a 'Block with validation error' test block.
*
* @Block(
* id = "settings_tray_test_validation",
* admin_label = @Translation("Block with validation error")
* )
*/
class ValidationErrorBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return ['#markup' => '<span>If I had more time this would be very witty :(.</span>'];
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::validateConfigurationForm($form, $form_state);
$form_state->setError($form['label'], 'Sorry system error. Please save again.');
}
}

View file

@ -0,0 +1,27 @@
.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode a,
.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode input {
pointer-events: inherit !important;
}
/**
* Remove all transitions for testing.
*/
* {
/* CSS transitions. */
-o-transition-property: none !important;
-moz-transition-property: none !important;
-ms-transition-property: none !important;
-webkit-transition-property: none !important;
transition-property: none !important;
/* CSS transforms. */
-o-transform: none !important;
-moz-transform: none !important;
-ms-transform: none !important;
-webkit-transform: none !important;
transform: none !important;
/* CSS animations. */
-webkit-animation: none !important;
-moz-animation: none !important;
-o-animation: none !important;
-ms-animation: none !important;
animation: none !important;
}

View file

@ -0,0 +1,8 @@
name: 'CSS Test fix'
type: module
description: 'Provides CSS fixes for tests.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- drupal:settings_tray

View file

@ -0,0 +1,5 @@
drupal.css_fix:
version: VERSION
css:
theme:
css/css_fix.theme.css: {}

View file

@ -0,0 +1,16 @@
<?php
/**
* @file
* Module for attaching CSS during tests.
*
* CSS pointer-events properties cause testing errors.
*/
/**
* Implements hook_page_attachments().
*/
function settings_tray_test_css_page_attachments(array &$attachments) {
// Unconditionally attach an asset to the page.
$attachments['#attached']['library'][] = 'settings_tray_test_css/drupal.css_fix';
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\settings_tray\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Settings Tray BC routes.
*
* @group settings_tray
* @group legacy
*/
class BcRoutesTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'settings_tray',
];
/**
* @expectedDeprecation The 'entity.block.off_canvas_form' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'entity.block.settings_tray_form' route instead.
*/
public function testOffCanvasFormRouteBc() {
$block = $this->placeBlock('system_powered_by_block');
$url_for_current_route = Url::fromRoute('entity.block.settings_tray_form', ['block' => $block->id()])->toString(TRUE)->getGeneratedUrl();
$url_for_bc_route = Url::fromRoute('entity.block.off_canvas_form', ['block' => $block->id()])->toString(TRUE)->getGeneratedUrl();
$this->assertSame($url_for_current_route, $url_for_bc_route);
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Drupal\Tests\settings_tray\Functional;
use Drupal\block\Entity\Block;
use Drupal\Tests\BrowserTestBase;
/**
* Tests opening and saving block forms in the off-canvas dialog.
*
* @group settings_tray
*/
class SettingsTrayTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'settings_tray',
'settings_tray_test',
];
/**
* Gets the block CSS selector.
*
* @param \Drupal\block\Entity\Block $block
* The block.
*
* @return string
* The CSS selector.
*/
protected function getBlockSelector(Block $block) {
return '#block-' . $block->id();
}
/**
* Tests the 3 possible forms[settings_tray] annotations: class, FALSE, none.
*
* There is also functional JS test coverage to ensure that the two blocks
* that support Settings Tray (the "class" and "none" cases) do work
* correctly.
*
* @see SettingsTrayBlockFormTest::testBlocks()
*/
public function testPossibleAnnotations() {
$test_block_plugin_ids = [
// Block that explicitly provides an "settings_tray" form class.
'settings_tray_test_class',
// Block that explicitly provides no "settings_tray" form, thus opting out.
'settings_tray_test_false',
// Block that does nothing explicit for Settings Tray.
'settings_tray_test_none',
];
$placed_blocks = [];
foreach ($test_block_plugin_ids as $plugin_id) {
$placed_blocks[$plugin_id] = $this->placeBlock($plugin_id);
}
$this->drupalGet('');
$web_assert = $this->assertSession();
foreach ($placed_blocks as $plugin_id => $placed_block) {
$block_selector = $this->getBlockSelector($placed_block);
// All blocks are rendered.
$web_assert->elementExists('css', $block_selector);
// All blocks except 'settings_tray_test_false' are editable. For more
// detailed test coverage, which requires JS execution, see
// \Drupal\Tests\settings_tray\FunctionalJavascript\SettingsTrayBlockFormTest::testBlocks().
if ($plugin_id === 'settings_tray_test_false') {
$web_assert->elementNotExists('css', "{$block_selector}[data-drupal-settingstray=\"editable\"]");
}
else {
$web_assert->elementExists('css', "{$block_selector}[data-drupal-settingstray=\"editable\"]");
}
}
}
/**
* Tests that certain blocks opt out from Settings Tray.
*/
public function testOptOut() {
$web_assert = $this->assertSession();
$non_excluded_block = $this->placeBlock('system_powered_by_block');
$excluded_block_plugin_ids = ['page_title_block', 'system_main_block', 'settings_tray_test_false'];
$block_selectors = [];
// Place blocks that should be excluded.
foreach ($excluded_block_plugin_ids as $excluded_block_plugin_id) {
// The block HTML 'id' attribute will be "block-[block_id]".
$block_selectors[] = $this->getBlockSelector($this->placeBlock($excluded_block_plugin_id));
}
$this->drupalGet('');
// Assert that block has been marked as "editable" and contextual that
// should exist does.
$web_assert->elementExists('css', $this->getBlockSelector($non_excluded_block) . "[data-drupal-settingstray=\"editable\"]");
// Assert that each block that has a "forms[settings_tray] = FALSE" annotation:
// - is still rendered on the page
// - but is not marked as "editable" by settings_tray_preprocess_block()
// - and does not have the Settings Tray contextual link.
foreach ($block_selectors as $block_selector) {
$web_assert->elementExists('css', $block_selector);
$web_assert->elementNotExists('css', "{$block_selector}[data-drupal-settingstray=\"editable\"]");
$web_assert->elementNotExists('css', "$block_selector [data-settings-tray-edit]");
}
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace Drupal\Tests\settings_tray\FunctionalJavascript;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\user\Entity\Role;
/**
* Tests handling of configuration overrides.
*
* @group settings_tray
*/
class ConfigAccessTest extends SettingsTrayTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'menu_link_content',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$user = $this->createUser([
'administer blocks',
'access contextual links',
'access toolbar',
]);
$this->drupalLogin($user);
}
/**
* Tests access to block forms with related configuration is correct.
*/
public function testBlockConfigAccess() {
$page = $this->getSession()->getPage();
$web_assert = $this->assertSession();
// Confirm that System Branding block does not expose Site Name field
// without permission.
$block = $this->placeBlock('system_branding_block');
$this->drupalGet('user');
$this->enableEditMode();
$this->openBlockForm($this->getBlockSelector($block));
// The site name field should not appear because the user doesn't have
// permission.
$web_assert->fieldNotExists('settings[site_information][site_name]');
$page->pressButton('Save Site branding');
$this->waitForOffCanvasToClose();
$this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)');
$web_assert->assertWaitOnAjaxRequest();
// Confirm we did not save changes to the configuration.
$this->assertEquals('Drupal', \Drupal::configFactory()->getEditable('system.site')->get('name'));
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), ['administer site configuration']);
$this->drupalGet('user');
$this->openBlockForm($this->getBlockSelector($block));
// The site name field should appear because the user does have permission.
$web_assert->fieldExists('settings[site_information][site_name]');
// Confirm that the Menu block does not expose menu configuration without
// permission.
// Add a link or the menu will not render.
$menu_link_content = MenuLinkContent::create([
'title' => 'This is on the menu',
'menu_name' => 'main',
'link' => ['uri' => 'route:<front>'],
]);
$menu_link_content->save();
$this->assertNotEmpty($menu_link_content->isEnabled());
$menu_without_overrides = \Drupal::configFactory()->getEditable('system.menu.main')->get();
$block = $this->placeBlock('system_menu_block:main');
$this->drupalGet('user');
$web_assert->pageTextContains('This is on the menu');
$this->openBlockForm($this->getBlockSelector($block));
// Edit menu form should not appear because the user doesn't have
// permission.
$web_assert->pageTextNotContains('Edit menu');
$page->pressButton('Save Main navigation');
$this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)');
$web_assert->assertWaitOnAjaxRequest();
// Confirm we did not save changes to the menu or the menu link.
$this->assertEquals($menu_without_overrides, \Drupal::configFactory()->getEditable('system.menu.main')->get());
$menu_link_content = MenuLinkContent::load($menu_link_content->id());
$this->assertNotEmpty($menu_link_content->isEnabled());
// Confirm menu is still on the page.
$this->drupalGet('user');
$web_assert->pageTextContains('This is on the menu');
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), ['administer menu']);
$this->drupalGet('user');
$web_assert->pageTextContains('This is on the menu');
$this->openBlockForm($this->getBlockSelector($block));
// Edit menu form should appear because the user does have permission.
$web_assert->pageTextContains('Edit menu');
}
}

View file

@ -0,0 +1,174 @@
<?php
namespace Drupal\Tests\settings_tray\FunctionalJavascript;
use Drupal\block\Entity\Block;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\user\Entity\Role;
/**
* Tests handling of configuration overrides.
*
* @group settings_tray
*/
class OverriddenConfigurationTest extends SettingsTrayTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'settings_tray_override_test',
'menu_ui',
'menu_link_content',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$user = $this->createUser([
'administer blocks',
'access contextual links',
'access toolbar',
]);
$this->drupalLogin($user);
}
/**
* Test blocks with overridden related configuration removed when overridden.
*/
public function testOverriddenConfigurationRemoved() {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), ['administer site configuration', 'administer menu']);
// Confirm the branding block does include 'site_information' section when
// the site name is not overridden.
$branding_block = $this->placeBlock('system_branding_block');
$this->drupalGet('user');
$this->enableEditMode();
$this->openBlockForm($this->getBlockSelector($branding_block));
$web_assert->fieldExists('settings[site_information][site_name]');
// Confirm the branding block does not include 'site_information' section
// when the site name is overridden.
$this->container->get('state')->set('settings_tray_override_test.site_name', TRUE);
$this->drupalGet('user');
$this->openBlockForm($this->getBlockSelector($branding_block));
$web_assert->fieldNotExists('settings[site_information][site_name]');
$page->pressButton('Save Site branding');
$this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)');
$web_assert->assertWaitOnAjaxRequest();
// Confirm we did not save changes to the configuration.
$this->assertEquals('Llama Fan Club', \Drupal::configFactory()->get('system.site')->get('name'));
$this->assertEquals('Drupal', \Drupal::configFactory()->getEditable('system.site')->get('name'));
// Add a link or the menu will not render.
$menu_link_content = MenuLinkContent::create([
'title' => 'This is on the menu',
'menu_name' => 'main',
'link' => ['uri' => 'route:<front>'],
]);
$menu_link_content->save();
// Confirm the menu block does include menu section when the menu is not
// overridden.
$menu_block = $this->placeBlock('system_menu_block:main');
$web_assert->assertWaitOnAjaxRequest();
$this->drupalGet('user');
$web_assert->pageTextContains('This is on the menu');
$this->openBlockForm($this->getBlockSelector($menu_block));
$web_assert->elementExists('css', '#menu-overview');
// Confirm the menu block does not include menu section when the menu is
// overridden.
$this->container->get('state')->set('settings_tray_override_test.menu', TRUE);
$this->drupalGet('user');
$web_assert->pageTextContains('This is on the menu');
$menu_with_overrides = \Drupal::configFactory()->get('system.menu.main')->get();
$menu_without_overrides = \Drupal::configFactory()->getEditable('system.menu.main')->get();
$this->openBlockForm($this->getBlockSelector($menu_block));
$web_assert->elementNotExists('css', '#menu-overview');
$page->pressButton('Save Main navigation');
$this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)');
$web_assert->assertWaitOnAjaxRequest();
// Confirm we did not save changes to the configuration.
$this->assertEquals('Labely label', \Drupal::configFactory()->get('system.menu.main')->get('label'));
$this->assertEquals('Main navigation', \Drupal::configFactory()->getEditable('system.menu.main')->get('label'));
$this->assertEquals($menu_with_overrides, \Drupal::configFactory()->get('system.menu.main')->get());
$this->assertEquals($menu_without_overrides, \Drupal::configFactory()->getEditable('system.menu.main')->get());
$web_assert->pageTextContains('This is on the menu');
}
/**
* Tests that blocks with configuration overrides are disabled.
*/
public function testOverriddenBlock() {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
$overridden_block = $this->placeBlock('system_powered_by_block', [
'id' => 'overridden_block',
'label_display' => 1,
'label' => 'This will be overridden.',
]);
$this->drupalGet('user');
$block_selector = $this->getBlockSelector($overridden_block);
// Confirm the block is marked as Settings Tray editable.
$this->assertEquals('editable', $page->find('css', $block_selector)->getAttribute('data-drupal-settingstray'));
// Confirm the label is not overridden.
$web_assert->elementContains('css', $block_selector, 'This will be overridden.');
$this->enableEditMode();
$this->openBlockForm($block_selector);
// Confirm the block Settings Tray functionality is disabled when block is
// overridden.
$this->container->get('state')->set('settings_tray_override_test.block', TRUE);
$overridden_block->save();
$block_config = \Drupal::configFactory()->getEditable('block.block.overridden_block');
$block_config->set('settings', $block_config->get('settings'))->save();
$this->drupalGet('user');
$this->assertOverriddenBlockDisabled($overridden_block, 'Now this will be the label.');
// Test a non-overridden block does show the form in the off-canvas dialog.
$block = $this->placeBlock('system_powered_by_block', [
'label_display' => 1,
'label' => 'Labely label',
]);
$this->drupalGet('user');
$block_selector = $this->getBlockSelector($block);
// Confirm the block is marked as Settings Tray editable.
$this->assertEquals('editable', $page->find('css', $block_selector)->getAttribute('data-drupal-settingstray'));
// Confirm the label is not overridden.
$web_assert->elementContains('css', $block_selector, 'Labely label');
$this->openBlockForm($block_selector);
}
/**
* Asserts that an overridden block has Settings Tray disabled.
*
* @param \Drupal\block\Entity\Block $overridden_block
* The overridden block.
* @param string $override_text
* The override text that should appear in the block.
*/
protected function assertOverriddenBlockDisabled(Block $overridden_block, $override_text) {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
$block_selector = $this->getBlockSelector($overridden_block);
$block_id = $overridden_block->id();
// Confirm the block does not have a quick edit link.
$contextual_links = $page->findAll('css', "$block_selector .contextual-links li a");
$this->assertNotEmpty($contextual_links);
foreach ($contextual_links as $link) {
$this->assertNotContains("/admin/structure/block/manage/$block_id/off-canvas", $link->getAttribute('href'));
}
// Confirm the block is not marked as Settings Tray editable.
$this->assertFalse($page->find('css', $block_selector)
->hasAttribute('data-drupal-settingstray'));
// Confirm the text is actually overridden.
$web_assert->elementContains('css', $this->getBlockSelector($overridden_block), $override_text);
}
}

View file

@ -0,0 +1,207 @@
<?php
namespace Drupal\Tests\settings_tray\FunctionalJavascript;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\user\Entity\Role;
/**
* Test Settings Tray and Quick Edit modules integration.
*
* @group settings_tray
*/
class QuickEditIntegrationTest extends SettingsTrayTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'block_content',
'quickedit',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$user = $this->createUser([
'administer blocks',
'access contextual links',
'access toolbar',
'administer nodes',
'access in-place editing',
]);
$this->drupalLogin($user);
}
/**
* Tests QuickEdit links behavior.
*/
public function testQuickEditLinks() {
$quick_edit_selector = '#quickedit-entity-toolbar';
$node_selector = '[data-quickedit-entity-id="node/1"]';
$body_selector = '[data-quickedit-field-id="node/1/body/en/full"]';
$web_assert = $this->assertSession();
// Create a Content type and two test nodes.
$this->createContentType(['type' => 'page']);
$auth_role = Role::load(Role::AUTHENTICATED_ID);
$this->grantPermissions($auth_role, [
'edit any page content',
'access content',
]);
$node = $this->createNode(
[
'title' => 'Page One',
'type' => 'page',
'body' => [
[
'value' => 'Regular NODE body for the test.',
'format' => 'plain_text',
],
],
]
);
$page = $this->getSession()->getPage();
$block_plugin = 'system_powered_by_block';
foreach ($this->getTestThemes() as $theme) {
$this->enableTheme($theme);
$block = $this->placeBlock($block_plugin);
$block_selector = $this->getBlockSelector($block);
// Load the same page twice.
foreach ([1, 2] as $page_load_times) {
$this->drupalGet('node/' . $node->id());
// The 2nd page load we should already be in edit mode.
if ($page_load_times == 1) {
$this->enableEditMode();
}
// In Edit mode clicking field should open QuickEdit toolbar.
$page->find('css', $body_selector)->click();
$this->assertElementVisibleAfterWait('css', $quick_edit_selector);
$this->disableEditMode();
// Exiting Edit mode should close QuickEdit toolbar.
$web_assert->elementNotExists('css', $quick_edit_selector);
// When not in Edit mode QuickEdit toolbar should not open.
$page->find('css', $body_selector)->click();
$web_assert->elementNotExists('css', $quick_edit_selector);
$this->enableEditMode();
$this->openBlockForm($block_selector);
$page->find('css', $body_selector)->click();
$this->assertElementVisibleAfterWait('css', $quick_edit_selector);
// Off-canvas dialog should be closed when opening QuickEdit toolbar.
$this->waitForOffCanvasToClose();
$this->openBlockForm($block_selector);
// QuickEdit toolbar should be closed when opening Off-canvas dialog.
$web_assert->elementNotExists('css', $quick_edit_selector);
}
// Check using contextual links to invoke QuickEdit and open the tray.
$this->drupalGet('node/' . $node->id());
$web_assert->assertWaitOnAjaxRequest();
$this->disableEditMode();
// Open QuickEdit toolbar before going into Edit mode.
$this->clickContextualLink($node_selector, "Quick edit");
$this->assertElementVisibleAfterWait('css', $quick_edit_selector);
// Open off-canvas and enter Edit mode via contextual link.
$this->clickContextualLink($block_selector, "Quick edit");
$this->waitForOffCanvasToOpen();
// QuickEdit toolbar should be closed when opening off-canvas dialog.
$web_assert->elementNotExists('css', $quick_edit_selector);
// Open QuickEdit toolbar via contextual link while in Edit mode.
$this->clickContextualLink($node_selector, "Quick edit", FALSE);
$this->waitForOffCanvasToClose();
$this->assertElementVisibleAfterWait('css', $quick_edit_selector);
$this->disableEditMode();
}
}
/**
* Tests that contextual links in custom blocks are changed.
*
* "Quick edit" is quickedit.module link.
* "Quick edit settings" is settings_tray.module link.
*/
public function testCustomBlockLinks() {
$this->createBlockContentType('basic', TRUE);
$block_content = $this->createBlockContent('Custom Block', 'basic', TRUE);
$this->placeBlock('block_content:' . $block_content->uuid(), ['id' => 'custom']);
$this->drupalGet('user');
$page = $this->getSession()->getPage();
$this->toggleContextualTriggerVisibility('#block-custom');
$page->find('css', '#block-custom .contextual button')->press();
$links = $page->findAll('css', "#block-custom .contextual-links li a");
$link_labels = [];
/** @var \Behat\Mink\Element\NodeElement $link */
foreach ($links as $link) {
$link_labels[$link->getAttribute('href')] = $link->getText();
}
$href = array_search('Quick edit', $link_labels);
$this->assertEquals('', $href);
$href = array_search('Quick edit settings', $link_labels);
$destination = (string) $this->loggedInUser->toUrl()->toString();
$this->assertTrue(strstr($href, "/admin/structure/block/manage/custom/settings-tray?destination=$destination") !== FALSE);
}
/**
* Creates a custom block.
*
* @param bool|string $title
* (optional) Title of block. When no value is given uses a random name.
* Defaults to FALSE.
* @param string $bundle
* (optional) Bundle name. Defaults to 'basic'.
* @param bool $save
* (optional) Whether to save the block. Defaults to TRUE.
*
* @return \Drupal\block_content\Entity\BlockContent
* Created custom block.
*/
protected function createBlockContent($title = FALSE, $bundle = 'basic', $save = TRUE) {
$title = $title ?: $this->randomName();
$block_content = BlockContent::create([
'info' => $title,
'type' => $bundle,
'langcode' => 'en',
'body' => [
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
'format' => 'plain_text',
],
]);
if ($block_content && $save === TRUE) {
$block_content->save();
}
return $block_content;
}
/**
* Creates a custom block type (bundle).
*
* @param string $label
* The block type label.
* @param bool $create_body
* Whether or not to create the body field.
*
* @return \Drupal\block_content\Entity\BlockContentType
* Created custom block type.
*/
protected function createBlockContentType($label, $create_body = FALSE) {
$bundle = BlockContentType::create([
'id' => $label,
'label' => $label,
'revision' => FALSE,
]);
$bundle->save();
if ($create_body) {
block_content_add_body_field($bundle->id());
}
return $bundle;
}
}

View file

@ -0,0 +1,291 @@
<?php
namespace Drupal\Tests\settings_tray\FunctionalJavascript;
use Drupal\settings_tray_test\Plugin\Block\SettingsTrayFormAnnotationIsClassBlock;
use Drupal\settings_tray_test\Plugin\Block\SettingsTrayFormAnnotationNoneBlock;
use Drupal\user\Entity\Role;
/**
* Testing opening and saving block forms in the off-canvas dialog.
*
* @group settings_tray
*/
class SettingsTrayBlockFormTest extends SettingsTrayTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'search',
'settings_tray_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$user = $this->createUser([
'administer blocks',
'access contextual links',
'access toolbar',
'administer nodes',
'search content',
]);
$this->drupalLogin($user);
}
/**
* Tests opening off-canvas dialog by click blocks and elements in the blocks.
*/
public function testBlocks() {
foreach ($this->getBlockTests() as $test) {
call_user_func_array([$this, 'doTestBlocks'], $test);
}
}
/**
* Tests opening off-canvas dialog by click blocks and elements in the blocks.
*/
protected function doTestBlocks($theme, $block_plugin, $new_page_text, $element_selector, $label_selector, $button_text, $toolbar_item, $permissions) {
if ($permissions) {
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), $permissions);
}
if ($new_page_text) {
// Some asserts can be based on this value, so it should not be the same
// for different blocks, because it can be saved in the site config.
$new_page_text = $new_page_text . ' ' . $theme . ' ' . $block_plugin;
}
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
$this->enableTheme($theme);
$block = $this->placeBlock($block_plugin);
$block_selector = $this->getBlockSelector($block);
$block_id = $block->id();
$this->drupalGet('user');
$link = $web_assert->waitForElement('css', "$block_selector .contextual-links li a");
$this->assertEquals('Quick edit', $link->getHtml(), "'Quick edit' is the first contextual link for the block.");
$destination = (string) $this->loggedInUser->toUrl()->toString();
$this->assertContains("/admin/structure/block/manage/$block_id/settings-tray?destination=$destination", $link->getAttribute('href'));
if (isset($toolbar_item)) {
// Check that you can open a toolbar tray and it will be closed after
// entering edit mode.
if ($element = $page->find('css', "#toolbar-administration a.is-active")) {
// If a tray was open from page load close it.
$element->click();
$this->waitForNoElement("#toolbar-administration a.is-active");
}
$page->find('css', $toolbar_item)->click();
$this->assertElementVisibleAfterWait('css', "{$toolbar_item}.is-active");
}
$this->enableEditMode();
if (isset($toolbar_item)) {
$this->waitForNoElement("{$toolbar_item}.is-active");
}
$this->openBlockForm($block_selector);
switch ($block_plugin) {
case 'system_powered_by_block':
// Confirm "Display Title" is not checked.
$web_assert->checkboxNotChecked('settings[label_display]');
// Confirm Title is not visible.
$this->assertEquals($this->isLabelInputVisible(), FALSE, 'Label is not visible');
$page->checkField('settings[label_display]');
$this->assertEquals($this->isLabelInputVisible(), TRUE, 'Label is visible');
// Fill out form, save the form.
$page->fillField('settings[label]', $new_page_text);
break;
case 'system_branding_block':
// Fill out form, save the form.
$page->fillField('settings[site_information][site_name]', $new_page_text);
break;
case 'settings_tray_test_class':
$web_assert->elementExists('css', '[data-drupal-selector="edit-settings-some-setting"]');
break;
}
if (isset($new_page_text)) {
$page->pressButton($button_text);
// Make sure the changes are present.
$new_page_text_locator = "$block_selector $label_selector:contains($new_page_text)";
$this->assertElementVisibleAfterWait('css', $new_page_text_locator);
// The page is loaded with the new change but make sure page is
// completely loaded.
$this->assertPageLoadComplete();
}
$this->openBlockForm($block_selector);
$this->disableEditMode();
// Canvas should close when editing module is closed.
$this->waitForOffCanvasToClose();
$this->enableEditMode();
// Open block form by clicking a element inside the block.
// This confirms that default action for links and form elements is
// suppressed.
$this->openBlockForm("$block_selector {$element_selector}", $block_selector);
$web_assert->elementTextContains('css', '.contextual-toolbar-tab button', 'Editing');
$web_assert->elementAttributeContains('css', '.dialog-off-canvas-main-canvas', 'class', 'js-settings-tray-edit-mode');
// Simulate press the Escape key.
$this->getSession()->executeScript('jQuery("body").trigger(jQuery.Event("keyup", { keyCode: 27 }));');
$this->waitForOffCanvasToClose();
$this->getSession()->wait(100);
$this->getSession()->executeScript("jQuery('[data-quickedit-entity-id]').trigger('mouseleave')");
$this->getSession()->getPage()->find('css', static::TOOLBAR_EDIT_LINK_SELECTOR)->mouseOver();
$this->assertEditModeDisabled();
$this->assertNotEmpty($web_assert->waitForElement('css', '#drupal-live-announce:contains(Exited edit mode)'));
$this->waitForNoElement('.contextual-toolbar-tab button:contains(Editing)');
$web_assert->elementAttributeNotContains('css', '.dialog-off-canvas-main-canvas', 'class', 'js-settings-tray-edit-mode');
// Clean up test data so each test does not impact the next.
$block->delete();
if ($permissions) {
user_role_revoke_permissions(Role::AUTHENTICATED_ID, $permissions);
}
}
/**
* Creates tests for ::testBlocks().
*/
public function getBlockTests() {
$blocks = [];
foreach ($this->getTestThemes() as $theme) {
$blocks += [
"$theme: block-powered" => [
'theme' => $theme,
'block_plugin' => 'system_powered_by_block',
'new_page_text' => 'Can you imagine anyone showing the label on this block',
'element_selector' => 'span a',
'label_selector' => 'h2',
'button_text' => 'Save Powered by Drupal',
'toolbar_item' => '#toolbar-item-user',
NULL,
],
"$theme: block-branding" => [
'theme' => $theme,
'block_plugin' => 'system_branding_block',
'new_page_text' => 'The site that will live a very short life',
'element_selector' => "a[rel='home']:last-child",
'label_selector' => "a[rel='home']:last-child",
'button_text' => 'Save Site branding',
'toolbar_item' => '#toolbar-item-administration',
['administer site configuration'],
],
"$theme: block-search" => [
'theme' => $theme,
'block_plugin' => 'search_form_block',
'new_page_text' => NULL,
'element_selector' => '#edit-submit',
'label_selector' => 'h2',
'button_text' => 'Save Search form',
'toolbar_item' => NULL,
NULL,
],
// This is the functional JS test coverage accompanying
// \Drupal\Tests\settings_tray\Functional\SettingsTrayTest::testPossibleAnnotations().
"$theme: " . SettingsTrayFormAnnotationIsClassBlock::class => [
'theme' => $theme,
'block_plugin' => 'settings_tray_test_class',
'new_page_text' => NULL,
'element_selector' => 'span',
'label_selector' => NULL,
'button_text' => NULL,
'toolbar_item' => NULL,
NULL,
],
// This is the functional JS test coverage accompanying
// \Drupal\Tests\settings_tray\Functional\SettingsTrayTest::testPossibleAnnotations().
"$theme: " . SettingsTrayFormAnnotationNoneBlock::class => [
'theme' => $theme,
'block_plugin' => 'settings_tray_test_none',
'new_page_text' => NULL,
'element_selector' => 'span',
'label_selector' => NULL,
'button_text' => NULL,
'toolbar_item' => NULL,
NULL,
],
];
}
return $blocks;
}
/**
* Tests enabling and disabling Edit Mode.
*/
public function testEditModeEnableDisable() {
foreach ($this->getTestThemes() as $theme) {
$this->enableTheme($theme);
$block = $this->placeBlock('system_powered_by_block');
foreach (['contextual_link', 'toolbar_link'] as $enable_option) {
$this->drupalGet('user');
$this->assertEditModeDisabled();
switch ($enable_option) {
// Enable Edit mode.
case 'contextual_link':
$this->clickContextualLink($this->getBlockSelector($block), "Quick edit");
$this->waitForOffCanvasToOpen();
$this->assertEditModeEnabled();
break;
case 'toolbar_link':
$this->enableEditMode();
break;
}
$this->disableEditMode();
// Make another page request to ensure Edit mode is still disabled.
$this->drupalGet('user');
$this->assertEditModeDisabled();
// Make sure on this page request it also re-enables and disables
// correctly.
$this->enableEditMode();
$this->disableEditMode();
}
}
}
/**
* Test that validation errors appear in the off-canvas dialog.
*/
public function testValidationMessages() {
$page = $this->getSession()->getPage();
$web_assert = $this->assertSession();
foreach ($this->getTestThemes() as $theme) {
$this->enableTheme($theme);
$block = $this->placeBlock('settings_tray_test_validation');
$this->drupalGet('user');
$this->enableEditMode();
$this->openBlockForm($this->getBlockSelector($block));
$page->pressButton('Save Block with validation error');
$web_assert->assertWaitOnAjaxRequest();
// The settings_tray_test_validation test plugin form always has a
// validation error.
$web_assert->elementContains('css', '#drupal-off-canvas', 'Sorry system error. Please save again');
$this->disableEditMode();
$block->delete();
}
}
/**
* {@inheritdoc}
*/
protected function getTestThemes() {
// Remove 'seven' theme. Setting Tray "Edit Mode" will not work with 'seven'
// because it removes all contextual links the off-canvas dialog should.
return array_filter(parent::getTestThemes(), function ($theme) {
return $theme !== 'seven';
});
}
}

View file

@ -0,0 +1,189 @@
<?php
namespace Drupal\Tests\settings_tray\FunctionalJavascript;
use Drupal\block\Entity\Block;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase;
/**
* Base class for Settings Tray tests.
*/
class SettingsTrayTestBase extends OffCanvasTestBase {
use ContextualLinkClickTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'settings_tray',
// Add test module to override CSS pointer-events properties because they
// cause test failures.
'settings_tray_test_css',
];
const TOOLBAR_EDIT_LINK_SELECTOR = '#toolbar-bar div.contextual-toolbar-tab button';
const LABEL_INPUT_SELECTOR = 'input[data-drupal-selector="edit-settings-label"]';
/**
* Open block form by clicking the element found with a css selector.
*
* @param string $block_selector
* A css selector selects the block or an element within it.
* @param string $contextual_link_container
* The element that contains the contextual links. If none provide the
* $block_selector will be used.
*/
protected function openBlockForm($block_selector, $contextual_link_container = '') {
if (!$contextual_link_container) {
$contextual_link_container = $block_selector;
}
// Ensure that contextual link element is present because this is required
// to open the off-canvas dialog in edit mode.
$contextual_link = $this->assertSession()->waitForElement('css', "$contextual_link_container .contextual-links a");
$this->assertNotEmpty($contextual_link);
// When page first loads Edit Mode is not triggered until first contextual
// link is added.
$this->assertElementVisibleAfterWait('css', '.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode');
// Ensure that all other Ajax activity is completed.
$this->assertSession()->assertWaitOnAjaxRequest();
$block = $this->getSession()->getPage()->find('css', $block_selector);
$block->mouseOver();
$block->click();
$this->waitForOffCanvasToOpen();
$this->assertOffCanvasBlockFormIsValid();
}
/**
* Enables edit mode by pressing edit button in the toolbar.
*/
protected function enableEditMode() {
$this->pressToolbarEditButton();
$this->assertEditModeEnabled();
}
/**
* Disables edit mode by pressing edit button in the toolbar.
*/
protected function disableEditMode() {
$this->assertSession()->assertWaitOnAjaxRequest();
$this->pressToolbarEditButton();
$this->assertEditModeDisabled();
}
/**
* Press the toolbar Edit button provided by the contextual module.
*/
protected function pressToolbarEditButton() {
$this->assertSession()->waitForElement('css', '[data-contextual-id] .contextual-links a');
$edit_button = $this->getSession()
->getPage()
->find('css', static::TOOLBAR_EDIT_LINK_SELECTOR);
$this->getSession()->executeScript("jQuery('[data-quickedit-entity-id]').trigger('mouseleave')");
$edit_button->mouseOver();
$edit_button->press();
}
/**
* Assert that edit mode has been properly disabled.
*/
protected function assertEditModeDisabled() {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
$this->getSession()->executeScript("jQuery('[data-quickedit-entity-id]').trigger('mouseleave')");
$page->find('css', static::TOOLBAR_EDIT_LINK_SELECTOR)->mouseOver();
$this->assertTrue($page->waitFor(10, function ($page) {
return !$page->find('css', '.contextual .trigger:not(.visually-hidden)');
}));
// Contextual triggers should be hidden.
$web_assert->elementExists('css', '.contextual .trigger.visually-hidden');
// No contextual triggers should be not hidden.
$web_assert->elementNotExists('css', '.contextual .trigger:not(.visually-hidden)');
// The toolbar edit button should read "Edit".
$web_assert->elementContains('css', static::TOOLBAR_EDIT_LINK_SELECTOR, 'Edit');
// The main canvas element should NOT have the "js-settings-tray-edit-mode"
// class.
$web_assert->elementNotExists('css', '.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode');
}
/**
* Assert that edit mode has been properly enabled.
*/
protected function assertEditModeEnabled() {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
// Move the mouse over the toolbar button so that isn't over a contextual
// links area which cause the contextual link to be shown.
$page->find('css', static::TOOLBAR_EDIT_LINK_SELECTOR)->mouseOver();
$this->assertTrue($page->waitFor(10, function ($page) {
return !$page->find('css', '.contextual .trigger.visually-hidden');
}));
// No contextual triggers should be hidden.
$web_assert->elementNotExists('css', '.contextual .trigger.visually-hidden');
// The toolbar edit button should read "Editing".
$web_assert->elementContains('css', static::TOOLBAR_EDIT_LINK_SELECTOR, 'Editing');
// The main canvas element should have the "js-settings-tray-edit-mode" class.
$web_assert->elementExists('css', '.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode');
}
/**
* Asserts that Off-Canvas block form is valid.
*/
protected function assertOffCanvasBlockFormIsValid() {
$web_assert = $this->assertSession();
// Confirm that Block title display label has been changed.
$web_assert->elementTextContains('css', '.form-item-settings-label-display label', 'Display block title');
// Confirm Block title label is shown if checkbox is checked.
if ($this->getSession()->getPage()->find('css', 'input[name="settings[label_display]"]')->isChecked()) {
$this->assertEquals($this->isLabelInputVisible(), TRUE, 'Label is visible');
$web_assert->elementTextContains('css', '.form-item-settings-label label', 'Block title');
}
else {
$this->assertEquals($this->isLabelInputVisible(), FALSE, 'Label is not visible');
}
// Check that common block form elements exist.
$web_assert->elementExists('css', static::LABEL_INPUT_SELECTOR);
$web_assert->elementExists('css', 'input[data-drupal-selector="edit-settings-label-display"]');
// Check that advanced block form elements do not exist.
$web_assert->elementNotExists('css', 'input[data-drupal-selector="edit-visibility-request-path-pages"]');
$web_assert->elementNotExists('css', 'select[data-drupal-selector="edit-region"]');
}
/**
* {@inheritdoc}
*/
protected function getTestThemes() {
// Remove 'seven' theme. Settings Tray "Edit Mode" will not work with
// 'seven' because it removes all contextual links.
return array_filter(parent::getTestThemes(), function ($theme) {
return $theme !== 'seven';
});
}
/**
* Gets the block CSS selector.
*
* @param \Drupal\block\Entity\Block $block
* The block.
*
* @return string
* The CSS selector.
*/
public function getBlockSelector(Block $block) {
return '#block-' . str_replace('_', '-', $block->id());
}
/**
* Determines if the label input is visible.
*
* @return bool
* TRUE if the label is visible, FALSE if it is not.
*/
protected function isLabelInputVisible() {
return $this->getSession()->getPage()->find('css', static::LABEL_INPUT_SELECTOR)->isVisible();
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Drupal\Tests\settings_tray\Unit\Access;
use Drupal\block\BlockInterface;
use Drupal\Core\Access\AccessResultAllowed;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Access\AccessResultNeutral;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck
* @group settings_tray
*/
class BlockPluginHasSettingsTrayFormAccessCheckTest extends UnitTestCase {
/**
* @covers ::access
* @covers ::accessBlockPlugin
* @dataProvider providerTestAccess
*/
public function testAccess($with_forms, array $plugin_definition, AccessResultInterface $expected_access_result) {
$block_plugin = $this->prophesize()->willImplement(BlockPluginInterface::class);
if ($with_forms) {
$block_plugin->willImplement(PluginWithFormsInterface::class);
$block_plugin->hasFormClass(Argument::type('string'))->will(function ($arguments) use ($plugin_definition) {
return !empty($plugin_definition['forms'][$arguments[0]]);
});
}
$block = $this->prophesize(BlockInterface::class);
$block->getPlugin()->willReturn($block_plugin->reveal());
$access_check = new BlockPluginHasSettingsTrayFormAccessCheck();
$this->assertEquals($expected_access_result, $access_check->access($block->reveal()));
$this->assertEquals($expected_access_result, $access_check->accessBlockPlugin($block_plugin->reveal()));
}
/**
* Provides test data for ::testAccess().
*/
public function providerTestAccess() {
$annotation_forms_settings_tray_class = [
'forms' => [
'settings_tray' => $this->randomMachineName(),
],
];
$annotation_forms_settings_tray_not_set = [];
$annotation_forms_settings_tray_false = [
'forms' => [
'settings_tray' => FALSE,
],
];
return [
'block plugin with forms, forms[settings_tray] set to class' => [
TRUE,
$annotation_forms_settings_tray_class,
new AccessResultAllowed(),
],
'block plugin with forms, forms[settings_tray] not set' => [
TRUE,
$annotation_forms_settings_tray_not_set,
new AccessResultNeutral(),
],
'block plugin with forms, forms[settings_tray] set to FALSE' => [
TRUE,
$annotation_forms_settings_tray_false,
new AccessResultNeutral(),
],
// In practice, all block plugins extend BlockBase, which means they all
// implement PluginWithFormsInterface, but this may change in the future.
// This ensures Settings Tray will continue to work correctly.
'block plugin without forms, forms[settings_tray] set to class' => [
FALSE,
$annotation_forms_settings_tray_class,
new AccessResultNeutral(),
],
'block plugin without forms, forms[settings_tray] not set' => [
FALSE,
$annotation_forms_settings_tray_not_set,
new AccessResultNeutral(),
],
'block plugin without forms, forms[settings_tray] set to FALSE' => [
FALSE,
$annotation_forms_settings_tray_false,
new AccessResultNeutral(),
],
];
}
}