diff --git a/css/ui_toggle.css b/css/ui_toggle.css index 687609d706bec3df44b7bb1948bc88cf1fb3318a..3d44375b4b365f0707e643a6a52ef9536e4f7361 100644 --- a/css/ui_toggle.css +++ b/css/ui_toggle.css @@ -32,6 +32,48 @@ text-align: right; } +.frontend-editing-toolbar-toggle { + background: rgba(var(--fe-editing-primary-color), 0.6); + padding: 0.5rem 0.75rem; + text-transform: uppercase; + color: white; + display: block; + font-weight: 500; + border-radius: 999px; + box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; + text-align: right; + height: 1.9rem; + margin-right: 1rem; + min-width: 5rem; +} + +.frontend-editing-toolbar-toggle a { + color: white; +} + +.gin-secondary-toolbar .toolbar-secondary .toolbar-bar .toolbar-tab.frontend-editing-toolbar-toggle:hover { + border-radius: 999px; +} + +.gin-secondary-toolbar .toolbar-secondary .toolbar-bar .toolbar-tab.frontend-editing-toolbar-toggle:focus-within { + border-radius: 999px; +} + +.gin-secondary-toolbar__layout-container a.frontend-editing-toggle-link:focus { + box-shadow: none; +} + +a.frontend-editing-toggle-link, +.toolbar-tab a.frontend-editing-toggle-link:focus:not(:hover) { + text-decoration: none; +} + +.frontend-editing-toggle-link:hover:focus, +.frontend-editing-toggle-link:hover { + color: white; + text-decoration: underline; +} + .frontend-editing-toggle a::before { content: ' '; position: absolute; @@ -50,7 +92,26 @@ border-radius: 999px; } -.frontend-editing-toggle a.frontend-editing--enabled { +.frontend-editing-toolbar-toggle a::before { + content: ' '; + position: absolute; + top: 0; + left: 0.5rem; + bottom: 0; + width: 1.5rem; + height: 1.5rem; + margin-bottom: auto; + margin-top: auto; + background-color: #fff; + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iIzIzMjIyMiIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTkgMy4xNzFhMS44MjkgMS44MjkgMCAwIDAtMS4yOTMuNTM2TDQuMzk1IDE3LjAxOWwtLjk3IDMuNTU2IDMuNTU2LS45N0wyMC4yOTMgNi4yOTNBMS44MjkgMS44MjkgMCAwIDAgMTkgMy4xN1ptLTEuNDY1LTEuNzA4YTMuODI5IDMuODI5IDAgMCAxIDQuMTcyIDYuMjQ0bC0xMy41IDEzLjVhMSAxIDAgMCAxLS40NDQuMjU4bC01LjUgMS41YTEgMSAwIDAgMS0xLjIyOC0xLjIyOGwxLjUtNS41YTEgMSAwIDAgMSAuMjU4LS40NDRsMTMuNS0xMy41YTMuODI5IDMuODI5IDAgMCAxIDEuMjQyLS44M1oiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg=="); + background-size: 14px; + background-repeat: no-repeat; + background-position: center; + border-radius: 100%; +} + +.frontend-editing-toggle a.frontend-editing--enabled, +.frontend-editing-toolbar-toggle:has(a.frontend-editing--enabled) { background: rgb(var(--fe-editing-primary-color)); text-align: left; } @@ -59,6 +120,10 @@ left: calc(100% - 1.75rem - 0.5rem); } +.frontend-editing-toolbar-toggle a.frontend-editing--enabled::before { + left: calc(100% - 1.75rem - 0.25rem); +} + .frontend-editing-toggle-not-configured { box-shadow: 0 0 0 0 rgba(255, 0, 0, 1); transform: scale(1); @@ -81,3 +146,23 @@ box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); } } + +@media only screen and (max-width: 975px) { + .frontend-editing-toolbar-toggle { + margin-top: 0.25rem; + margin-right: 0; + } + + .frontend-editing-toolbar-toggle a::before { + left: auto; + right: 2.35rem; + } + + .frontend-editing-toolbar-toggle a.frontend-editing--enabled::before { + left: calc(100% - 1rem - 0.1rem); + } + + a.frontend-editing-toggle-link { + position: relative; + } +} diff --git a/frontend_editing.module b/frontend_editing.module index 1ee9a3e1be54ff6b9253ebfe349f947bcbdce810..f55ebeebe53405b0045ec6b431a51041e606ba11 100644 --- a/frontend_editing.module +++ b/frontend_editing.module @@ -18,6 +18,7 @@ use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FormatterInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Hook\Attribute\LegacyHook; use Drupal\Core\Render\Element; use Drupal\Core\Render\Markup; use Drupal\Core\Routing\RouteMatchInterface; @@ -25,6 +26,7 @@ use Drupal\Core\Url; use Drupal\frontend_editing\Ajax\CloseSidePanelCommand; use Drupal\frontend_editing\Ajax\EntityPreviewCommand; use Drupal\frontend_editing\Ajax\ScrollTopCommand; +use Drupal\frontend_editing\Hook\FrontendEditingHooks; use Drupal\node\NodeInterface; /** @@ -274,6 +276,7 @@ function frontend_editing_field_formatter_third_party_settings_form(FormatterInt } return $element; } + /** * Check formatter third party settings. */ @@ -289,6 +292,7 @@ function frontend_editing_check_third_party_formatter_settings(&$element) { } } } + /** * Implements hook_field_formatter_settings_summary_alter(). */ @@ -802,3 +806,11 @@ function frontend_editing_preprocess_page_title(&$variables) { } } } + +/** + * Implements hook_toolbar(). + */ +#[LegacyHook] +function frontend_editing_toolbar() { + return \Drupal::service(FrontendEditingHooks::class)->toolbar(); +} diff --git a/frontend_editing.services.yml b/frontend_editing.services.yml index 696c3d4aafa0641a7f655b36cd54f58977cc31f4..159ff0081951f68b1d2ab2e80730adb92f7f4646 100644 --- a/frontend_editing.services.yml +++ b/frontend_editing.services.yml @@ -9,3 +9,10 @@ services: frontend_editing.form_builder: class: Drupal\frontend_editing\FrontendEditingFormBuilder arguments: ['@entity_type.manager', '@entity.form_builder', '@form_builder'] + frontend_editing.toolbar_item: + class: Drupal\frontend_editing\ToolbarItem + arguments: [ '@plugin.manager.element_info', '@user.data', '@current_user' ] + Drupal\frontend_editing\ToolbarItem: '@frontend_editing.toolbar_item' + Drupal\frontend_editing\Hook\FrontendEditingHooks: + class: Drupal\frontend_editing\Hook\FrontendEditingHooks + autowire: true diff --git a/src/Controller/FrontendEditingController.php b/src/Controller/FrontendEditingController.php index 6a76076a2aeac2ffb8022e005f0640e291576a63..b447d9f9d8afef36fa76cb9c962cd2e33243248e 100644 --- a/src/Controller/FrontendEditingController.php +++ b/src/Controller/FrontendEditingController.php @@ -11,7 +11,6 @@ use Drupal\Core\Ajax\InvokeCommand; use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityFormBuilder; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Render\Element; use Drupal\Core\Render\RendererInterface; @@ -79,6 +78,8 @@ class FrontendEditingController extends ControllerBase { * The user data storage. * @param \Drupal\frontend_editing\FieldReferenceHelperInterface $field_reference_helper * The field reference helper. + * @param \Drupal\frontend_editing\FrontendEditingFormBuilderInterface $frontend_editing_form_builder + * The frontend editing form builder. */ public function __construct(RendererInterface $renderer, EntityRepositoryInterface $entity_repository, UserDataInterface $userData, FieldReferenceHelperInterface $field_reference_helper, FrontendEditingFormBuilderInterface $frontend_editing_form_builder) { $this->renderer = $renderer; @@ -121,22 +122,22 @@ class FrontendEditingController extends ControllerBase { $response = new AjaxResponse(); if ($new_state) { $message = $this->t('Frontend editing has been enabled.'); - $response->addCommand(new InvokeCommand('#frontend-editing-toggle-link', 'addClass', ['frontend-editing--enabled'])); - $response->addCommand(new InvokeCommand('#frontend-editing-toggle-link', 'text', [$this->t('On')])); + $response->addCommand(new InvokeCommand('.frontend-editing-toggle-link', 'addClass', ['frontend-editing--enabled'])); + $response->addCommand(new InvokeCommand('.frontend-editing-toggle-link', 'text', [$this->t('On')])); $response->addCommand(new InvokeCommand('body', 'removeClass', ['frontend-editing--hidden'])); } else { $message = $this->t('Frontend editing has been disabled.'); - $response->addCommand(new InvokeCommand('#frontend-editing-toggle-link', 'removeClass', ['frontend-editing--enabled'])); - $response->addCommand(new InvokeCommand('#frontend-editing-toggle-link', 'text', [$this->t('Off')])); + $response->addCommand(new InvokeCommand('.frontend-editing-toggle-link', 'removeClass', ['frontend-editing--enabled'])); + $response->addCommand(new InvokeCommand('.frontend-editing-toggle-link', 'text', [$this->t('Off')])); $response->addCommand(new InvokeCommand('body', 'addClass', ['frontend-editing--hidden'])); } $response->addCommand(new MessageCommand($message, NULL, ['type' => 'status'])); - $response->addCommand(new InvokeCommand('#frontend-editing-toggle-link', 'attr', [ + $response->addCommand(new InvokeCommand('.frontend-editing-toggle-link', 'attr', [ 'data-toggle-state', $new_state, ])); - $response->addCommand(new InvokeCommand('#frontend-editing-toggle-link', 'removeClass', ['frontend-editing-toggle-not-configured'])); + $response->addCommand(new InvokeCommand('.frontend-editing-toggle-link', 'removeClass', ['frontend-editing-toggle-not-configured'])); return $response; } diff --git a/src/Controller/PreviewController.php b/src/Controller/PreviewController.php index 41977a855132a91fd7e9ba70c648cf00f0feaa66..02423c42c2fe7a9d0404923d54744a300fa03b69 100644 --- a/src/Controller/PreviewController.php +++ b/src/Controller/PreviewController.php @@ -34,7 +34,7 @@ class PreviewController extends PreviewControllerBase { /** * {@inheritdoc} */ - public function view(EntityInterface $entity_preview, $view_mode_id = 'default', $langcode = NULL, Request $request = NULL) { + public function view(EntityInterface $entity_preview, $view_mode_id = 'default', $langcode = NULL, ?Request $request = NULL) { $build = parent::view($entity_preview, $view_mode_id, $langcode); // In case it is ajax request, respond with ajax response. if ($request->isXmlHttpRequest()) { @@ -52,7 +52,7 @@ class PreviewController extends PreviewControllerBase { /** * {@inheritdoc} */ - public function nodeView(EntityInterface $node_preview, $view_mode_id = 'full', $langcode = NULL, Request $request = NULL) { + public function nodeView(EntityInterface $node_preview, $view_mode_id = 'full', $langcode = NULL, ?Request $request = NULL) { $build = parent::view($node_preview, $view_mode_id, $langcode); // In case it is ajax request, respond with ajax response. if ($request->isXmlHttpRequest()) { diff --git a/src/Form/EntityReferenceAddForm.php b/src/Form/EntityReferenceAddForm.php index 8a12b26d0829d3dff3404f75c6d3a008cbdeef09..ea7cbf208ab0c338113da9279fe0559f17f6d2d6 100644 --- a/src/Form/EntityReferenceAddForm.php +++ b/src/Form/EntityReferenceAddForm.php @@ -48,7 +48,7 @@ class EntityReferenceAddForm extends FormBase { /** * {@inheritdoc} */ - public function buildForm(array $form, FormStateInterface $form_state, FieldDefinitionInterface $field_definition = NULL) { + public function buildForm(array $form, FormStateInterface $form_state, ?FieldDefinitionInterface $field_definition = NULL) { if (!empty($field_definition)) { $settings = $field_definition->getSettings(); if ($settings['target_type'] == 'media' && $this->moduleHandler->moduleExists('media_library_form_element')) { diff --git a/src/Hook/FrontendEditingHooks.php b/src/Hook/FrontendEditingHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..c9915594a7c8924184d9f57f5bca263afa6ea21b --- /dev/null +++ b/src/Hook/FrontendEditingHooks.php @@ -0,0 +1,96 @@ +<?php + +namespace Drupal\frontend_editing\Hook; + +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Render\ElementInfoManager; +use Drupal\Core\Routing\AdminContext; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\frontend_editing\ToolbarItem; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +/** + * Hook implementations for frontend_editing. + */ +class FrontendEditingHooks { + + /** + * Constructs FrontendEditing hooks. + * + * @param \Drupal\Core\Session\AccountProxyInterface $currentUser + * The current user. + * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory + * The config factory object. + * @param \Drupal\Core\Routing\AdminContext $adminContext + * The admin context. + * @param \Drupal\Core\Render\ElementInfoManager $elementInfoManager + * The element info manager. + */ + public function __construct( + #[Autowire(service: 'current_user')] + protected AccountProxyInterface $currentUser, + #[Autowire(service: 'config.factory')] + protected ConfigFactoryInterface $configFactory, + #[Autowire(service: 'router.admin_context')] + protected AdminContext $adminContext, + #[Autowire(service: 'plugin.manager.element_info')] + protected ElementInfoManager $elementInfoManager, + ) {} + + /** + * Implements hook_toolbar(). + */ + #[Hook('toolbar')] + public function toolbar() { + $items = []; + $cache_metadata = new CacheableMetadata(); + $cache_metadata->addCacheContexts(['user.permissions']); + if (!$this->currentUser->hasPermission('access frontend editing')) { + $cache_metadata->applyTo($items); + return $items; + } + + $config = $this->configFactory->get('frontend_editing.settings'); + $cache_metadata->addCacheableDependency($config); + $cache_metadata->addCacheContexts(['route']); + $ui_toggle_enabled = $config->get('ui_toggle'); + if ($ui_toggle_enabled || $this->adminContext->isAdminRoute()) { + $cache_metadata->applyTo($items); + return $items; + } + + $items['toggle'] = [ + '#type' => 'toolbar_item', + 'tab' => [ + '#lazy_builder' => [ + 'frontend_editing.toolbar_item:renderToggle', + [], + ], + '#create_placeholder' => TRUE, + '#cache' => [ + 'tags' => [ + 'frontend_editing:toggle', + ], + ], + ], + '#wrapper_attributes' => [ + 'class' => [ + 'frontend-editing-toolbar-toggle', + ], + ], + '#weight' => -11, + ]; + // \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an + // #attributes property to each toolbar item's tab child automatically. + // Lazy builders don't support an #attributes property so we need to + // add another render callback to remove the #attributes property. We start + // by adding the defaults, and then we append our own pre render callback. + $items['toggle'] += $this->elementInfoManager->getInfo('toolbar_item'); + $items['toggle']['#pre_render'][] = [ToolbarItem::class, 'removeTabAttributes']; + $cache_metadata->applyTo($items); + return $items; + } + +} diff --git a/src/ToolbarItem.php b/src/ToolbarItem.php new file mode 100644 index 0000000000000000000000000000000000000000..e9320ab6ca02c1e4d80b37a369cd870c5d607220 --- /dev/null +++ b/src/ToolbarItem.php @@ -0,0 +1,100 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\frontend_editing; + +use Drupal\Component\Utility\Html; +use Drupal\Core\Render\ElementInfoManagerInterface; +use Drupal\Core\Security\TrustedCallbackInterface; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Url; +use Drupal\user\UserDataInterface; + +/** + * Defines a class for lazy building render arrays. + * + * @internal + */ +final class ToolbarItem implements TrustedCallbackInterface { + + use StringTranslationTrait; + + /** + * Constructs LazyBuilders object. + * + * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfo + * Element info. + * @param \Drupal\user\UserDataInterface $userData + * The user data. + * @param \Drupal\Core\Session\AccountProxyInterface $currentUser + * The current user. + */ + public function __construct( + protected ElementInfoManagerInterface $elementInfo, + protected UserDataInterface $userData, + protected AccountProxyInterface $currentUser, + ) {} + + /** + * Render announcements. + * + * @return array + * Render array. + */ + public function renderToggle(): array { + $toggle_state = $this->userData->get( + 'frontend_editing', + $this->currentUser->id(), + 'enabled' + ); + $active_class = $toggle_state ? 'frontend-editing--enabled' : ''; + + $build = [ + '#type' => 'link', + '#url' => Url::fromRoute('frontend_editing.toggle'), + '#cache' => [ + 'context' => ['user.permissions'], + ], + '#title' => $toggle_state ? $this->t('On') : $this->t('Off'), + '#id' => Html::getId('frontend-editing-toolbar-toggle-link'), + '#attributes' => [ + 'title' => $this->t('Enable frontend editing actions'), + 'class' => [ + 'use-ajax', + 'frontend-editing-toggle-link', + $active_class, + ], + ], + '#attached' => [ + 'library' => [ + 'core/drupal.ajax', + 'frontend_editing/ui_toggle', + ], + ], + ]; + + // The renderer has already added element defaults by the time the lazy + // builder is run. + // @see https://www.drupal.org/project/drupal/issues/2609250 + $build += $this->elementInfo->getInfo('link'); + return $build; + } + + /** + * {@inheritdoc} + */ + public static function trustedCallbacks(): array { + return ['renderToggle', 'removeTabAttributes']; + } + + /** + * Render callback. + */ + public static function removeTabAttributes(array $element): array { + unset($element['tab']['#attributes']); + return $element; + } + +}