Commit 7b0c27c8 authored by Tymoteusz Motylewski's avatar Tymoteusz Motylewski Committed by Benni Mack
Browse files

[!!!][FEATURE] Refactor and streamline click menu / context menu

This change unifies the ClickMenu functionality of the pagetree (ExtJS)
with the ClickMenu code given in other areas of the TYPO3 Backend.

The following changes are made:
* Unify the naming, it's "ContextMenu" not "ClickMenu" anymore
* Configuration for record types are unified, the clickmenu shows
  the same entries in the same order in any place.
* ExtJS-based ContextMenu is removed, all based on the new
  ContextMenu functionality.
* A new way for extending the items inside the ContextMenu
  is handled via ItemProviders, which can easily be extended.
* Configuring clickmenu items is not done based on PageTS (as it
  was handled with the ExtJS pagetree), however certain items can
  be disabled via PageTS.

Resolves: #78192
Releases: master
Change-Id: I380ac73ced10fdc7b1fdec7261e2d56da3d7d938
Reviewed-on: https://review.typo3.org/50124


Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 42e986bf
......@@ -15,6 +15,7 @@ div#contentMenu1 {
min-width: 150px;
&-item {
cursor: pointer;
padding: 5px;
border-bottom-color: transparent;
border-top-color: transparent;
......
This diff is collapsed.
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Backend\ContextMenu;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class for generating the click menu
* @internal
*/
class ContextMenu
{
/**
* Click menu item providers shipped with EXT:backend
*
* @var array
*/
protected $itemProviders = [
ItemProviders\PageProvider::class,
ItemProviders\RecordProvider::class
];
/**
* @param string $table
* @param string $identifier
* @param string $context
* @return array
*/
public function getItems(string $table, string $identifier, string $context=''): array
{
$items = [];
$itemsProviders = $this->getAvailableProviders($table, $identifier, $context);
/** @var $provider \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface */
foreach ($itemsProviders as $provider) {
$items = $provider->addItems($items);
}
return $this->cleanItems($items);
}
/**
* @param string $table
* @param string $identifier
* @param string $context
* @return array of \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface
*/
protected function getAvailableProviders(string $table, string $identifier, string $context): array
{
$providers = $this->itemProviders;
if (is_array($GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders'])) {
$providers = array_merge($this->itemProviders, $GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders']);
}
$availableProviders = [];
foreach ($providers as $providerClass) {
$provider = GeneralUtility::makeInstance($providerClass, $table, $identifier, $context);
if ($provider->canHandle()) {
$priority = $provider->getPriority();
$availableProviders[$priority] = $provider;
}
}
krsort($availableProviders);
return $availableProviders;
}
/**
* Clean up double dividers.
* Don't render menu when there are no item or submenu.
*
* @param array $items
* @return array
*/
protected function cleanItems(array $items): array
{
$canRender = false;
$prevItemWasDivider = false;
foreach ($items as $key => $item) {
if ($item['type'] === 'item') {
$canRender = true;
$prevItemWasDivider = false;
continue;
}
if ($item['type'] === 'divider') {
if ($prevItemWasDivider === true) {
unset($items[$key]);
} else {
$prevItemWasDivider = true;
}
continue;
}
if ($item['type'] === 'submenu') {
$childItems = $this->cleanItems($item['childItems']);
if (empty($childItems)) {
unset($items[$key]);
} else {
$items[$key]['childItems'] = $childItems;
$canRender = true;
$prevItemWasDivider = false;
}
continue;
}
}
//Remove first and last divider
$fistItem = reset($items);
if ($fistItem['type'] === 'divider') {
$key = key($items);
unset($items[$key]);
}
$lastItem = end($items);
if ($lastItem['type'] === 'divider') {
$key = key($items);
unset($items[$key]);
}
//no menu when there are no item or submenu
if (!$canRender) {
$items = [];
}
return $items;
}
}
<?php
namespace TYPO3\CMS\Backend\ContextMenu;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Context Menu Action
*/
class ContextMenuAction
{
/**
* Label
*
* @var string
*/
protected $label = '';
/**
* Identifier
*
* @var string
*/
protected $id = '';
/**
* Icon
*
* @var string
*/
protected $icon = '';
/**
* Callback Action
*
* @var string
*/
protected $callbackAction = '';
/**
* Type
*
* @var string
*/
protected $type = '';
/**
* Child Action Collection
*
* @var ContextMenuActionCollection
*/
protected $childActions;
/**
* Custom Action Attributes
*
* @var array
*/
protected $customAttributes = [];
/**
* Returns the label
*
* @return string
*/
public function getLabel()
{
return $this->label;
}
/**
* Sets the label
*
* @param string $label
*/
public function setLabel($label)
{
$this->label = $label;
}
/**
* Returns the identifier
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Sets the identifier
*
* @param string $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* Returns the icon
*
* @return string
*/
public function getIcon()
{
return $this->icon;
}
/**
* Sets the icon
*
* @param string $icon
* @return void
*/
public function setIcon($icon)
{
$this->icon = $icon;
}
/**
* Returns the callback action
*
* @return string
*/
public function getCallbackAction()
{
return $this->callbackAction;
}
/**
* Sets the callback action
*
* @param string $callbackAction
*/
public function setCallbackAction($callbackAction)
{
$this->callbackAction = $callbackAction;
}
/**
* Returns the type
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Sets the type
*
* @param string $type
* @return void
*/
public function setType($type)
{
$this->type = $type;
}
/**
* Returns the child actions
*
* @return ContextMenuActionCollection
*/
public function getChildActions()
{
return $this->childActions;
}
/**
* Sets the child actions
*
* @param ContextMenuActionCollection $actions
* @return void
*/
public function setChildActions(ContextMenuActionCollection $actions)
{
$this->childActions = $actions;
}
/**
* Returns TRUE if the action has child actions
*
* @return bool
*/
public function hasChildActions()
{
return $this->childActions !== null;
}
/**
* Sets the custom attributes
*
* @param array $customAttributes
* @return void
*/
public function setCustomAttributes(array $customAttributes)
{
$this->customAttributes = $customAttributes;
}
/**
* Returns the custom attributes
*
* @return array
*/
public function getCustomAttributes()
{
return $this->customAttributes;
}
/**
* Returns the action as an array
*
* @return array
*/
public function toArray()
{
$arrayRepresentation = [
'label' => $this->getLabel(),
'id' => $this->getId(),
'icon' => $this->getIcon(),
'callbackAction' => $this->getCallbackAction(),
'type' => $this->getType(),
'customAttributes' => $this->getCustomAttributes()
];
$arrayRepresentation['childActions'] = '';
if ($this->hasChildActions()) {
$arrayRepresentation['childActions'] = $this->childActions->toArray();
}
return $arrayRepresentation;
}
}
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Lang\LanguageService;
/**
* Abstract provider is a base class for context menu item providers
*/
class AbstractProvider implements ProviderInterface
{
/**
* Language Service property. Used to access localized labels
*
* @var LanguageService
*/
protected $languageService;
/**
* @var BackendUserAuthentication
*/
protected $backendUser;
/**
* @var \TYPO3\CMS\Backend\Clipboard\Clipboard
*/
protected $clipboard;
/**
* Array of items the class is providing
*
* @var array
*/
protected $itemsConfiguration = [];
/**
* Click menu items disabled by TSConfig
*
* @var array
*/
protected $disabledItems = [];
/**
* Current table name
*
* @var string
*/
protected $table = '';
/**
* @var string clicked record identifier (usually uid or file combined identifier)
*/
protected $identifier = '';
/**
* Context - from where the click menu was triggered (e.g. 'tree')
*
* @var string
*/
protected $context = '';
/**
* Lightweight constructor, just to be able to call ->canHandle(). Rest of the initialization is done
* in the initialize() method
*
* @param string $table
* @param string $identifier
* @param string $context
*/
public function __construct(string $table, string $identifier, string $context='')
{
$this->table = $table;
$this->identifier = $identifier;
$this->context = $context;
$this->languageService = $GLOBALS['LANG'];
$this->backendUser = $GLOBALS['BE_USER'];
}
/**
* Provider initialization, heavy stuff
*/
protected function initialize()
{
$this->initClipboard();
$this->initDisabledItems();
}
/**
* Returns the provider priority which is used for determining the order in which providers are adding items
* to the result array. Highest priority means provider is evaluated first.
*
* @return int
*/
public function getPriority(): int
{
return 100;
}
/**
* Whether this provider can handle given request (usually a check based on table, uid and context)
*
* @return bool
*/
public function canHandle(): bool
{
return false;
}
/**
* Initialize clipboard object - necessary for all copy/cut/paste operations
*/
protected function initClipboard()
{
$clipboard = GeneralUtility::makeInstance(Clipboard::class);
$clipboard->initializeClipboard();
// This locks the clipboard to the Normal for this request.
$clipboard->lockToNormal();
$this->clipboard = $clipboard;
}
/**
* Fills $this->disabledItems with the values from TSConfig.
* Disabled items can be set separately for each context.
*/
protected function initDisabledItems()
{
$TSkey = $this->table . ($this->context ? '.' . $this->context : '');
$this->disabledItems = GeneralUtility::trimExplode(',', $this->backendUser->getTSConfigVal('options.contextMenu.table.' . $TSkey . '.disableItems'), true);
}
/**
* Adds new items to the given array or modifies existing items
*
* @param array $items
* @return array
*/
public function addItems(array $items): array
{
$this->initialize();
$items += $this->prepareItems($this->itemsConfiguration);
return $items;
}
/**
* Converts item configuration (from $this->itemsConfiguration) into an array ready for returning by controller
*
* @param array $itemsConfiguration
* @return array
*/
protected function prepareItems(array $itemsConfiguration): array
{
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$items = [];
foreach ($itemsConfiguration as $name => $configuration) {
$type = !empty($configuration['type']) ? $configuration['type'] : 'item';
if ($this->canRender($name, $type)) {
$items[$name] = [
'type' => $type,
'label' => !empty($configuration['label']) ? htmlspecialchars($this->languageService->sL($configuration['label'])) : '',
'icon' => !empty($configuration['iconIdentifier']) ? $iconFactory->getIcon($configuration['iconIdentifier'], Icon::SIZE_SMALL)->render() : '',
'additionalAttributes' => $this->getAdditionalAttributes($name),
'callbackAction' => !empty($configuration['callbackAction']) ? $configuration['callbackAction'] : ''
];
if ($type === 'submenu') {
$items[$name]['childItems'] = $this->prepareItems($configuration['childItems']);
}
}
}
return $items;
}
/**
* Returns an array of additional attributes for given item. Additional attributes are used to pass item specific data
* to the JS. E.g. message for the delete confirmation dialog
*
* @param string $itemName
* @return array
*/
protected function getAdditionalAttributes(string $itemName): array
{
return [];
}
/**
* Checks whether certain item can be rendered (e.g. check for disabled items or permissions)
*
* @param string $itemName