toolbar_plus-1.0.x-dev/src/ToolbarPlusUi.php
src/ToolbarPlusUi.php
<?php
declare(strict_types=1);
namespace Drupal\toolbar_plus;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\AdminContext;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Extension\ExtensionPathResolver;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\toolbar_plus\Event\ShouldNotEditModeEvent;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Toolbar+ UI.
*/
final class ToolbarPlusUi {
use StringTranslationTrait;
public function __construct(
private readonly RequestStack $requestStack,
private readonly RouteMatchInterface $routeMatch,
private readonly AdminContext $routerAdminContext,
private readonly ToolPluginManager $toolbarManager,
private readonly AccountProxyInterface $currentUser,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly ExtensionPathResolver $extensionPathResolver,
private readonly EntityDisplayRepositoryInterface $entityDisplayRepository,
) {}
/**
* Add toolbar.
*
* Adds an Edit mode toolbar to the navigation module's left sidebar.
*
* @param array $variables
* The navigation sidebar.
*
* @return void
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function buildToolbar(&$variables) {
if ($this->shouldNotEditMode()) {
return;
}
// Determine what the entity is being edited. This is okay since
// shouldNotEditMode ensures that we only have one entity parameter.
$parameters = $this->routeMatch->getParameters()->all();
foreach ($parameters as $entity) {
if ($entity instanceof EntityInterface) {
break;
}
}
// Hide the navigation items and reveal the Edit mode toolbar based on the
// the state.
$state = $this->getEditModeState();
$variables['#cache']['contexts'][] = 'cookies:editMode';
$variables['#cache']['contexts'][] = 'user.permissions';
$toolbar_classes = ['toolbar-block'];
if ($state === 'enabled') {
foreach ($variables['content']['content'] as &$navigation_item) {
$navigation_item['#attributes']['class'][] = 'toolbar-plus-hidden';
}
} else {
$toolbar_classes[] = 'toolbar-plus-hidden';
}
// Add a button to toggle into editing mode.
$variables['#attached']['library'][] = 'toolbar_plus/toolbar_plus';
$children = Element::children($variables['content']['footer']);
$last_key = end($children);
$variables['content']['footer'][$last_key]['content']['toolbar_plus_toggle_edit_mode'] = [
'#type' => 'inline_template',
'#template' => "<a id='toggle-edit-mode' data-drupal-tooltip='{{edit_mode}}' data-drupal-tooltip-class='admin-toolbar__tooltip' class='toolbar-button toolbar-button--collapsible{{toolbar_state}}' data-index-text='0' data-icon-text='Ed' href='javascript:void(0)'><span class='toolbar-button__label'>{{label}}</span></a>",
'#context' => [
'label' => t('Edit'),
'edit_mode' => t('Edit mode'),
'toolbar_state' => $state === 'enabled' ? ' active' : '',
],
];
// Add tool buttons.
foreach ($this->getToolPlugins($entity) as $id => $tool) {
$tool->addAttachments($variables['#attached']);
// Add styles for the plugin buttons and cursors.
$icons = $tool->getIconsPath();
$style = "<style>";
if (!empty($icons['mouse_icon'])) {
$style .= ".$id,\n.$id a {\n cursor: url('{$icons['mouse_icon']}') 0 2, auto;\n}\n";
}
if (!empty($icons['toolbar_button_icons'])) {
foreach ($icons['toolbar_button_icons'] as $name => $path) {
$style .= ".toolbar-button--icon--$name {\n --icon: url('$path');\n}\n";
}
}
$style .= "</style>";
// Add a button for the tool.
$tools[$id] = [
'#wrapper_attributes' => [
'class' => ['toolbar-block__list-item'],
],
'#type' => 'inline_template',
'#template' => "<a href='javascript:void(0)' data-tool='{{id}}' class='toolbar-button toolbar-button--collapsible toolbar-plus-button toolbar-button--icon--{{id}}'><span class='toolbar-button__label'>{{label}}</span></a>$style",
'#context' => [
'id' => $id,
'label' => $tool->label(),
],
];
}
// Add the toolbar.
$variables['content']['content']['toolbar_plus'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'toolbar-plus',
'class' => $toolbar_classes,
],
// Match the navigation module markup.
'admin_toolbar_item' => [
'#type' => 'container',
'#attributes' => [
'class' => ['admin-toolbar__item'],
],
'title' => [
'#type' => 'html_tag',
'#tag' => 'h4',
'#value' => t('Editing Toolbar'),
'#attributes' => [
'class' => ['visually-hidden', 'focusable'],
],
],
'tools' => [
'#theme' => 'item_list',
'#attributes' => [
'class' => ['toolbar-block__list'],
],
'#items' => $tools,
],
],
'#weight' => 99,
];
// Set the initial mode and active tool when visiting a page for the first time.
$entity_type = \Drupal::entityTypeManager()->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle());
$initial_mode = $entity_type->getThirdPartySetting('toolbar_plus', 'initial_mode', NULL);
$variables['content']['content']['toolbar_plus']['#attached']['drupalSettings']['toolbarPlus']['initialMode'] = $initial_mode;
if ($initial_mode === 'edit_mode') {
$default_tool_default = \Drupal::moduleHandler()->moduleExists('edit_plus') ? 'edit_plus' : NULL;
$default_tool = $entity_type->getThirdPartySetting('toolbar_plus', 'default_tool', $default_tool_default);
if ($default_tool) {
$variables['content']['content']['toolbar_plus']['#attached']['drupalSettings']['toolbarPlus']['defaultTool'] = $default_tool;
}
}
}
/**
* Add a top bar.
*
* @param array $page_top
* The page top render array.
*
* @return void
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function buildTopBar(&$page_top) {
if ($this->shouldNotEditMode()) {
return;
}
$tool_top_bars = [];
// Add tool specific buttons.
foreach ($this->getToolPlugins() as $id => $tool) {
$top_bar = $tool->buildTopBar();
if (!empty($top_bar)) {
$tool_top_bars[$id] = [
'#type' => 'container',
'#attributes' => [
'id' => "$id-top-bar",
'class' => ['top-bar__content', 'top-bar__left'],
],
'top_bar' => $top_bar,
];
if ($this->getActiveTool() !== $id) {
$tool_top_bars[$id]['#attributes']['class'][] = 'toolbar-plus-hidden';
}
}
}
// Add some global buttons that work with all tools.
$tool_top_bars['toolbar_plus'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'toolbar_plus-top-bar',
'class' => ['top-bar__content', 'top-bar__right'],
],
'refresh' => [
'#type' => 'container',
'#attributes' => [
'id' => 'toolbar-plus-refresh',
'title' => t('Refresh the editing UI'),
'class' => ['toolbar-button', 'toolbar-button--collapsible', 'toolbar-button--icon--refresh'],
],
'label' => [
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => $this->t('Refresh'),
'#attributes' => [
'class' => ['toolbar-button__label'],
],
],
],
'save' => [
'#type' => 'container',
'#attributes' => [
'id' => 'toolbar-plus-save',
'title' => t('Commit the tempstore changes'),
'class' => ['toolbar-button', 'toolbar-button--collapsible', 'toolbar-button--icon--save'],
],
'label' => [
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => $this->t('Save'),
'#attributes' => [
'class' => ['toolbar-button__label'],
],
],
],
'discard-changes' => [
'#type' => 'container',
'#attributes' => [
'id' => 'toolbar-plus-discard-changes',
'title' => t('Discard the tempstore changes'),
'class' => ['toolbar-button', 'toolbar-button--collapsible', 'toolbar-button--icon--discard-changes'],
],
'label' => [
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => $this->t('Discard changes'),
'#attributes' => [
'class' => ['toolbar-button__label'],
],
],
],
];
// Add the top bar.
$page_top['toolbar_plus_top_bar'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'toolbar-plus-top-bar',
'class' => ['top-bar'],
'data-drupal-admin-styles' => '',
],
'#cache' => ['contexts' => ['user.permissions', 'cookies:editMode']],
'tools' => $tool_top_bars,
];
// Hide the top bar when not in Editing mode.
if ($this->getEditModeState() === 'disabled') {
$page_top['toolbar_plus_top_bar']['#attributes']['class'][] = 'toolbar-plus-hidden';
}
}
/**
* Build side bars.
*
* @param array $page_top
* The page_top render array.
*
* @return void
*/
public function buildSideBars(&$page_top) {
if ($this->shouldNotEditMode()) {
return;
}
// Collect tool sidebars.
$tool_left_sidebars = [];
$tool_right_sidebars = [];
foreach ($this->getToolPlugins() as $id => $tool) {
$left_side_bar = $tool->buildLeftSideBar();
if (!empty($left_side_bar)) {
$tool_left_sidebars[$id] = [
'#type' => 'container',
'#attributes' => [
'id' => "$id-left-sidebar",
],
'left_side_bar' => $left_side_bar,
];
if ($this->getActiveTool() !== $id) {
$tool_left_sidebars[$id]['#attributes']['class'][] = 'toolbar-plus-hidden';
}
}
$right_side_bar = $tool->buildRightSideBar();
if (!empty($right_side_bar)) {
$tool_right_sidebars[$id] = [
'#type' => 'container',
'#attributes' => [
'id' => "$id-right-sidebar",
],
'right_side_bar' => $right_side_bar,
];
if ($this->getActiveTool() !== $id) {
$tool_right_sidebars[$id]['#attributes']['class'][] = 'toolbar-plus-hidden';
}
}
}
// Add the sidebars wrapper.
$page_top['toolbar_plus_left_sidebar'] = [
'#type' => 'html_tag',
'#tag' => 'aside',
'#attributes' => [
'id' => 'toolbar-plus-left-sidebar',
'class' => ['toolbar-plus-sidebar-wrapper'],
],
'#cache' => ['contexts' => ['user.permissions', 'cookies:editMode']],
'left_sidebars' => $tool_left_sidebars,
];
$page_top['toolbar_plus_right_sidebar'] = [
'#type' => 'html_tag',
'#tag' => 'aside',
'#attributes' => [
'id' => 'toolbar-plus-right-sidebar',
'class' => ['toolbar-plus-sidebar-wrapper'],
],
'#cache' => ['contexts' => ['user.permissions', 'cookies:editMode']],
'right_sidebars' => $tool_right_sidebars,
];
// Hide the sidebar when not in Editing mode.
if ($this->getEditModeState() === 'disabled') {
$page_top['toolbar_plus_sidebar']['#attributes']['class'][] = 'toolbar-plus-hidden';
}
}
public function preprocessTopBar(&$variables) {
if ($this->shouldNotEditMode()) {
return;
}
// Hide the navigation top bar when in Editing mode.
$variables['#cache']['contexts'][] = 'cookies:editMode';
if ($this->getEditModeState() === 'enabled') {
$variables['attributes']['class'][] = 'toolbar-plus-hidden';
}
}
/**
* Get tool plugins.
*
* @param \Drupal\Core\Entity\EntityInterface|NULL $entity
* If an entity is provided it will check if the tool plugin applies to this
* entity.
*
* @return array
* An array of tool plugins.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function getToolPlugins(EntityInterface $entity = NULL): array {
$tool_definitions = $this->toolbarManager->getDefinitions();
$tools = [];
if (!empty($tool_definitions)) {
foreach ($tool_definitions as $tool_definition) {
$tool = $this->toolbarManager->createInstance($tool_definition['id']);
if (is_null($entity) || ($entity && $tool->applies($entity))) {
$tools[$tool_definition['id']] = $tool;
}
}
}
return $tools;
}
/**
* Should not edit mode.
*
* @return bool
* Whether edit mode should be available on this page.
*/
public function shouldNotEditMode(): bool {
// @todo Only run once.
return $this->eventDispatcher->dispatch(new ShouldNotEditModeEvent(), ShouldNotEditModeEvent::class)->shouldNotEdit();
}
/**
* Get edit mode state.
*
* Many UI state items are stored in JS's Local and Session storage. Edit mode
* is stored as a cookie so that the server can conditionally render the page
* elements with attributes used for the editing UI. Rendering this server side
* prevents a flashing that would occur if we waited till the JS was loaded
* to enable the editing UI.
*
* @return string
* Whether edit mode is enabled (Whether the toolbar is open or closed).
*/
public function getEditModeState(): string {
// Cookies are for pages like /node/10
return $this->requestStack->getCurrentRequest()->cookies->get('editMode') ??
// Query parameters are for pages like /lb-plus/place-block/overrides/node.16
$this->requestStack->getCurrentRequest()->get('editMode') ??
'disabled';
}
/**
* Get active tool.
*
* @return string
* The active tool.
*/
public function getActiveTool(): string {
return $this->requestStack->getCurrentRequest()->cookies->get('activeTool') ?? 'pointer';
}
/**
* Is valid view mode.
*
* @return bool
* Whether the view_mode is a valid view mode.
*/
public function isValidViewMode(EntityInterface $entity, string $view_mode): bool {
$valid_view_modes = $this->entityDisplayRepository->getViewModes($entity->getEntityTypeId());
$valid_view_modes['default'] = TRUE;
return !empty($valid_view_modes[$view_mode]);
}
}
