[!!!][FEATURE] Refactor and streamline click menu / context menu 24/50124/73
authorTymoteusz Motylewski <t.motylewski@gmail.com>
Mon, 23 Jan 2017 13:49:26 +0000 (14:49 +0100)
committerBenni Mack <benni@typo3.org>
Tue, 7 Feb 2017 17:32:18 +0000 (18:32 +0100)
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 <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
91 files changed:
Build/Resources/Public/Less/TYPO3/_element_contextmenu.less
typo3/sysext/backend/Classes/ClickMenu/ClickMenu.php [deleted file]
typo3/sysext/backend/Classes/ContextMenu/ContextMenu.php [new file with mode: 0644]
typo3/sysext/backend/Classes/ContextMenu/ContextMenuAction.php [deleted file]
typo3/sysext/backend/Classes/ContextMenu/ContextMenuActionCollection.php [deleted file]
typo3/sysext/backend/Classes/ContextMenu/ItemProviders/AbstractProvider.php [new file with mode: 0644]
typo3/sysext/backend/Classes/ContextMenu/ItemProviders/PageProvider.php [new file with mode: 0644]
typo3/sysext/backend/Classes/ContextMenu/ItemProviders/ProviderInterface.php [new file with mode: 0644]
typo3/sysext/backend/Classes/ContextMenu/ItemProviders/RecordProvider.php [new file with mode: 0644]
typo3/sysext/backend/Classes/ContextMenu/Pagetree/ContextMenuDataProvider.php [deleted file]
typo3/sysext/backend/Classes/ContextMenu/Pagetree/Extdirect/ContextMenuConfiguration.php [deleted file]
typo3/sysext/backend/Classes/Controller/BackendController.php
typo3/sysext/backend/Classes/Controller/ClickMenuController.php [deleted file]
typo3/sysext/backend/Classes/Controller/ContentElement/NewContentElementController.php
typo3/sysext/backend/Classes/Controller/ContextMenuController.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Controller/EditDocumentController.php
typo3/sysext/backend/Classes/Controller/File/CreateFolderController.php
typo3/sysext/backend/Classes/Controller/File/FileUploadController.php
typo3/sysext/backend/Classes/Controller/File/RenameFileController.php
typo3/sysext/backend/Classes/Controller/File/ReplaceFileController.php
typo3/sysext/backend/Classes/Controller/FileSystemNavigationFrameController.php
typo3/sysext/backend/Classes/Controller/NewRecordController.php
typo3/sysext/backend/Classes/Controller/PageLayoutController.php
typo3/sysext/backend/Classes/Form/Container/OuterWrapContainer.php
typo3/sysext/backend/Classes/Template/Components/MetaInformation.php
typo3/sysext/backend/Classes/Template/DocumentTemplate.php
typo3/sysext/backend/Classes/Tree/View/FolderTreeView.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/backend/Classes/View/PageLayoutView.php
typo3/sysext/backend/Classes/View/PageTreeView.php
typo3/sysext/backend/Configuration/Backend/AjaxRoutes.php
typo3/sysext/backend/Resources/Public/Css/backend.css
typo3/sysext/backend/Resources/Public/JavaScript/ClickMenu.js [deleted file]
typo3/sysext/backend/Resources/Public/JavaScript/ContextMenu.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/ContextMenuActions.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/DragUploader.js
typo3/sysext/backend/Resources/Public/JavaScript/LegacyTree.js
typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/app.js
typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/contextmenu.js [deleted file]
typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/loadorder.txt
typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/nodeui.js
typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/tree.js
typo3/sysext/beuser/Classes/ContextMenu/ItemProvider.php [new file with mode: 0644]
typo3/sysext/beuser/Classes/Hook/BackendControllerHook.php [new file with mode: 0644]
typo3/sysext/beuser/Resources/Private/Layouts/Default.html
typo3/sysext/beuser/Resources/Private/Partials/BackendUser/IndexListRow.html
typo3/sysext/beuser/Resources/Private/Partials/BackendUser/OnlineListRow.html
typo3/sysext/beuser/Resources/Private/Partials/BackendUserGroup/IndexListRow.html
typo3/sysext/beuser/Resources/Private/Templates/BackendUser/Compare.html
typo3/sysext/beuser/Resources/Private/Templates/BackendUser/Index.html
typo3/sysext/beuser/Resources/Public/JavaScript/ContextMenuActions.js [new file with mode: 0644]
typo3/sysext/beuser/ext_localconf.php
typo3/sysext/beuser/ext_tables.php
typo3/sysext/core/Classes/Core/Bootstrap.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Documentation/Changelog/master/Breaking-78192-RefactorClickMenuContextMenu.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-78192-RefactorClickMenuContextMenu.rst [new file with mode: 0644]
typo3/sysext/core/Resources/Public/JavaScript/Contrib/extjs/ext-all-debug.js
typo3/sysext/core/Resources/Public/JavaScript/Contrib/extjs/ext-all.js
typo3/sysext/filelist/Classes/ContextMenu/ItemProviders/FileDragProvider.php [new file with mode: 0644]
typo3/sysext/filelist/Classes/ContextMenu/ItemProviders/FileProvider.php [new file with mode: 0644]
typo3/sysext/filelist/Classes/ContextMenu/ItemProviders/FileStorageProvider.php [new file with mode: 0644]
typo3/sysext/filelist/Classes/ContextMenu/ItemProviders/FilemountsProvider.php [new file with mode: 0644]
typo3/sysext/filelist/Classes/Controller/FileListController.php
typo3/sysext/filelist/Classes/FileList.php
typo3/sysext/filelist/Classes/Hook/BackendControllerHook.php [new file with mode: 0644]
typo3/sysext/filelist/Classes/ViewHelpers/Link/ClickMenuOnIconViewHelper.php
typo3/sysext/filelist/Resources/Public/JavaScript/ContextMenuActions.js [new file with mode: 0644]
typo3/sysext/filelist/ext_localconf.php [new file with mode: 0644]
typo3/sysext/filelist/ext_tables.php
typo3/sysext/fluid/Classes/ViewHelpers/Be/ContainerViewHelper.php
typo3/sysext/fluid/Classes/ViewHelpers/Be/PageInfoViewHelper.php
typo3/sysext/func/Classes/Controller/PageFunctionsController.php
typo3/sysext/impexp/Classes/Clickmenu.php [deleted file]
typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php [new file with mode: 0644]
typo3/sysext/impexp/Classes/Controller/ImportExportController.php
typo3/sysext/impexp/Classes/Hook/ContextMenuDisableItemsHook.php [deleted file]
typo3/sysext/impexp/Resources/Public/JavaScript/ContextMenuActions.js [new file with mode: 0644]
typo3/sysext/impexp/ext_tables.php
typo3/sysext/info/Classes/Controller/InfoModuleController.php
typo3/sysext/lang/Resources/Private/Language/locallang_misc.xlf
typo3/sysext/recordlist/Classes/RecordList.php
typo3/sysext/sys_action/Classes/ActionTask.php
typo3/sysext/sys_note/Resources/Private/Templates/Note/List.html
typo3/sysext/tstemplate/Classes/Controller/TypoScriptTemplateModuleController.php
typo3/sysext/tstemplate/Resources/Private/Templates/InformationModule.html
typo3/sysext/version/Classes/ClickMenu/VersionClickMenu.php [deleted file]
typo3/sysext/version/Classes/ContextMenu/ItemProvider.php [new file with mode: 0644]
typo3/sysext/version/Classes/Controller/VersionModuleController.php
typo3/sysext/version/Resources/Public/JavaScript/ContextMenuActions.js [new file with mode: 0644]
typo3/sysext/version/ext_tables.php

index da7330d..5d75112 100644 (file)
@@ -15,6 +15,7 @@ div#contentMenu1 {
                min-width: 150px;
 
                &-item {
+                       cursor: pointer;
                        padding: 5px;
                        border-bottom-color: transparent;
                        border-top-color: transparent;
diff --git a/typo3/sysext/backend/Classes/ClickMenu/ClickMenu.php b/typo3/sysext/backend/Classes/ClickMenu/ClickMenu.php
deleted file mode 100644 (file)
index 440197e..0000000
+++ /dev/null
@@ -1,1609 +0,0 @@
-<?php
-namespace TYPO3\CMS\Backend\ClickMenu;
-
-/*
- * 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\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\Imaging\Icon;
-use TYPO3\CMS\Core\Imaging\IconFactory;
-use TYPO3\CMS\Core\Resource\Folder;
-use TYPO3\CMS\Core\Resource\ResourceFactory;
-use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
-use TYPO3\CMS\Core\Type\Bitmask\Permission;
-use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Lang\LanguageService;
-
-/**
- * Class for generating the click menu
- * @internal
- */
-class ClickMenu
-{
-    /**
-     * Defines if the click menu is first level or second.
-     * Second means the click menu is triggered from another menu.
-     *
-     * @var int
-     */
-    public $cmLevel = 0;
-
-    /**
-     * Clipboard array (submitted by eg. pressing the paste button)
-     *
-     * @var bool
-     */
-    public $CB;
-
-    /**
-     * If set, the calling document should be in the listframe of a frameset.
-     *
-     * @var bool
-     */
-    public $listFrame = false;
-
-    /**
-     * If set, the menu is about database records, not files. (set if part 2 [1] of the item-var is NOT blank)
-     *
-     * @var bool
-     */
-    public $isDBmenu = false;
-
-    /**
-     * Stores the parts of the input $item string, splitted by "|":
-     * [0] = table/file, [1] = uid/blank, [2] = flag: If set, listFrame,
-     * If "2" then "content frame" is forced  [3] = ("+" prefix = disable
-     * all by default, enable these. Default is to disable) Items key list
-     *
-     * @var array
-     */
-    public $iParts = [];
-
-    /**
-     * Contains list of keywords of items to disable in the menu
-     *
-     * @var array
-     */
-    public $disabledItems = [];
-
-    /**
-     * Array of classes to be used for user processing of the menu content.
-     * This is for the API of adding items to the menu from outside.
-     *
-     * @var array
-     */
-    public $extClassArray = [];
-
-    /**
-     * Set, when edit icon is drawn.
-     *
-     * @var bool
-     */
-    public $editPageIconSet = false;
-
-    /**
-     * Set to TRUE, if editing of the element is OK.
-     *
-     * @var bool
-     */
-    public $editOK = false;
-
-    /**
-     * The current record
-     *
-     * @var array
-     */
-    public $rec = [];
-
-    /**
-     * Clipboard set from the outside
-     * Declared as public for now, should become protected
-     * soon-ish
-     * @var Clipboard;
-     */
-    public $clipObj;
-
-    /**
-     * The current page record
-     * @var array
-     */
-    protected $pageinfo;
-
-    /**
-     * Language Service property. Used to access localized labels
-     *
-     * @var LanguageService
-     */
-    protected $languageService;
-
-    /**
-     * @var BackendUserAuthentication
-     */
-    protected $backendUser;
-
-    /**
-     * @var IconFactory
-     */
-    protected $iconFactory;
-
-    /**
-     * @param LanguageService $languageService Language Service to inject
-     * @param BackendUserAuthentication $backendUser
-     */
-    public function __construct(LanguageService $languageService = null, BackendUserAuthentication $backendUser = null)
-    {
-        $this->languageService = $languageService ?: $GLOBALS['LANG'];
-        $this->backendUser = $backendUser ?: $GLOBALS['BE_USER'];
-        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-    }
-
-    /**
-     * Initialize click menu
-     *
-     * @return array the array to be returned as JSON including all click menu items
-     */
-    public function init()
-    {
-        $CMcontent = '';
-        // Setting GPvars:
-        $this->cmLevel = (int)GeneralUtility::_GP('cmLevel');
-        $this->CB = GeneralUtility::_GP('CB');
-
-        // Deal with Drag&Drop context menus
-        if ((string)GeneralUtility::_GP('dragDrop') !== '') {
-            return $this->printDragDropClickMenu(GeneralUtility::_GP('dragDrop'), GeneralUtility::_GP('srcId'), GeneralUtility::_GP('dstId'));
-        }
-        // Can be set differently as well
-        $this->iParts[0] = GeneralUtility::_GP('table');
-        $this->iParts[1] = GeneralUtility::_GP('uid');
-        $this->iParts[2] = GeneralUtility::_GP('listFr');
-        $this->iParts[3] = GeneralUtility::_GP('enDisItems');
-        // Setting flags:
-        if ($this->iParts[2]) {
-            $this->listFrame = true;
-        }
-        if (isset($this->iParts[1]) && $this->iParts[1] !== '') {
-            $this->isDBmenu = true;
-        }
-        $TSkey = ($this->isDBmenu ? 'page' : 'folder') . ($this->listFrame ? 'List' : 'Tree');
-        $this->disabledItems = GeneralUtility::trimExplode(',', $this->backendUser->getTSConfigVal('options.contextMenu.' . $TSkey . '.disableItems'), true);
-        // &cmLevel flag detected (2nd level menu)
-        if (!$this->cmLevel) {
-            // Make 1st level clickmenu:
-            if ($this->isDBmenu) {
-                $CMcontent = $this->printDBClickMenu($this->iParts[0], $this->iParts[1]);
-            } else {
-                $CMcontent = $this->printFileClickMenu($this->iParts[0]);
-            }
-        } else {
-            // Make 2nd level clickmenu (only for DBmenus)
-            if ($this->isDBmenu) {
-                $CMcontent = $this->printNewDBLevel($this->iParts[0], $this->iParts[1]);
-            }
-        }
-        // Return clickmenu content:
-        return $CMcontent;
-    }
-
-    /***************************************
-     *
-     * DATABASE
-     *
-     ***************************************/
-    /**
-     * Make 1st level clickmenu:
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @return array the array to be returned as JSON
-     */
-    public function printDBClickMenu($table, $uid)
-    {
-        $uid = (int)$uid;
-        // Get record:
-        $this->rec = BackendUtility::getRecordWSOL($table, $uid);
-        $menuItems = [];
-        $root = 0;
-        $DBmount = false;
-        // Rootlevel
-        if ($table === 'pages' && $uid === 0) {
-            $root = 1;
-        }
-        // DB mount
-        if ($table === 'pages' && in_array($uid, $this->backendUser->returnWebmounts())) {
-            $DBmount = true;
-        }
-        // Used to hide cut,copy icons for l10n-records
-        $l10nOverlay = false;
-        // Should only be performed for overlay-records within the same table
-        if (BackendUtility::isTableLocalizable($table) && $table !== 'pages_language_overlay') {
-            $l10nOverlay = (int)$this->rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0;
-        }
-        // If record found (or root), go ahead and fill the $menuItems array which will contain data for the elements to render.
-        if (is_array($this->rec) || $root) {
-            // Get permissions
-            $lCP = $this->backendUser->calcPerms(BackendUtility::getRecord('pages', $table === 'pages' ? (int)$this->rec['uid'] : (int)$this->rec['pid']));
-            // View
-            if (!in_array('view', $this->disabledItems, true)) {
-                if ($table === 'pages') {
-                    $menuItems['view'] = $this->DB_view($uid);
-                }
-                if ($table === 'tt_content') {
-                    $ws_rec = BackendUtility::getRecordWSOL($table, (int)$this->rec['uid']);
-                    $menuItems['view'] = $this->DB_view((int)$ws_rec['pid']);
-                }
-            }
-            // Edit:
-            if (!$root && ($this->backendUser->isPSet($lCP, $table, 'edit') || $this->backendUser->isPSet($lCP, $table, 'editcontent'))) {
-                if (!in_array('edit', $this->disabledItems, true)) {
-                    $menuItems['edit'] = $this->DB_edit($table, $uid);
-                }
-                $this->editOK = true;
-            }
-            // New:
-            if (!in_array('new', $this->disabledItems, true) && $this->backendUser->isPSet($lCP, $table, 'new')) {
-                $menuItems['new'] = $this->DB_new($table, $uid);
-            }
-            // Info:
-            if (!in_array('info', $this->disabledItems, true) && !$root) {
-                $menuItems['info'] = $this->DB_info($table, $uid);
-            }
-            $menuItems['spacer1'] = 'spacer';
-            // Copy:
-            if (!in_array('copy', $this->disabledItems, true) && !$root && !$DBmount && !$l10nOverlay) {
-                $menuItems['copy'] = $this->DB_copycut($table, $uid, 'copy');
-            }
-            // Cut:
-            if (!in_array('cut', $this->disabledItems, true) && !$root && !$DBmount && !$l10nOverlay && $this->editOK) {
-                $menuItems['cut'] = $this->DB_copycut($table, $uid, 'cut');
-            }
-            // Paste:
-            $elFromAllTables = count($this->clipObj->elFromTable(''));
-            if (!in_array('paste', $this->disabledItems, true) && $elFromAllTables) {
-                $selItem = $this->clipObj->getSelectedRecord();
-                $elInfo = [
-                    GeneralUtility::fixed_lgd_cs($selItem['_RECORD_TITLE'], $this->backendUser->uc['titleLen']),
-                    $root ? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] : GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($table, $this->rec), $this->backendUser->uc['titleLen']),
-                    $this->clipObj->currentMode()
-                ];
-                if ($table === 'pages' && $lCP & Permission::PAGE_NEW) {
-                    if ($elFromAllTables) {
-                        $menuItems['pasteinto'] = $this->DB_paste('', $uid, 'into', $elInfo);
-                    }
-                }
-                $elFromTable = count($this->clipObj->elFromTable($table));
-                if (!$root && !$DBmount && $elFromTable && $GLOBALS['TCA'][$table]['ctrl']['sortby']) {
-                    $menuItems['pasteafter'] = $this->DB_paste($table, -$uid, 'after', $elInfo);
-                }
-            }
-
-            // Delete:
-            $elInfo = [GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($table, $this->rec), $this->backendUser->uc['titleLen'])];
-            $disableDeleteTS = $this->backendUser->getTSConfig('options.disableDelete');
-            $disableDelete = (bool) trim(isset($disableDeleteTS['properties'][$table]) ? $disableDeleteTS['properties'][$table] : $disableDeleteTS['value']);
-            if (!in_array('delete', $this->disabledItems, true) && !$root && !$DBmount && $this->backendUser->isPSet($lCP, $table, 'delete') && !$disableDelete) {
-                $menuItems['spacer2'] = 'spacer';
-                $menuItems['delete'] = $this->DB_delete($table, $uid, $elInfo);
-            }
-            if (!in_array('history', $this->disabledItems, true)) {
-                $menuItems['history'] = $this->DB_history($table, $uid);
-            }
-
-            $localItems = [];
-            if (!$this->cmLevel && !in_array('moreoptions', $this->disabledItems, true)) {
-                // Creating menu items here:
-                if ($this->editOK) {
-                    $localItems['spacer3'] = 'spacer';
-                    $localItems['moreoptions'] = $this->linkItem(
-                        $this->label('more') . '&nbsp;&nbsp;<span class="fa fa-caret-right"></span>',
-                        '<span class="fa fa-fw"></span>',
-                        'TYPO3.ClickMenu.fetch(' . GeneralUtility::quoteJSvalue(GeneralUtility::linkThisScript() . '&cmLevel=1&subname=moreoptions') . ');return false;',
-                        false,
-                        true
-                    );
-                    $menuItemHideUnhideAllowed = false;
-                    $hiddenField = '';
-                    // Check if column for disabled is defined
-                    if (isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
-                        $hiddenField = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
-                        if (
-                            $hiddenField !== '' && !empty($GLOBALS['TCA'][$table]['columns'][$hiddenField]['exclude'])
-                            && $this->backendUser->check('non_exclude_fields', $table . ':' . $hiddenField)
-                        ) {
-                            $menuItemHideUnhideAllowed = true;
-                        }
-                    }
-                    if ($menuItemHideUnhideAllowed && !in_array('hide', $this->disabledItems, true)) {
-                        $localItems['hide'] = $this->DB_hideUnhide($table, $this->rec, $hiddenField);
-                    }
-                    $anyEnableColumnsFieldAllowed = false;
-                    // Check if columns are defined
-                    if (isset($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'])) {
-                        $columnsToCheck = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
-                        if ($table === 'pages' && !empty($columnsToCheck)) {
-                            $columnsToCheck[] = 'extendToSubpages';
-                        }
-                        foreach ($columnsToCheck as $currentColumn) {
-                            if (
-                                !empty($GLOBALS['TCA'][$table]['columns'][$currentColumn]['exclude'])
-                                && $this->backendUser->check('non_exclude_fields', $table . ':' . $currentColumn)
-                            ) {
-                                $anyEnableColumnsFieldAllowed = true;
-                            }
-                        }
-                    }
-                    if ($anyEnableColumnsFieldAllowed && !in_array('edit_access', $this->disabledItems, true)) {
-                        $localItems['edit_access'] = $this->DB_editAccess($table, $uid);
-                    }
-                }
-                // Find delete element among the input menu items and insert the local items just before that:
-                $c = 0;
-                $deleteFound = false;
-                foreach ($menuItems as $key => $value) {
-                    $c++;
-                    if ($key === 'delete') {
-                        $deleteFound = true;
-                        break;
-                    }
-                }
-                if ($deleteFound) {
-                    // .. subtract two... (delete item + its spacer element...)
-                    $c -= 2;
-                    // and insert the items just before the delete element.
-                    array_splice($menuItems, $c, 0, $localItems);
-                } else {
-                    $menuItems = array_merge($menuItems, $localItems);
-                }
-            }
-        }
-        // Adding external elements to the menuItems array
-        $menuItems = $this->processingByExtClassArray($menuItems, $table, $uid);
-        // Processing by external functions?
-        $menuItems = $this->externalProcessingOfDBMenuItems($menuItems);
-        if (!is_array($this->rec)) {
-            $this->rec = [];
-        }
-
-        // Return the printed elements:
-        return $this->printItems($menuItems);
-    }
-
-    /**
-     * Make 2nd level clickmenu (only for DBmenus)
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @return array the array to be returned as JSON
-     */
-    public function printNewDBLevel($table, $uid)
-    {
-        $localItems = [];
-        $uid = (int)$uid;
-        // Setting internal record to the table/uid :
-        $this->rec = BackendUtility::getRecordWSOL($table, $uid);
-        $menuItems = [];
-        $root = 0;
-        // Rootlevel
-        if ($table === 'pages' && $uid === 0) {
-            $root = 1;
-        }
-        // If record was found, check permissions and get menu items.
-        if (is_array($this->rec) || $root) {
-            $lCP = $this->backendUser->calcPerms(BackendUtility::getRecord('pages', $table === 'pages' ? (int)$this->rec['uid'] : (int)$this->rec['pid']));
-            // Edit:
-            if (!$root && ($this->backendUser->isPSet($lCP, $table, 'edit') || $this->backendUser->isPSet($lCP, $table, 'editcontent'))) {
-                $this->editOK = true;
-            }
-            $menuItems = $this->processingByExtClassArray($menuItems, $table, $uid);
-        }
-
-        $subname = GeneralUtility::_GP('subname');
-        if ($subname === 'moreoptions') {
-            // If the page can be edited, then show this:
-            if ($this->editOK) {
-                if (($table === 'pages' || $table === 'tt_content') && !in_array('move_wizard', $this->disabledItems, true)) {
-                    $localItems['move_wizard'] = $this->DB_moveWizard($table, $uid, $this->rec);
-                }
-                if (($table === 'pages' || $table === 'tt_content') && !in_array('new_wizard', $this->disabledItems, true)) {
-                    $localItems['new_wizard'] = $this->DB_newWizard($table, $uid, $this->rec);
-                }
-                if ($table === 'pages' && !in_array('perms', $this->disabledItems, true) && $this->backendUser->check('modules', 'system_BeuserTxPermission')) {
-                    $localItems['perms'] = $this->DB_perms($table, $uid, $this->rec);
-                }
-                if (!in_array('db_list', $this->disabledItems, true) && $this->backendUser->check('modules', 'web_list')) {
-                    $localItems['db_list'] = $this->DB_db_list($table, $uid, $this->rec);
-                }
-            }
-            // Temporary mount point item:
-            if ($table === 'pages') {
-                $localItems['temp_mount_point'] = $this->DB_tempMountPoint($uid);
-            }
-            // Merge the locally created items into the current menu items passed to this function.
-            $menuItems = array_merge($menuItems, $localItems);
-        }
-
-        // Return the printed elements:
-        if (!is_array($menuItems)) {
-            $menuItems = [];
-        }
-        return $this->printItems($menuItems);
-    }
-
-    /**
-     * Processing the $menuItems array (for extension classes) (DATABASE RECORDS)
-     *
-     * @param array $menuItems Array for manipulation.
-     * @return array Processed $menuItems array
-     */
-    public function externalProcessingOfDBMenuItems($menuItems)
-    {
-        return $menuItems;
-    }
-
-    /**
-     * Processing the $menuItems array by external classes (typ. adding items)
-     *
-     * @param array $menuItems Array for manipulation.
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @return array Processed $menuItems array
-     */
-    public function processingByExtClassArray($menuItems, $table, $uid)
-    {
-        if (is_array($this->extClassArray)) {
-            foreach ($this->extClassArray as $conf) {
-                $obj = GeneralUtility::makeInstance($conf['name']);
-                $menuItems = $obj->main($this, $menuItems, $table, $uid);
-            }
-        }
-        return $menuItems;
-    }
-
-    /**
-     * Returning JavaScript for the onClick event linking to the input URL.
-     *
-     * @param string $url The URL relative to TYPO3_mainDir
-     * @param string $retUrl The return_url-parameter
-     * @param bool $hideCM If set, the "hideCM()" will be called
-     * @param string $overrideLoc If set, gives alternative location to load in (for example top frame or somewhere else)
-     * @return string JavaScript for an onClick event.
-     */
-    public function urlRefForCM($url, $retUrl = '', $hideCM = true, $overrideLoc = '')
-    {
-        $loc = 'top.list_frame';
-        return ($overrideLoc ? 'var docRef=' . $overrideLoc : 'var docRef=(top.list_frame)?top.list_frame:' . $loc)
-            . '; docRef.location.href=' . GeneralUtility::quoteJSvalue($url) . ($retUrl ? '+' . GeneralUtility::quoteJSvalue('&' . $retUrl . '=') . '+top.rawurlencode('
-            . $this->frameLocation('docRef.document') . '.pathname+' . $this->frameLocation('docRef.document') . '.search)' : '')
-            . ';';
-    }
-
-    /**
-     * Adding CM element for Clipboard "copy" and "cut"
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @param string $type Type: "copy" or "cut
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_copycut($table, $uid, $type)
-    {
-        $isSel = '';
-        if ($this->clipObj->current === 'normal') {
-            $isSel = $this->clipObj->isSelected($table, $uid);
-        }
-        $addParam = [];
-        if ($this->listFrame) {
-            $addParam['reloadListFrame'] = 1;
-        }
-        $icon = $this->iconFactory->getIcon('actions-edit-' . $type . ($isSel === $type ? '-release' : ''), Icon::SIZE_SMALL)->render();
-        return $this->linkItem(
-            $this->label($type),
-            $icon,
-            'TYPO3.ClickMenu.fetch(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlDB($table, $uid, ($type === 'copy' ? 1 : 0), ($isSel == $type), $addParam)) . ');return false;'
-        );
-    }
-
-    /**
-     * Adding CM element for Clipboard "paste into"/"paste after"
-     * NOTICE: $table and $uid should follow the special syntax for paste, see clipboard-class :: pasteUrl();
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record. NOTICE: Special syntax!
-     * @param string $type Type: "into" or "after
-     * @param array $elInfo Contains instructions about whether to copy or cut an element.
-     * @return array Item array, element in $menuItems
-     * @see \TYPO3\CMS\Backend\Clipboard\Clipboard::pasteUrl()
-     * @internal
-     */
-    public function DB_paste($table, $uid, $type, $elInfo)
-    {
-        $loc = 'top.list_frame';
-        $jsCode = $loc . '.location.href='
-            . GeneralUtility::quoteJSvalue($this->clipObj->pasteUrl($table, $uid, 0) . '&redirect=')
-            . ' + top.rawurlencode(' . $this->frameLocation($loc . '.document') . '.pathname+'
-            . $this->frameLocation($loc . '.document') . '.search);';
-
-        if ($this->backendUser->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) {
-            $title = $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste');
-            $lllKey = ($elInfo[2] === 'copy' ? 'copy' : 'move') . '_' . $type;
-            $confirmMessage = sprintf(
-                $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:mess.' . $lllKey),
-                $elInfo[0],
-                $elInfo[1]
-            );
-            $jsCode = 'top.TYPO3.Modal.confirm(' . GeneralUtility::quoteJSvalue($title) . ', '
-                . GeneralUtility::quoteJSvalue($confirmMessage) . ')'
-                . '.on(\'button.clicked\', function(e) { if (e.target.name === \'ok\') {'
-                . $jsCode
-                . '} top.TYPO3.Modal.dismiss(); });';
-        }
-        $editOnClick = 'if(' . $loc . ') { ' . $jsCode . ' }';
-        return $this->linkItem(
-            $this->label('paste' . $type),
-            $this->iconFactory->getIcon('actions-document-paste-' . $type, Icon::SIZE_SMALL)->render(),
-            $editOnClick . 'return false;'
-        );
-    }
-
-    /**
-     * Adding CM element for Info
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_info($table, $uid)
-    {
-        return $this->linkItem(
-            $this->label('info'),
-            $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render(),
-            'top.launchView(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($uid) . ');'
-        );
-    }
-
-    /**
-     * Adding CM element for History
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_history($table, $uid)
-    {
-        $url = BackendUtility::getModuleUrl('record_history', ['element' => $table . ':' . $uid]);
-        return $this->linkItem(
-            htmlspecialchars($this->languageService->getLL('CM_history')),
-            $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render(),
-            $this->urlRefForCM($url, 'returnUrl')
-        );
-    }
-
-    /**
-     * Adding CM element for Permission setting
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @param array $rec The "pages" record with "perms_*" fields inside.
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_perms($table, $uid, $rec)
-    {
-        if (!ExtensionManagementUtility::isLoaded('beuser')) {
-            return '';
-        }
-
-        $parameters = [
-            'id' => $uid,
-        ];
-
-        if ($rec['perms_userid'] == $this->backendUser->user['uid'] || $this->backendUser->isAdmin()) {
-            $parameters['returnId'] = $uid;
-            $parameters['tx_beuser_system_beusertxpermission'] = ['action' => 'edit'];
-        }
-
-        $url = BackendUtility::getModuleUrl('system_BeuserTxPermission', $parameters);
-        return $this->linkItem(
-            htmlspecialchars($this->languageService->getLL('CM_perms')),
-            $this->iconFactory->getIcon('status-status-locked', Icon::SIZE_SMALL)->render(),
-            $this->urlRefForCM($url)
-        );
-    }
-
-    /**
-     * Adding CM element for DBlist
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @param array $rec Record of the element (needs "pid" field if not pages-record)
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_db_list($table, $uid, $rec)
-    {
-        $urlParams = [];
-        $urlParams['id'] = $table === 'pages' ? $uid : $rec['pid'];
-        $urlParams['table'] = $table === 'pages' ? '' : $table;
-        $url = BackendUtility::getModuleUrl('web_list', $urlParams);
-        return $this->linkItem(
-            htmlspecialchars($this->languageService->getLL('CM_db_list')),
-            $this->iconFactory->getIcon('actions-system-list-open', Icon::SIZE_SMALL)->render(),
-            'top.nextLoadModuleUrl=' . GeneralUtility::quoteJSvalue($url) . ';top.goToModule(\'web_list\', 1);'
-        );
-    }
-
-    /**
-     * Adding CM element for Moving wizard
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @param array $rec Record. Needed for tt-content elements which will have the sys_language_uid sent
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_moveWizard($table, $uid, $rec)
-    {
-        // Hardcoded field for tt_content elements.
-        $url = BackendUtility::getModuleUrl('move_element') . '&table=' . $table . '&uid=' . $uid;
-        $url .= ($table === 'tt_content' ? '&sys_language_uid=' . (int)$rec['sys_language_uid'] : '');
-        return $this->linkItem(
-            htmlspecialchars($this->languageService->getLL('CM_moveWizard' . ($table === 'pages' ? '_page' : ''))),
-            $this->iconFactory->getIcon('actions-' . ($table === 'pages' ? 'page' : 'document') . '-move', Icon::SIZE_SMALL)->render(),
-            $this->urlRefForCM($url, 'returnUrl')
-        );
-    }
-
-    /**
-     * Adding CM element for Create new wizard (either BackendUtility::getModuleUrl('db_new') or BackendUtility::getModuleUrl('new_content_element') or custom wizard)
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @param array $rec Record.
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_newWizard($table, $uid, $rec)
-    {
-        if ($table === 'pages') {
-            $url = BackendUtility::getModuleUrl('db_new', ['id' => $uid, 'pagesOnly' => 1]);
-        } else {
-            //  If mod.newContentElementWizard.override is set, use a custom module instead
-            $tsConfig = BackendUtility::getModTSconfig((int)$this->pageinfo['uid'], 'mod');
-            $newContentWizardModuleName = isset($tsConfig['properties']['newContentElementWizard.']['override'])
-                ? $tsConfig['properties']['newContentElementWizard.']['override']
-                : 'new_content_element';
-            $url = BackendUtility::getModuleUrl($newContentWizardModuleName, ['id' => $rec['pid'], 'sys_language_uid' => (int)$rec['sys_language_uid']]);
-        }
-        return $this->linkItem(
-            htmlspecialchars($this->languageService->getLL('CM_newWizard')),
-            $this->iconFactory->getIcon(($table === 'pages' ? 'actions-page-new' : 'actions-document-new'), Icon::SIZE_SMALL)->render(),
-            $this->urlRefForCM($url, 'returnUrl')
-        );
-    }
-
-    /**
-     * Adding CM element for Editing of the access related fields of a table (disable, starttime, endtime, fe_groups)
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_editAccess($table, $uid)
-    {
-        $url = BackendUtility::getModuleUrl('record_edit', [
-            'columnsOnly' => (implode(',', $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']) . ($table === 'pages' ? ',extendToSubpages' : '')),
-            'edit[' . $table . '][' . $uid . ']' => 'edit'
-        ]);
-        return $this->linkItem(
-            htmlspecialchars($this->languageService->getLL('CM_editAccess')),
-            $this->iconFactory->getIcon('actions-document-edit-access', Icon::SIZE_SMALL)->render(),
-            $this->urlRefForCM($url, 'returnUrl'),
-            1
-        );
-    }
-
-    /**
-     * Adding CM element for edit page properties
-     *
-     * @param int $uid page uid to edit (PID)
-     * @return array Item array, element in $menuItems
-     * @internal
-     * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
-     */
-    public function DB_editPageProperties($uid)
-    {
-        GeneralUtility::logDeprecatedFunction();
-        $url = BackendUtility::getModuleUrl('record_edit', [
-            'edit[pages][' . $uid . ']' => 'edit'
-        ]);
-        return $this->linkItem(
-            htmlspecialchars($this->languageService->getLL('CM_editPageProperties')),
-            $this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render(),
-            $this->urlRefForCM($url, 'returnUrl'),
-            1
-        );
-    }
-
-    /**
-     * Adding CM element for regular editing of the element!
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_edit($table, $uid)
-    {
-        // If another module was specified, replace the default Page module with the new one
-        $newPageModule = trim($this->backendUser->getTSConfigVal('options.overridePageModule'));
-        $pageModule = BackendUtility::isModuleSetInTBE_MODULES($newPageModule) ? $newPageModule : 'web_layout';
-        $loc = 'top.list_frame';
-        $iconName = 'actions-open';
-        if ($table === 'pages') {
-            $iconName = 'actions-page-open';
-        }
-        $theIcon = $this->iconFactory->getIcon($iconName, Icon::SIZE_SMALL)->render();
-
-        $link = BackendUtility::getModuleUrl('record_edit', [
-            'edit[' . $table . '][' . $uid . ']' => 'edit'
-        ]);
-
-        if ($this->iParts[0] === 'pages' && $this->iParts[1] && $this->backendUser->check('modules', $pageModule)) {
-            $this->editPageIconSet = true;
-        }
-        $editOnClick = 'if(' . $loc . '){' . $loc . '.location.href=' . GeneralUtility::quoteJSvalue($link . '&returnUrl=') . '+top.rawurlencode(' . $this->frameLocation(($loc . '.document')) . '.pathname+' . $this->frameLocation(($loc . '.document')) . '.search);}';
-        return $this->linkItem($this->label('edit'), $theIcon, $editOnClick . ';');
-    }
-
-    /**
-     * Adding CM element for regular Create new element
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_new($table, $uid)
-    {
-        $frame = 'top.list_frame';
-        $location = $this->frameLocation($frame . '.document');
-        $module = $this->listFrame
-            ? GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('record_edit', ['edit[' . $table . '][-' . $uid . ']' => 'new']) . '&returnUrl=') . '+top.rawurlencode(' . $location . '.pathname+' . $location . '.search)'
-            : GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('db_new', ['id' => (int)$uid]));
-        $editOnClick = 'if(' . $frame . '){' . $frame . '.location.href=' . $module . ';}';
-        $icon = $this->iconFactory->getIcon('actions-' . ($table === 'pages' ? 'page' : 'document') . '-new', Icon::SIZE_SMALL)->render();
-        return $this->linkItem($this->label('new'), $icon, $editOnClick);
-    }
-
-    /**
-     * Adding CM element for Delete
-     *
-     * @param string $table Table name
-     * @param int $uid UID for the current record.
-     * @param array $elInfo Label for including in the confirmation message, EXT:lang/Resources/Private/Language/locallang_core.xlf:mess.delete
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_delete($table, $uid, $elInfo)
-    {
-        $loc = 'top.list_frame';
-        $jsCode = $loc . '.location.href='
-            . GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('tce_db') . '&redirect=')
-            . '+top.rawurlencode(' . $this->frameLocation($loc . '.document') . '.pathname+'
-            . $this->frameLocation($loc . '.document') . '.search)+'
-            . GeneralUtility::quoteJSvalue(
-                '&cmd[' . $table . '][' . $uid . '][delete]=1&prErr=1'
-            )
-            . ';';
-
-        if ($table === 'pages') {
-            $jsCode .= 'if (top && top.TYPO3.Backend && top.TYPO3.Backend.NavigationContainer.PageTree) {';
-            $jsCode .= '     top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree.defer(500);';
-            $jsCode .= '}';
-        }
-
-        if ($this->backendUser->jsConfirmation(JsConfirmation::DELETE)) {
-            $title = $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:delete');
-            $confirmMessage = sprintf(
-                $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:mess.delete'),
-                $elInfo[0]
-            );
-            $confirmMessage .= BackendUtility::referenceCount(
-                $table,
-                $uid,
-                ' ' . $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord')
-            );
-            $confirmMessage .= BackendUtility::translationCount(
-                $table,
-                $uid,
-                ' ' . $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
-            );
-            $jsCode = 'top.TYPO3.Modal.confirm(' . GeneralUtility::quoteJSvalue($title) . ', '
-                . GeneralUtility::quoteJSvalue($confirmMessage) . ')'
-                . '.on(\'button.clicked\', function(e) { if (e.target.name === \'ok\') {'
-                . $jsCode
-                . '} top.TYPO3.Modal.dismiss(); });';
-        }
-        $editOnClick = 'if(' . $loc . ') { ' . $jsCode . ' }';
-        return $this->linkItem(
-            $this->label('delete'),
-            $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render(),
-            $editOnClick . 'return false;'
-        );
-    }
-
-    /**
-     * Adding CM element for View Page
-     *
-     * @param int $id Page uid (PID)
-     * @param string $anchor Anchor, if any
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_view($id, $anchor = '')
-    {
-        $icon = $this->iconFactory->getIcon('actions-document-view', Icon::SIZE_SMALL)->render();
-        return $this->linkItem($this->label('view'), $icon, BackendUtility::viewOnClick($id, '', null, $anchor) . ';');
-    }
-
-    /**
-     * Adding element for setting temporary mount point.
-     *
-     * @param int $page_id Page uid (PID)
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_tempMountPoint($page_id)
-    {
-        return $this->linkItem(
-            $this->label('tempMountPoint'),
-            $this->iconFactory->getIcon('actions-pagetree-mountroot', Icon::SIZE_SMALL)->render(),
-            'if (top.nav_frame) {
-                               var node = top.TYPO3.Backend.NavigationContainer.PageTree.getSelected();
-                               if (node === null) {
-                                       return false;
-                               }
-
-                               var useNode = {
-                                       attributes: {
-                                               nodeData: {
-                                                       id: ' . (int)$page_id . '
-                                               }
-                                       }
-                               };
-
-                               node.ownerTree.commandProvider.mountAsTreeRoot(useNode, node.ownerTree);
-                       }
-                       ');
-    }
-
-    /**
-     * Adding CM element for hide/unhide of the input record
-     *
-     * @param string $table Table name
-     * @param array $rec Record array
-     * @param string $hideField Name of the hide field
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function DB_hideUnhide($table, $rec, $hideField)
-    {
-        return $this->DB_changeFlag($table, $rec, $hideField, $this->label(($rec[$hideField] ? 'un' : '') . 'hide'));
-    }
-
-    /**
-     * Adding CM element for a flag field of the input record
-     *
-     * @param string $table Table name
-     * @param array $rec Record array
-     * @param string $flagField Name of the flag field
-     * @param string $title Menu item Title
-     * @return array Item array, element in $menuItems
-     */
-    public function DB_changeFlag($table, $rec, $flagField, $title)
-    {
-        $uid = $rec['_ORIG_uid'] ?: $rec['uid'];
-        $loc = 'top.list_frame';
-        $editOnClick = 'if(' . $loc . '){' . $loc . '.location.href=' .
-            GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('tce_db') . '&redirect=') . '+top.rawurlencode(' .
-            $this->frameLocation($loc . '.document') . '.pathname+' . $this->frameLocation(($loc . '.document')) . '.search)+' .
-            GeneralUtility::quoteJSvalue(
-                '&data[' . $table . '][' . $uid . '][' . $flagField . ']=' . ($rec[$flagField] ? 0 : 1) . '&prErr=1'
-            ) . ';};';
-        if ($table === 'pages') {
-            $editOnClick .= 'top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree.defer(500);';
-        }
-        return $this->linkItem(
-            $title,
-            $this->iconFactory->getIcon('actions-edit-' . ($rec[$flagField] ? 'un' : '') . 'hide', Icon::SIZE_SMALL)->render(),
-            $editOnClick . 'return false;',
-            1
-        );
-    }
-
-    /***************************************
-     *
-     * FILE
-     *
-     ***************************************/
-    /**
-     * Make 1st level clickmenu:
-     *
-     * @param string $combinedIdentifier The combined identifier
-     * @return string HTML content
-     * @see \TYPO3\CMS\Core\Resource\ResourceFactory::retrieveFileOrFolderObject()
-     */
-    public function printFileClickMenu($combinedIdentifier)
-    {
-        $identifier = '';
-        $menuItems = [];
-        $combinedIdentifier = rawurldecode($combinedIdentifier);
-        $fileObject = ResourceFactory::getInstance()
-                ->retrieveFileOrFolderObject($combinedIdentifier);
-        if ($fileObject) {
-            $folder = false;
-            $isStorageRoot = false;
-            $isOnline = true;
-            $userMayViewStorage = false;
-            $userMayEditStorage = false;
-            $identifier = $fileObject->getCombinedIdentifier();
-            if ($fileObject instanceof Folder) {
-                $folder = true;
-                if ($fileObject->getIdentifier() === $fileObject->getStorage()->getRootLevelFolder()->getIdentifier()) {
-                    $isStorageRoot = true;
-                    if ($this->backendUser->check('tables_select', 'sys_file_storage')) {
-                        $userMayViewStorage = true;
-                    }
-                    if ($this->backendUser->check('tables_modify', 'sys_file_storage')) {
-                        $userMayEditStorage = true;
-                    }
-                }
-                if (!$fileObject->getStorage()->isOnline()) {
-                    $isOnline = false;
-                }
-            }
-            // Hide
-            if (!in_array('hide', $this->disabledItems, true) && $isStorageRoot && $userMayEditStorage) {
-                $record = BackendUtility::getRecord('sys_file_storage', $fileObject->getStorage()->getUid());
-                $menuItems['hide'] = $this->DB_changeFlag(
-                    'sys_file_storage',
-                    $record,
-                    'is_online',
-                    $this->label($record['is_online'] ? 'offline' : 'online')
-                );
-            }
-            // Edit
-            if (!in_array('edit', $this->disabledItems, true) && $fileObject->checkActionPermission('write')) {
-                if (!$folder && !$isStorageRoot && $fileObject->isIndexed() && $this->backendUser->check('tables_modify', 'sys_file_metadata')) {
-                    $metaData = $fileObject->_getMetaData();
-                    $menuItems['edit2'] = $this->DB_edit('sys_file_metadata', $metaData['uid']);
-                }
-                if (!$folder && GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'], $fileObject->getExtension()) && $fileObject->checkActionPermission('write')) {
-                    $menuItems['edit'] = $this->FILE_launch($identifier, 'file_edit', 'editcontent', 'actions-page-open');
-                } elseif ($isStorageRoot && $userMayEditStorage) {
-                    $menuItems['edit'] = $this->DB_edit('sys_file_storage', $fileObject->getStorage()->getUid());
-                }
-            }
-            // Rename
-            if (!in_array('rename', $this->disabledItems, true) && !$isStorageRoot && $fileObject->checkActionPermission('rename')) {
-                $menuItems['rename'] = $this->FILE_launch($identifier, 'file_rename', 'rename', 'actions-edit-rename');
-            }
-            // Upload
-            if (!in_array('upload', $this->disabledItems, true) && $folder && $isOnline && $fileObject->checkActionPermission('write')) {
-                $menuItems['upload'] = $this->FILE_launch($identifier, 'file_upload', 'upload', 'actions-edit-upload');
-            }
-            // New
-            if (!in_array('new', $this->disabledItems, true) && $folder && $isOnline && $fileObject->checkActionPermission('write')) {
-                $menuItems['new'] = $this->FILE_launch($identifier, 'file_newfolder', 'new', 'actions-document-new');
-            }
-            // Info
-            if (!in_array('info', $this->disabledItems, true) && $fileObject->checkActionPermission('read')) {
-                if ($isStorageRoot && $userMayViewStorage) {
-                    $menuItems['info'] = $this->DB_info('sys_file_storage', $fileObject->getStorage()->getUid());
-                } elseif (!$folder) {
-                    $menuItems['info'] = $this->fileInfo($identifier);
-                }
-            }
-            $menuItems[] = 'spacer';
-            // Copy:
-            if (!in_array('copy', $this->disabledItems, true) && !$isStorageRoot && $fileObject->checkActionPermission('read')) {
-                $menuItems['copy'] = $this->FILE_copycut($identifier, 'copy');
-            }
-            // Cut:
-            if (!in_array('cut', $this->disabledItems, true) && !$isStorageRoot && $fileObject->checkActionPermission('move')) {
-                $menuItems['cut'] = $this->FILE_copycut($identifier, 'cut');
-            }
-            // Paste:
-            $elFromAllTables = count($this->clipObj->elFromTable('_FILE'));
-            if (!in_array('paste', $this->disabledItems, true) && $elFromAllTables && $folder && $fileObject->checkActionPermission('write')) {
-                $elArr = $this->clipObj->elFromTable('_FILE');
-                $selItem = reset($elArr);
-                $clickedFileOrFolder = ResourceFactory::getInstance()->retrieveFileOrFolderObject($combinedIdentifier);
-                $fileOrFolderInClipBoard = ResourceFactory::getInstance()->retrieveFileOrFolderObject($selItem);
-                $elInfo = [
-                    $fileOrFolderInClipBoard->getName(),
-                    $clickedFileOrFolder->getName(),
-                    $this->clipObj->currentMode()
-                ];
-                if (!$fileOrFolderInClipBoard instanceof Folder || !$fileOrFolderInClipBoard->getStorage()->isWithinFolder($fileOrFolderInClipBoard, $clickedFileOrFolder)) {
-                    $menuItems['pasteinto'] = $this->FILE_paste($identifier, $selItem, $elInfo);
-                }
-            }
-            $menuItems[] = 'spacer';
-            // Delete:
-            if (!in_array('delete', $this->disabledItems, true) && $fileObject->checkActionPermission('delete')) {
-                if ($isStorageRoot && $userMayEditStorage) {
-                    $elInfo = [GeneralUtility::fixed_lgd_cs($fileObject->getStorage()->getName(), $this->backendUser->uc['titleLen'])];
-                    $menuItems['delete'] = $this->DB_delete('sys_file_storage', $fileObject->getStorage()->getUid(), $elInfo);
-                } elseif (!$isStorageRoot) {
-                    $menuItems['delete'] = $this->FILE_delete($identifier);
-                }
-            }
-        }
-        // Adding external elements to the menuItems array
-        $menuItems = $this->processingByExtClassArray($menuItems, $identifier, 0);
-        // Processing by external functions?
-        $menuItems = $this->externalProcessingOfFileMenuItems($menuItems);
-        // Return the printed elements:
-        return $this->printItems($menuItems);
-    }
-
-    /**
-     * Processing the $menuItems array (for extension classes) (FILES)
-     *
-     * @param array $menuItems Array for manipulation.
-     * @return array Processed $menuItems array
-     */
-    public function externalProcessingOfFileMenuItems($menuItems)
-    {
-        return $menuItems;
-    }
-
-    /**
-     * Multi-function for adding an entry to the $menuItems array
-     *
-     * @param string $path Path to the file/directory (target)
-     * @param string $moduleName Script (deprecated) or module name (e.g. file_edit) to pass &target= to
-     * @param string $type "type" is the code which fetches the correct label for the element from "cm.
-     * @param string $iconName
-     * @param bool $noReturnUrl If set, the return URL parameter will not be set in the link
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function FILE_launch($path, $moduleName, $type, $iconName, $noReturnUrl = false)
-    {
-        $loc = 'top.list_frame';
-        $scriptUrl = BackendUtility::getModuleUrl($moduleName);
-
-        $editOnClick = 'if(' . $loc . '){' . $loc . '.location.href=' . GeneralUtility::quoteJSvalue($scriptUrl . '&target=' . rawurlencode($path)) . ($noReturnUrl ? '' : '+\'&returnUrl=\'+top.rawurlencode(' . $this->frameLocation($loc . '.document') . '.pathname+' . $this->frameLocation($loc . '.document') . '.search)') . ';}';
-        return $this->linkItem(
-            $this->label($type),
-            $this->iconFactory->getIcon($iconName, Icon::SIZE_SMALL)->render(),
-            $editOnClick
-        );
-    }
-
-    /**
-     * Returns element for copy or cut of files.
-     *
-     * @param string $path Path to the file/directory (target)
-     * @param string $type Type: "copy" or "cut
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function FILE_copycut($path, $type)
-    {
-        $isSel = '';
-        // Pseudo table name for use in the clipboard.
-        $table = '_FILE';
-        $uid = GeneralUtility::shortMD5($path);
-        if ($this->clipObj->current === 'normal') {
-            $isSel = $this->clipObj->isSelected($table, $uid);
-        }
-        $addParam = [];
-        if ($this->listFrame) {
-            $addParam['reloadListFrame'] = 1;
-        }
-        return $this->linkItem(
-            $this->label($type),
-            $this->iconFactory->getIcon('actions-edit-' . $type . ($isSel === $type ? '-release' : ''), Icon::SIZE_SMALL)->render(),
-            'TYPO3.ClickMenu.fetch(' . GeneralUtility::quoteJSvalue($this->clipObj->selUrlFile($path, ($type === 'copy' ? 1 : 0), ($isSel == $type), $addParam)) . ');return false;'
-        );
-    }
-
-    /**
-     * Creates element for deleting of target
-     *
-     * @param string $path Path to the file/directory (target)
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function FILE_delete($path)
-    {
-        $loc = 'top.list_frame';
-        $jsCode = $loc . '.location.href='
-            . GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('tce_file') . '&redirect=')
-            . '+top.rawurlencode(' . $this->frameLocation(($loc . '.document'))
-            . '.pathname+' . $this->frameLocation(($loc . '.document')) . '.search)+' .
-            GeneralUtility::quoteJSvalue(
-                '&file[delete][0][data]=' . rawurlencode($path)
-            );
-        if ($this->backendUser->jsConfirmation(JsConfirmation::DELETE)) {
-            $fileOrFolderObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($path);
-            $title = $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:delete');
-            $confirmMessage = sprintf(
-                $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:mess.delete'),
-                $fileOrFolderObject->getName()
-            );
-            if ($fileOrFolderObject instanceof Folder) {
-                $confirmMessage .= BackendUtility::referenceCount('_FILE', $fileOrFolderObject->getIdentifier(), ' ' . $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.referencesToFolder'));
-            } else {
-                $confirmMessage .= BackendUtility::referenceCount('sys_file', $fileOrFolderObject->getUid(), ' ' . $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.referencesToFile'));
-            }
-            $jsCode = 'top.TYPO3.Modal.confirm(' . GeneralUtility::quoteJSvalue($title) . ', '
-                . GeneralUtility::quoteJSvalue($confirmMessage) . ')'
-                . '.on(\'button.clicked\', function(e) { if (e.target.name === \'ok\') {'
-                . $jsCode
-                . '} top.TYPO3.Modal.dismiss(); });';
-        }
-        $editOnClick = 'if(' . $loc . ') { ' . $jsCode . ' }';
-        return $this->linkItem(
-            $this->label('delete'),
-            $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render(),
-            $editOnClick . 'return false;'
-        );
-    }
-
-    /**
-     * Creates element for pasting files.
-     *
-     * @param string $path Path to the file/directory (target)
-     * @param string $target target - NOT USED.
-     * @param array $elInfo Various values for the labels.
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function FILE_paste($path, $target, $elInfo)
-    {
-        $loc = 'top.list_frame';
-
-        $jsCode = $loc . '.location.href='
-            . GeneralUtility::quoteJSvalue($this->clipObj->pasteUrl('_FILE', $path, 0) . '&redirect=')
-            . '+top.rawurlencode(' . $this->frameLocation($loc . '.document')
-            . '.pathname+' . $this->frameLocation($loc . '.document') . '.search);';
-
-        if ($this->backendUser->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) {
-            $title = $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste');
-
-            $confirmMessage = sprintf(
-                $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:mess.'
-                    . ($elInfo[2] === 'copy' ? 'copy' : 'move') . '_into'),
-                $elInfo[0],
-                $elInfo[1]
-            );
-
-            $jsCode = 'top.TYPO3.Modal.confirm(' . GeneralUtility::quoteJSvalue($title) . ', '
-                . GeneralUtility::quoteJSvalue($confirmMessage) . ')'
-                . '.on(\'button.clicked\', function(e) { if (e.target.name === \'ok\') {'
-                . $jsCode
-                . '} top.TYPO3.Modal.dismiss(); });';
-        }
-        $editOnClick = 'if(' . $loc . ') { ' . $jsCode . ' }';
-        return $this->linkItem(
-            $this->label('pasteinto'),
-            $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render(),
-            $editOnClick . 'return false;'
-        );
-    }
-
-    /**
-     * Adding ClickMenu element for file info
-     *
-     * @param string $identifier The combined identifier of the file.
-     * @return array Item array, element in $menuItems
-     */
-    protected function fileInfo($identifier)
-    {
-        return $this->DB_info('_FILE', $identifier);
-    }
-
-    /***************************************
-     *
-     * DRAG AND DROP
-     *
-     ***************************************/
-    /**
-     * Make 1st level clickmenu:
-     *
-     * @param string $table The absolute path
-     * @param int $srcId UID for the current record.
-     * @param int $dstId Destination ID
-     * @return array the array to be returned as JSON
-     */
-    public function printDragDropClickMenu($table, $srcId, $dstId)
-    {
-        $menuItems = [];
-        // If the drag and drop menu should apply to PAGES use this set of menu items
-        if ($table === 'pages') {
-            // Move Into:
-            $menuItems['movePage_into'] = $this->dragDrop_copymovepage($srcId, $dstId, 'move', 'into');
-            // Move After:
-            $menuItems['movePage_after'] = $this->dragDrop_copymovepage($srcId, $dstId, 'move', 'after');
-            // Copy Into:
-            $menuItems['copyPage_into'] = $this->dragDrop_copymovepage($srcId, $dstId, 'copy', 'into');
-            // Copy After:
-            $menuItems['copyPage_after'] = $this->dragDrop_copymovepage($srcId, $dstId, 'copy', 'after');
-        }
-        // If the drag and drop menu should apply to FOLDERS use this set of menu items
-        if ($table === 'folders') {
-            // Move Into:
-            $menuItems['moveFolder_into'] = $this->dragDrop_copymovefolder($srcId, $dstId, 'move');
-            // Copy Into:
-            $menuItems['copyFolder_into'] = $this->dragDrop_copymovefolder($srcId, $dstId, 'copy');
-        }
-        // Adding external elements to the menuItems array
-        $menuItems = $this->processingByExtClassArray($menuItems, 'dragDrop_' . $table, $srcId);
-        // to extend this, you need to apply a Context Menu to a "virtual" table called "dragDrop_pages" or similar
-        // Processing by external functions?
-        $menuItems = $this->externalProcessingOfDBMenuItems($menuItems);
-        // Return the printed elements:
-        return $this->printItems($menuItems);
-    }
-
-    /**
-     * Processing the $menuItems array (for extension classes) (DRAG'N DROP)
-     *
-     * @param array $menuItems Array for manipulation.
-     * @return array Processed $menuItems array
-     */
-    public function externalProcessingOfDragDropMenuItems($menuItems)
-    {
-        return $menuItems;
-    }
-
-    /**
-     * Adding CM element for Copying/Moving a Page Into/After from a drag & drop action
-     *
-     * @param int $srcUid source UID code for the record to modify
-     * @param int $dstUid destination UID code for the record to modify
-     * @param string $action Action code: either "move" or "copy
-     * @param string $into Parameter code: either "into" or "after
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function dragDrop_copymovepage($srcUid, $dstUid, $action, $into)
-    {
-        $negativeSign = $into === 'into' ? '' : '-';
-        $loc = 'top.list_frame';
-        $editOnClick = 'if(' . $loc . '){' . $loc . '.document.location=' .
-            GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('tce_db') . '&redirect=') . '+top.rawurlencode(' .
-            $this->frameLocation(($loc . '.document')) . '.pathname+' . $this->frameLocation(($loc . '.document')) . '.search)+' .
-            GeneralUtility::quoteJSvalue(
-                '&cmd[pages][' . $srcUid . '][' . $action . ']=' . $negativeSign . $dstUid . '&prErr=1'
-            ) . ';};';
-        return $this->linkItem(
-            $this->label($action . 'Page_' . $into),
-            $this->iconFactory->getIcon('actions-document-paste-' . $into, Icon::SIZE_SMALL)->render(),
-            $editOnClick . 'return false;'
-        );
-    }
-
-    /**
-     * Adding CM element for Copying/Moving a Folder Into from a drag & drop action
-     *
-     * @param string $srcPath source path for the record to modify
-     * @param string $dstPath destination path for the records to modify
-     * @param string $action Action code: either "move" or "copy
-     * @return array Item array, element in $menuItems
-     * @internal
-     */
-    public function dragDrop_copymovefolder($srcPath, $dstPath, $action)
-    {
-        $loc = 'top.list_frame';
-        $editOnClick = 'if(' . $loc . '){' . $loc . '.document.location=' .
-            GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('tce_file') . '&redirect=') . '+top.rawurlencode(' .
-            $this->frameLocation(($loc . '.document')) . '.pathname+' . $this->frameLocation(($loc . '.document')) . '.search)+' .
-            GeneralUtility::quoteJSvalue(
-                '&file[' . $action . '][0][data]=' . $srcPath . '&file[' . $action . '][0][target]=' . $dstPath . '&prErr=1'
-            ) . ';};';
-        return $this->linkItem(
-            $this->label($action . 'Folder_into'),
-            $this->iconFactory->getIcon('apps-pagetree-drag-move-into', Icon::SIZE_SMALL)->render(),
-            $editOnClick . 'return false;'
-        );
-    }
-
-    /***************************************
-     *
-     * COMMON
-     *
-     **************************************/
-    /**
-     * Prints the items from input $menuItems array - as JS section for writing to the div-layers.
-     *
-     * @param array $menuItems Array
-     * @return array the array to be returned via JSON
-     */
-    public function printItems($menuItems)
-    {
-        // Enable/Disable items
-        $menuItems = $this->enableDisableItems($menuItems);
-        // Clean up spacers
-        $menuItems = $this->cleanUpSpacers($menuItems);
-        // Adding JS part and return the content
-        return $this->printLayerJScode($menuItems);
-    }
-
-    /**
-     * Create the JavaScript section
-     *
-     * @param array $menuItems The $menuItems array to print
-     * @return array|null the items to print, and The JavaScript section which will print the content of the CM to the div-layer in the target frame.
-     */
-    public function printLayerJScode($menuItems)
-    {
-        // Clipboard must not be submitted - then it's probably a copy/cut situation.
-        if ($this->isCMlayers()) {
-            // Create the table displayed in the clickmenu layer:
-            // Wrap the inner table in another table to create outer border:
-            return [
-                'items' => $this->menuItemsForClickMenu($menuItems),
-                'level' => $this->cmLevel
-            ];
-        }
-    }
-
-    /**
-     * Traverses the menuItems and generates an output array for implosion in the CM div-layers table.
-     *
-     * @param array $menuItems Array
-     * @return array array for implosion in the CM div-layers table.
-     */
-    public function menuItemsForClickMenu($menuItems)
-    {
-        $out = [];
-        foreach ($menuItems as $cc => $i) {
-            // MAKE horizontal spacer
-            if (is_string($i) && $i === 'spacer') {
-                $out[] = '<span class="list-group-item list-group-item-divider"></span>';
-            } else {
-                // Just make normal element:
-                $onClick = $i[3];
-                $onClick = preg_replace('/return[[:space:]]+hideCM\\(\\)[[:space:]]*;/i', '', $onClick);
-                $onClick = preg_replace('/return[[:space:]]+false[[:space:]]*;/i', '', $onClick);
-                $onClick = preg_replace('/hideCM\\(\\);/i', '', $onClick);
-                if (!$i[5]) {
-                    $onClick .= 'TYPO3.ClickMenu.hideAll();';
-                }
-                $out[] = '
-                                       <a href="javascript:;" class="list-group-item" onclick="' . htmlspecialchars($onClick) . '">
-                                               <span class="list-group-item-icon">' . $i[2] . '</span> ' . $i[1] . '
-                                       </a>';
-            }
-        }
-        return $out;
-    }
-
-    /**
-     * Adds or inserts a menu item
-     * Can be used to set the position of new menu entries within the list of existing menu entries. Has this syntax: [cmd]:[menu entry key],[cmd].... cmd can be "after", "before" or "top" (or blank/"bottom" which is default). If "after"/"before" then menu items will be inserted after/before the existing entry with [menu entry key] if found. "after-spacer" and "before-spacer" do the same, but inserts before or after an item and a spacer. If not found, the bottom of list. If "top" the items are inserted in the top of the list.
-     *
-     * @param array $menuItems Menu items array
-     * @param array $newMenuItems Menu items array to insert
-     * @param string $position Position command string. Has this syntax: [cmd]:[menu entry key],[cmd].... cmd can be "after", "before" or "top" (or blank/"bottom" which is default). If "after"/"before" then menu items will be inserted after/before the existing entry with [menu entry key] if found. "after-spacer" and "before-spacer" do the same, but inserts before or after an item and a spacer. If not found, the bottom of list. If "top" the items are inserted in the top of the list.
-     * @return array Menu items array, processed.
-     */
-    public function addMenuItems($menuItems, $newMenuItems, $position = '')
-    {
-        if (is_array($newMenuItems)) {
-            $pointer = 0;
-            if ($position) {
-                $posArr = GeneralUtility::trimExplode(',', $position, true);
-                foreach ($posArr as $pos) {
-                    list($place, $menuEntry) = GeneralUtility::trimExplode(':', $pos, true);
-                    list($place, $placeExtra) = GeneralUtility::trimExplode('-', $place, true);
-                    // Bottom
-                    $pointer = count($menuItems);
-                    $found = false;
-                    if ($place) {
-                        switch (strtolower($place)) {
-                            case 'after':
-                            case 'before':
-                                if ($menuEntry) {
-                                    $p = 1;
-                                    reset($menuItems);
-                                    while (true) {
-                                        if ((string)key($menuItems) === $menuEntry) {
-                                            $pointer = $p;
-                                            $found = true;
-                                            break;
-                                        }
-                                        if (!next($menuItems)) {
-                                            break;
-                                        }
-                                        $p++;
-                                    }
-                                    if (!$found) {
-                                        break;
-                                    }
-                                    if ($place === 'before') {
-                                        $pointer--;
-                                        if ($placeExtra === 'spacer' and prev($menuItems) === 'spacer') {
-                                            $pointer--;
-                                        }
-                                    } elseif ($place === 'after') {
-                                        if ($placeExtra === 'spacer' and next($menuItems) === 'spacer') {
-                                            $pointer++;
-                                        }
-                                    }
-                                }
-                                break;
-                            default:
-                                if (strtolower($place) === 'top') {
-                                    $pointer = 0;
-                                } else {
-                                    $pointer = count($menuItems);
-                                }
-                                $found = true;
-                        }
-                    }
-                    if ($found) {
-                        break;
-                    }
-                }
-            }
-            $pointer = max(0, $pointer);
-            $menuItemsBefore = array_slice($menuItems, 0, $pointer ?: 0);
-            $menuItemsAfter = array_slice($menuItems, $pointer);
-            $menuItems = $menuItemsBefore + $newMenuItems + $menuItemsAfter;
-        }
-        return $menuItems;
-    }
-
-    /**
-     * Creating an array with various elements for the clickmenu entry
-     *
-     * @param string $str The label, htmlspecialchar'ed already
-     * @param string $icon <img>-tag for the icon
-     * @param string $onClick JavaScript onclick event for label/icon
-     * @param int $onlyCM ==1 and the element will NOT appear in clickmenus in the topframe (unless clickmenu is totally unavailable)! ==2 and the item will NEVER appear in top frame. (This is mostly for "less important" options since the top frame is not capable of holding so many elements horizontally)
-     * @param int $dontHide If set, the clickmenu layer will not hide itself onclick - used for secondary menus to appear...
-     * @return array $menuItem entry with 6 numerical entries: [0] is the HTML for display of the element with link and icon an mouseover etc., [1]-[5] is simply the input params passed through!
-     */
-    public function linkItem($str, $icon, $onClick, $onlyCM = 0, $dontHide = 0)
-    {
-        return [
-            '<a href="javascript:;" onclick="' . htmlspecialchars($onClick) . '">' . $str . $icon . '</a>',
-            $str,
-            $icon,
-            $onClick,
-            $onlyCM,
-            $dontHide
-        ];
-    }
-
-    /**
-     * Enabling / Disabling items based on list provided from GET var ($this->iParts[3])
-     *
-     * @param array $menuItems Menu items array
-     * @return array Menu items array, processed.
-     */
-    public function enableDisableItems($menuItems)
-    {
-        if ($this->iParts[3]) {
-            // Detect "only" mode: (only showing listed items)
-            if ($this->iParts[3][0] === '+') {
-                $this->iParts[3] = substr($this->iParts[3], 1);
-                $only = true;
-            } else {
-                $only = false;
-            }
-            // Do filtering:
-            // Transfer ONLY elements which are mentioned (or are spacers)
-            if ($only) {
-                $newMenuArray = [];
-                foreach ($menuItems as $key => $value) {
-                    if (GeneralUtility::inList($this->iParts[3], $key) || is_string($value) && $value === 'spacer') {
-                        $newMenuArray[$key] = $value;
-                    }
-                }
-                $menuItems = $newMenuArray;
-            } else {
-                // Traverse all elements except those listed (just unsetting them):
-                $elements = GeneralUtility::trimExplode(',', $this->iParts[3], true);
-                foreach ($elements as $value) {
-                    unset($menuItems[$value]);
-                }
-            }
-        }
-        // Return processed menu items:
-        return $menuItems;
-    }
-
-    /**
-     * Clean up spacers; Will remove any spacers in the start/end of menu items array plus any duplicates.
-     *
-     * @param array $menuItems Menu items array
-     * @return array Menu items array, processed.
-     */
-    public function cleanUpSpacers($menuItems)
-    {
-        // Remove doubles:
-        $prevItemWasSpacer = false;
-        foreach ($menuItems as $key => $value) {
-            if (is_string($value) && $value === 'spacer') {
-                if ($prevItemWasSpacer) {
-                    unset($menuItems[$key]);
-                }
-                $prevItemWasSpacer = true;
-            } else {
-                $prevItemWasSpacer = false;
-            }
-        }
-        // Remove first:
-        reset($menuItems);
-        $key = key($menuItems);
-        $value = current($menuItems);
-        if (is_string($value) && $value === 'spacer') {
-            unset($menuItems[$key]);
-        }
-        // Remove last:
-        end($menuItems);
-        $key = key($menuItems);
-        $value = current($menuItems);
-        if (is_string($value) && $value === 'spacer') {
-            unset($menuItems[$key]);
-        }
-        // Return processed menu items:
-        return $menuItems;
-    }
-
-    /**
-     * Get label from locallang_core.xlf:cm.*
-     *
-     * @param string $label The "cm."-suffix to get.
-     * @return string
-     */
-    public function label($label)
-    {
-        return htmlspecialchars($this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.' . $label));
-    }
-
-    /**
-     * Returns TRUE if there should be writing to the div-layers (commands sent to clipboard MUST NOT write to div-layers)
-     *
-     * @return bool
-     */
-    public function isCMlayers()
-    {
-        return !$this->CB;
-    }
-
-    /**
-     * Appends ".location" to input string
-     *
-     * @param string $str Input string, probably a JavaScript document reference
-     * @return string
-     */
-    public function frameLocation($str)
-    {
-        return $str . '.location';
-    }
-}
diff --git a/typo3/sysext/backend/Classes/ContextMenu/ContextMenu.php b/typo3/sysext/backend/Classes/ContextMenu/ContextMenu.php
new file mode 100644 (file)
index 0000000..b72da3e
--- /dev/null
@@ -0,0 +1,133 @@
+<?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;
+    }
+}
diff --git a/typo3/sysext/backend/Classes/ContextMenu/ContextMenuAction.php b/typo3/sysext/backend/Classes/ContextMenu/ContextMenuAction.php
deleted file mode 100644 (file)
index 4ddb31b..0000000
+++ /dev/null
@@ -1,246 +0,0 @@
-<?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;
-    }
-}
diff --git a/typo3/sysext/backend/Classes/ContextMenu/ContextMenuActionCollection.php b/typo3/sysext/backend/Classes/ContextMenu/ContextMenuActionCollection.php
deleted file mode 100644 (file)
index c9b2b53..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?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 Collection
- */
-class ContextMenuActionCollection extends \ArrayObject
-{
-    /**
-     * Returns the collection in an array representation for e.g. serialization
-     *
-     * @return array
-     */
-    public function toArray()
-    {
-        $iterator = $this->getIterator();
-        $arrayRepresentation = [];
-        while ($iterator->valid()) {
-            $arrayRepresentation[] = $iterator->current()->toArray();
-            $iterator->next();
-        }
-        return $arrayRepresentation;
-    }
-}
diff --git a/typo3/sysext/backend/Classes/ContextMenu/ItemProviders/AbstractProvider.php b/typo3/sysext/backend/Classes/ContextMenu/ItemProviders/AbstractProvider.php
new file mode 100644 (file)
index 0000000..25434c9
--- /dev/null
@@ -0,0 +1,225 @@
+<?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
+     * @param string $type
+     * @return bool
+     */
+    protected function canRender(string $itemName, string $type): bool
+    {
+        return true;
+    }
+
+    /**
+     * Returns a clicked record identifier
+     *
+     * @return string
+     */
+    protected function getIdentifier(): string
+    {
+        return '';
+    }
+
+}
diff --git a/typo3/sysext/backend/Classes/ContextMenu/ItemProviders/PageProvider.php b/typo3/sysext/backend/Classes/ContextMenu/ItemProviders/PageProvider.php
new file mode 100644 (file)
index 0000000..de5b724
--- /dev/null
@@ -0,0 +1,437 @@
+<?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\Core\Type\Bitmask\Permission;
+
+/**
+ * Context menu item provider for pages table
+ */
+class PageProvider extends RecordProvider
+{
+    /**
+     * @var string
+     */
+    protected $table = 'pages';
+
+    /**
+     * @var array
+     */
+    protected $itemsConfiguration = [
+        'view' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.view',
+            'iconIdentifier' => 'actions-document-view',
+            'callbackAction' => 'viewRecord'
+        ],
+        'edit' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.edit',
+            'iconIdentifier' => 'actions-page-open',
+            'callbackAction' => 'editRecord'
+        ],
+        'new' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.new',
+            'iconIdentifier' => 'actions-document-new',
+            'callbackAction' => 'newRecord'
+        ],
+        'info' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.info',
+            'iconIdentifier' => 'actions-document-info',
+            'callbackAction' => 'openInfoPopUp'
+        ],
+        'divider1' => [
+            'type' => 'divider'
+        ],
+        'copy' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.copy',
+            'iconIdentifier' => 'actions-edit-copy',
+            'callbackAction' => 'copy'
+        ],
+        'copyRelease' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.copy',
+            'iconIdentifier' => 'actions-edit-copy-release',
+            'callbackAction' => 'clipboardRelease'
+        ],
+        'cut' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.cut',
+            'iconIdentifier' => 'actions-edit-cut',
+            'callbackAction' => 'cut'
+        ],
+        'cutRelease' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.cut',
+            'iconIdentifier' => 'actions-edit-cut-release',
+            'callbackAction' => 'clipboardRelease'
+        ],
+        'pasteAfter' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.pasteafter',
+            'iconIdentifier' => 'actions-document-paste-after',
+            'callbackAction' => 'pasteAfter'
+        ],
+        'pasteInto' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.pasteinto',
+            'iconIdentifier' => 'actions-document-paste-into',
+            'callbackAction' => 'pasteInto'
+        ],
+        'divider2' => [
+            'type' => 'divider'
+        ],
+        'more' => [
+            'type' => 'submenu',
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.more',
+            'iconIdentifier' => '',
+            'callbackAction' => 'openSubmenu',
+            'childItems' => [
+                'newWizard' => [
+                    'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_newWizard',
+                    'iconIdentifier' => 'actions-page-new',
+                    'callbackAction' => 'newPageWizard',
+                ],
+                'openListModule' => [
+                    'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_db_list',
+                    'iconIdentifier' => 'actions-system-list-open',
+                    'callbackAction' => 'openListModule',
+                ],
+                'mountAsTreeRoot' => [
+                    'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.tempMountPoint',
+                    'iconIdentifier' => 'actions-pagetree-mountroot',
+                    'callbackAction' => 'mountAsTreeRoot',
+                ],
+            ],
+        ],
+        'divider3' => [
+            'type' => 'divider'
+        ],
+        'enable' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:enable',
+            'iconIdentifier' => 'actions-edit-unhide',
+            'callbackAction' => 'enableRecord',
+        ],
+        'disable' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:disable',
+            'iconIdentifier' => 'actions-edit-hide',
+            'callbackAction' => 'disableRecord',
+        ],
+        'delete' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.delete',
+            'iconIdentifier' => 'actions-edit-delete',
+            'callbackAction' => 'deleteRecord',
+        ],
+        'history' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_history',
+            'iconIdentifier' => 'actions-document-history-open',
+            'callbackAction' => 'openHistoryPopUp',
+        ],
+        'clearCache' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.clear_cache',
+            'iconIdentifier' => 'actions-system-cache-clear',
+            'callbackAction' => 'clearCache',
+        ],
+    ];
+
+    /**
+     * Checks if the provider can add items to the menu
+     *
+     * @return bool
+     */
+    public function canHandle(): bool
+    {
+        return $this->table === 'pages';
+    }
+
+    /**
+     * @return int
+     */
+    public function getPriority(): int
+    {
+        return 100;
+    }
+
+    /**
+     * @param string $itemName
+     * @param string $type
+     * @return bool
+     */
+    protected function canRender(string $itemName, string $type): bool
+    {
+        if (in_array($type, ['divider', 'submenu'], true)) {
+            return true;
+        }
+        if (in_array($itemName, $this->disabledItems, true)) {
+            return false;
+        }
+        $canRender = false;
+        switch ($itemName) {
+            case 'view':
+                $canRender = $this->canBeViewed();
+                break;
+            case 'edit':
+                $canRender = $this->canBeEdited();
+                break;
+            case 'new':
+            case 'newWizard':
+                $canRender = $this->canBeCreated();
+                break;
+            case 'info':
+                $canRender = $this->canShowInfo();
+                break;
+            case 'enable':
+                $canRender = $this->canBeEnabled();
+                break;
+            case 'disable':
+                $canRender = $this->canBeDisabled();
+                break;
+            case 'delete':
+                $canRender = $this->canBeDeleted();
+                break;
+            case 'history':
+                $canRender = $this->canShowHistory();
+                break;
+            case 'openListModule':
+                $canRender = $this->canOpenListModule();
+                break;
+            case 'mountAsTreeRoot':
+                $canRender = !$this->isRoot();
+                break;
+            case 'copy':
+                $canRender = $this->canBeCopied();
+                break;
+            case 'copyRelease':
+                $canRender = $this->isRecordInClipboard('copy');
+                break;
+            case 'cut':
+                $canRender = $this->canBeCut();
+                break;
+            case 'cutRelease':
+                $canRender = $this->isRecordInClipboard('cut');
+                break;
+            case 'pasteAfter':
+                $canRender = $this->canBePastedAfter();
+                break;
+            case 'pasteInto':
+                $canRender = $this->canBePastedInto();
+                break;
+            case 'clearCache':
+                $canRender = $this->canClearCache();
+                break;
+        }
+        return $canRender;
+    }
+
+    /**
+     * Saves calculated permissions for a page to speed things up
+     */
+    protected function initPermissions()
+    {
+        $this->pagePermissions = $this->backendUser->calcPerms($this->record);
+    }
+
+    /**
+     * Checks if the user may create pages below the given page
+     *
+     * @return bool
+     */
+    protected function canBeCreated(): bool
+    {
+        return $this->hasPagePermission(Permission::PAGE_NEW);
+    }
+
+    /**
+     * Checks if the user has editing rights
+     *
+     * @return bool
+     */
+    protected function canBeEdited(): bool
+    {
+        if ($this->isRoot()) {
+            return false;
+        }
+        if (isset($GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) {
+            return false;
+        }
+        if ($this->backendUser->isAdmin()) {
+            return true;
+        }
+        if (isset($GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) {
+            return false;
+        }
+        return !$this->isRecordLocked() && $this->hasPagePermission(Permission::PAGE_EDIT);
+    }
+
+    /**
+     * Check if a page is locked
+     *
+     * @return bool
+     */
+    protected function isRecordLocked(): bool
+    {
+        return (bool)$this->record['editlock'];
+    }
+
+    /**
+     * Checks if the page is allowed to can be cut
+     *
+     * @return bool
+     */
+    protected function canBeCut(): bool
+    {
+        return !$this->isWebMount()
+            && $this->canBeEdited()
+            && !$this->isDeletePlaceholder();
+    }
+
+    /**
+     * Checks if the page is allowed to be copied
+     *
+     * @return bool
+     */
+    protected function canBeCopied(): bool
+    {
+        return !$this->isRoot()
+            && !$this->isWebMount()
+            && !$this->isRecordInClipboard('copy')
+            && $this->hasPagePermission(Permission::PAGE_SHOW)
+            && !$this->isDeletePlaceholder();
+    }
+
+    /**
+     * Checks if something can be pasted into the node
+     *
+     * @return bool
+     */
+    protected function canBePastedInto(): bool
+    {
+        $clipboardElementCount = count($this->clipboard->elFromTable($this->table));
+
+        return $clipboardElementCount
+            && $this->canBeCreated()
+            && !$this->isDeletePlaceholder();
+    }
+
+    /**
+     * Checks if something can be pasted after the node
+     *
+     * @return bool
+     */
+    protected function canBePastedAfter(): bool
+    {
+        $clipboardElementCount = count($this->clipboard->elFromTable($this->table));
+        return $clipboardElementCount
+            && $this->canBeCreated()
+            && !$this->isDeletePlaceholder();
+    }
+
+    /**
+     * Checks if the page is allowed to be removed
+     *
+     * @return bool
+     */
+    protected function canBeRemoved(): bool
+    {
+        return !$this->isDeletePlaceholder()
+            && !$this->isRecordLocked()
+            && $this->hasPagePermission(Permission::PAGE_DELETE);
+    }
+
+    /**
+     * Checks if the page is allowed to be viewed in frontend
+     *
+     * @return bool
+     */
+    protected function canBeViewed(): bool
+    {
+        return !$this->isRoot() && !$this->isDeleted();
+    }
+
+    /**
+     * Checks if the page is allowed to show info
+     *
+     * @return bool
+     */
+    protected function canShowInfo(): bool
+    {
+        return !$this->isRoot();
+    }
+
+    /**
+     * Checks if the user has clear cache rights
+     *
+     * @return bool
+     */
+    protected function canClearCache(): bool
+    {
+        return !$this->isRoot()
+            && ($this->backendUser->isAdmin() || $this->backendUser->getTSConfigVal('options.clearCache.pages'));
+    }
+
+    /**
+     * Determines whether this node is deleted.
+     *
+     * @return bool
+     */
+    protected function isDeleted(): bool
+    {
+        return !empty($this->record['deleted']) || $this->isDeletePlaceholder();
+    }
+
+    /**
+     * Returns true if current record is a root page
+     *
+     * @return bool
+     */
+    protected function isRoot()
+    {
+        return (int)$this->identifier === 0;
+    }
+
+    /**
+     * Returns true if current record is a web mount
+     *
+     * @return bool
+     */
+    protected function isWebMount()
+    {
+        return in_array($this->identifier, $this->backendUser->returnWebmounts());
+    }
+
+    /**
+     * @param string $itemName
+     * @return array
+     */
+    protected function getAdditionalAttributes(string $itemName): array
+    {
+        $attributes = [];
+        if ($itemName === 'view') {
+            $attributes += $this->getViewAdditionalAttributes();
+        }
+        if ($itemName === 'delete') {
+            $attributes += $this->getDeleteAdditionalAttributes();
+        }
+        if ($itemName === 'pasteInto') {
+            $attributes += $this->getPasteAdditionalAttributes('into');
+        }
+        if ($itemName === 'pasteAfter') {
+            $attributes += $this->getPasteAdditionalAttributes('after');
+        }
+        return $attributes;
+    }
+
+    /**
+     * @return int
+     */
+    protected function getPreviewPid(): int
+    {
+        return (int)$this->record['uid'];
+    }
+}
diff --git a/typo3/sysext/backend/Classes/ContextMenu/ItemProviders/ProviderInterface.php b/typo3/sysext/backend/Classes/ContextMenu/ItemProviders/ProviderInterface.php
new file mode 100644 (file)
index 0000000..4770f3f
--- /dev/null
@@ -0,0 +1,42 @@
+<?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!
+ */
+
+/**
+ * Interface for context menu items providers
+ */
+interface ProviderInterface
+{
+    /**
+     * @param array $items
+     * @return array
+     */
+    public function addItems(array $items): array;
+
+    /**
+     * Returns the priority of the provider. Higher priority value means provider is executed first
+     *
+     * @return int
+     */
+    public function getPriority(): int;
+
+    /**
+     * Checks if the provider can add items to the menu
+     *
+     * @return bool
+     */
+    public function canHandle(): bool;
+}
diff --git a/typo3/sysext/backend/Classes/ContextMenu/ItemProviders/RecordProvider.php b/typo3/sysext/backend/Classes/ContextMenu/ItemProviders/RecordProvider.php
new file mode 100644 (file)
index 0000000..2ce8607
--- /dev/null
@@ -0,0 +1,648 @@
+<?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\Utility\BackendUtility;
+use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
+use TYPO3\CMS\Core\Type\Bitmask\Permission;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Versioning\VersionState;
+
+/**
+ * Class responsible for providing click menu items for db records which don't have custom provider (as e.g. pages)
+ *
+ */
+class RecordProvider extends AbstractProvider
+{
+    /**
+     * Database record
+     *
+     * @var array
+     */
+    protected $record = [];
+
+    /**
+     * Database record of the page $this->record is placed on
+     *
+     * @var array
+     */
+    protected $pageRecord = [];
+
+    /**
+     * Local cache for the result of BackendUserAuthentication::calcPerms()
+     *
+     * @var int
+     */
+    protected $pagePermissions = 0;
+
+    /**
+     * @var array
+     */
+    protected $itemsConfiguration = [
+        'view' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.view',
+            'iconIdentifier' => 'actions-document-view',
+            'callbackAction' => 'viewRecord'
+        ],
+        'edit' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.edit',
+            'iconIdentifier' => 'actions-open',
+            'callbackAction' => 'editRecord'
+        ],
+        'new' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.new',
+            'iconIdentifier' => 'actions-document-new',
+            'callbackAction' => 'newRecord'
+        ],
+        'info' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.info',
+            'iconIdentifier' => 'actions-document-info',
+            'callbackAction' => 'openInfoPopUp'
+        ],
+        'divider1' => [
+            'type' => 'divider'
+        ],
+        'copy' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.copy',
+            'iconIdentifier' => 'actions-edit-copy',
+            'callbackAction' => 'copy'
+        ],
+        'copyRelease' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.copy',
+            'iconIdentifier' => 'actions-edit-copy-release',
+            'callbackAction' => 'clipboardRelease'
+        ],
+        'cut' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.cut',
+            'iconIdentifier' => 'actions-edit-cut',
+            'callbackAction' => 'cut'
+        ],
+        'cutRelease' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.cut',
+            'iconIdentifier' => 'actions-edit-cut-release',
+            'callbackAction' => 'clipboardRelease'
+        ],
+        'pasteAfter' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.pasteafter',
+            'iconIdentifier' => 'actions-document-paste-after',
+            'callbackAction' => 'pasteAfter'
+        ],
+        'divider2' => [
+            'type' => 'divider'
+        ],
+        'more' => [
+            'type' => 'submenu',
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.more',
+            'iconIdentifier' => '',
+            'callbackAction' => 'openSubmenu',
+            'childItems' => [
+                'newWizard' => [
+                    'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_newWizard',
+                    'iconIdentifier' => 'actions-document-new',
+                    'callbackAction' => 'newContentWizard',
+                ],
+                'openListModule' => [
+                    'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_db_list',
+                    'iconIdentifier' => 'actions-system-list-open',
+                    'callbackAction' => 'openListModule',
+                ],
+            ],
+        ],
+        'divider3' => [
+            'type' => 'divider'
+        ],
+        'enable' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:enable',
+            'iconIdentifier' => 'actions-edit-unhide',
+            'callbackAction' => 'enableRecord',
+        ],
+        'disable' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:disable',
+            'iconIdentifier' => 'actions-edit-hide',
+            'callbackAction' => 'disableRecord',
+        ],
+        'delete' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.delete',
+            'iconIdentifier' => 'actions-edit-delete',
+            'callbackAction' => 'deleteRecord',
+        ],
+        'history' => [
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_history',
+            'iconIdentifier' => 'actions-document-history-open',
+            'callbackAction' => 'openHistoryPopUp',
+        ],
+    ];
+
+    /**
+     * Whether this provider should kick in
+     *
+     * @return bool
+     */
+    public function canHandle(): bool
+    {
+        if (in_array($this->table, ['sys_file', 'sys_filemounts', 'sys_file_storage', 'pages'], true)
+            || strpos($this->table, '-drag') !== false) {
+            return false;
+        }
+        return isset($GLOBALS['TCA'][$this->table]);
+    }
+
+    /**
+     * Initialize db record
+     */
+    protected function initialize()
+    {
+        parent::initialize();
+        $this->record = BackendUtility::getRecordWSOL($this->table, $this->identifier);
+    }
+
+    /**
+     * Priority is set to lower then default value, in order to skip this provider if there is less generic provider available.
+     *
+     * @return int
+     */
+    public function getPriority(): int
+    {
+        return 60;
+    }
+
+    /**
+     * This provider works as a fallback if there is no provider dedicated for certain table, thus it's only kicking in when $items are empty.
+     *
+     * @param array $items
+     * @return array
+     */
+    public function addItems(array $items): array
+    {
+        if (!empty($items)) {
+            return $items;
+        }
+        $this->initialize();
+        return $this->prepareItems($this->itemsConfiguration);
+    }
+
+    /**
+     * Whether a given item can be rendered (e.g. user has enough permissions)
+     *
+     * @param string $itemName
+     * @param string $type
+     * @return bool
+     */
+    protected function canRender(string $itemName, string $type): bool
+    {
+        if (in_array($type, ['divider', 'submenu'], true)) {
+            return true;
+        }
+        if (in_array($itemName, $this->disabledItems, true)) {
+            return false;
+        }
+        $canRender = false;
+        switch ($itemName) {
+            case 'view':
+                $canRender = $this->canBeViewed();
+                break;
+            case 'edit':
+            case 'new':
+                $canRender = $this->canBeEdited();
+                break;
+            case 'newWizard':
+                $canRender = $this->canOpenNewCEWizard();
+                break;
+            case 'info':
+                $canRender = $this->canShowInfo();
+                break;
+            case 'enable':
+                $canRender = $this->canBeEnabled();
+                break;
+            case 'disable':
+                $canRender = $this->canBeDisabled();
+                break;
+            case 'delete':
+                $canRender = $this->canBeDeleted();
+                break;
+            case 'history':
+                $canRender = $this->canShowHistory();
+                break;
+            case 'openListModule':
+                $canRender = $this->canOpenListModule();
+                break;
+            case 'copy':
+                $canRender = $this->canBeCopied();
+                break;
+            case 'copyRelease':
+                $canRender = $this->isRecordInClipboard('copy');
+                break;
+            case 'cut':
+                $canRender = $this->canBeCut();
+                break;
+            case 'cutRelease':
+                $canRender = $this->isRecordInClipboard('cut');
+                break;
+            case 'pasteAfter':
+                $canRender = $this->canBePastedAfter();
+                break;
+        }
+        return $canRender;
+    }
+
+    /**
+     * Saves calculated permissions for a page containing given record, to speed things up
+     */
+    protected function initPermissions()
+    {
+        $this->pageRecord = BackendUtility::getRecord('pages', $this->record['pid']);
+        $this->pagePermissions = $this->backendUser->calcPerms($this->pageRecord);
+    }
+
+    /**
+     * Returns true if a current user have access to given permission
+     *
+     * @see BackendUserAuthentication::doesUserHaveAccess()
+     * @param int $permission
+     * @return bool
+     */
+    protected function hasPagePermission(int $permission): bool
+    {
+        return $this->backendUser->isAdmin() || ($this->pagePermissions & $permission) == $permission;
+    }
+
+    /**
+     * Additional attributes for JS
+     *
+     * @param string $itemName
+     * @return array
+     */
+    protected function getAdditionalAttributes(string $itemName): array
+    {
+        $attributes = [];
+        if ($itemName === 'view') {
+            $attributes += $this->getViewAdditionalAttributes();
+        }
+        if ($itemName === 'newWizard' && $this->table === 'tt_content') {
+            $tsConfig = BackendUtility::getModTSconfig($this->record['pid'], 'mod');
+            $moduleName = isset($tsConfig['properties']['newContentElementWizard.']['override'])
+                ? $tsConfig['properties']['newContentElementWizard.']['override']
+                : 'new_content_element';
+            $urlParameters = [
+                'id' => $this->record['pid'],
+                'sys_language_uid' => $this->record['sys_language_uid'],
+                'colPos' => $this->record['colPos'],
+                'uid_pid' => -$this->record['uid']
+            ];
+            $url = BackendUtility::getModuleUrl($moduleName, $urlParameters);
+            $attributes += [
+                'data-new-wizard-url' => htmlspecialchars($url)
+            ];
+        }
+        if ($itemName === 'delete') {
+            $attributes += $this->getDeleteAdditionalAttributes();
+        }
+        if ($itemName === 'openListModule') {
+            $attributes += [
+                'data-page-uid' => $this->record['pid']
+            ];
+        }
+        if ($itemName === 'pasteAfter') {
+            $attributes += $this->getPasteAdditionalAttributes('after');
+        }
+        return $attributes;
+    }
+
+    /**
+     * Additional attributes for the 'view' item
+     *
+     * @return array
+     */
+    protected function getViewAdditionalAttributes(): array
+    {
+        $attributes = [];
+        $viewLink = $this->getViewLink();
+        if ($viewLink) {
+            $attributes += [
+                'data-preview-url' => htmlspecialchars($viewLink),
+            ];
+        }
+        return $attributes;
+    }
+
+    /**
+     * Additional attributes for the pasteInto and pasteAfter items
+     *
+     * @param string $type "after" or "into"
+     * @return array
+     */
+    protected function getPasteAdditionalAttributes(string $type): array
+    {
+        $attributes = [];
+        if ($this->backendUser->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) {
+            $selItem = $this->clipboard->getSelectedRecord();
+            $title = $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste');
+
+            $confirmMessage = sprintf(
+                $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:mess.'
+                    . ($this->clipboard->currentMode() === 'copy' ? 'copy' : 'move') . '_' . $type),
+                GeneralUtility::fixed_lgd_cs($selItem['_RECORD_TITLE'], $this->backendUser->uc['titleLen']),
+                GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($this->table, $this->record), $this->backendUser->uc['titleLen'])
+            );
+            $attributes += [
+                'data-title' => htmlspecialchars($title),
+                'data-message' => htmlspecialchars($confirmMessage)
+            ];
+        }
+        return $attributes;
+    }
+
+    /**
+     * Additional data for a "delete" action (confirmation modal title and message)
+     *
+     * @return array
+     */
+    protected function getDeleteAdditionalAttributes(): array
+    {
+        $attributes = [];
+        if ($this->backendUser->jsConfirmation(JsConfirmation::DELETE)) {
+            $recordTitle = GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($this->table, $this->record), $this->backendUser->uc['titleLen']);
+
+            $title = $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_mod_web_list.xlf:delete');
+            $confirmMessage = sprintf(
+                $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:mess.delete'),
+                $recordTitle
+            );
+            $confirmMessage .= BackendUtility::referenceCount(
+                $this->table,
+                $this->record['uid'],
+                ' ' . $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord')
+            );
+            $confirmMessage .= BackendUtility::translationCount(
+                $this->table,
+                $this->record['uid'],
+                ' ' . $this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
+            );
+            $attributes += [
+                'data-title' => htmlspecialchars($title),
+                'data-message' => htmlspecialchars($confirmMessage)
+            ];
+        }
+        return $attributes;
+    }
+
+    /**
+     * Returns id of the Page used for preview
+     *
+     * @return int
+     */
+    protected function getPreviewPid(): int
+    {
+        return (int)$this->record['pid'];
+    }
+
+    /**
+     * Returns the view link
+     *
+     * @return string
+     */
+    protected function getViewLink(): string
+    {
+        $javascriptLink = BackendUtility::viewOnClick($this->getPreviewPid());
+        $extractedLink = '';
+        if (preg_match('/window\\.open\\(\'([^\']+)\'/i', $javascriptLink, $match)) {
+            // Clean JSON-serialized ampersands ('&')
+            // @see GeneralUtility::quoteJSvalue()
+            $extractedLink = json_decode('"' . trim($match[1], '"') . '"');
+        }
+        return $extractedLink;
+    }
+
+    /**
+     * Checks if the page is allowed to show info
+     *
+     * @return bool
+     */
+    protected function canShowInfo(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Checks if the page is allowed to show info
+     *
+     * @return bool
+     */
+    protected function canShowHistory(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Checks if the record can be previewed in frontend
+     *
+     * @return bool
+     */
+    protected function canBeViewed(): bool
+    {
+        return $this->table === 'tt_content';
+    }
+
+    /**
+     * Whether a record can be edited
+     *
+     * @return bool
+     */
+    protected function canBeEdited(): bool
+    {
+        if (isset($GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) {
+            return false;
+        }
+        if ($this->backendUser->isAdmin()) {
+            return true;
+        }
+        if (isset($GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) {
+            return false;
+        }
+
+        $access = !$this->isRecordLocked()
+            && $this->backendUser->check('tables_modify', $this->table)
+            && $this->hasPagePermission(Permission::CONTENT_EDIT);
+        return $access;
+    }
+
+    /**
+     * Checks if the user has the right to delete the page
+     *
+     * @return bool
+     */
+    protected function canBeDeleted(): bool
+    {
+        $disableDeleteTS = $this->backendUser->getTSConfig('options.disableDelete');
+        $disableDelete = (bool) trim(isset($disableDeleteTS['properties'][$this->table]) ? $disableDeleteTS['properties'][$this->table] : (string)$disableDeleteTS['value']);
+        return !$disableDelete && $this->canBeEdited();
+    }
+
+    /**
+     * Returns true if current record can be unhidden/enabled
+     *
+     * @return bool
+     */
+    protected function canBeEnabled(): bool
+    {
+        return $this->hasDisableColumnWithValue(1) && $this->canBeEdited();
+    }
+
+    /**
+     * Returns true if current record can be hidden
+     *
+     * @return bool
+     */
+    protected function canBeDisabled(): bool
+    {
+        return $this->hasDisableColumnWithValue(0) && $this->canBeEdited();
+    }
+
+    /**
+     * Returns true new content element wizard can be shown
+     *
+     * @return bool
+     */
+    protected function canOpenNewCEWizard(): bool
+    {
+        $tsConfig = BackendUtility::getModTSconfig($this->record['pid'], 'mod.web_layout');
+        $wizardEnabled = true;
+        if (isset($tsConfig['properties']['disableNewContentElementWizard'])) {
+            $wizardEnabled = false;
+        }
+        return $this->table === 'tt_content' && $wizardEnabled && $this->canBeEdited();
+    }
+
+    /**
+     * @return bool
+     */
+    protected function canOpenListModule(): bool
+    {
+        return $this->backendUser->check('modules', 'web_list');
+    }
+
+    /**
+     * @return bool
+     */
+    protected function canBeCopied(): bool
+    {
+        return !$this->isRecordInClipboard('copy')
+            && !$this->isRecordATranslation();
+    }
+
+    /**
+     * @return bool
+     */
+    protected function canBeCut(): bool
+    {
+        return !$this->isRecordInClipboard('cut')
+            && $this->canBeEdited()
+            && !$this->isRecordATranslation();
+    }
+
+    /**
+     * Paste after is only shown for records from the same table (comparing record in clipboard and record clicked)
+     *
+     * @return bool
+     */
+    protected function canBePastedAfter(): bool
+    {
+        $clipboardElementCount = count($this->clipboard->elFromTable($this->table));
+
+        return $clipboardElementCount
+            && $this->backendUser->check('tables_modify', $this->table)
+            && $this->hasPagePermission(Permission::CONTENT_EDIT);
+    }
+
+    /**
+     * Checks if table have "disable" column (e.g. "hidden"), if user has access to this column
+     * and if it contains given value
+     *
+     * @param int $value
+     * @return bool
+     */
+    protected function hasDisableColumnWithValue(int $value): bool
+    {
+        if (isset($GLOBALS['TCA'][$this->table]['ctrl']['enablecolumns']['disabled'])) {
+            $hiddenFieldName = $GLOBALS['TCA'][$this->table]['ctrl']['enablecolumns']['disabled'];
+            if (
+                $hiddenFieldName !== '' && !empty($GLOBALS['TCA'][$this->table]['columns'][$hiddenFieldName]['exclude'])
+                && $this->backendUser->check('non_exclude_fields', $this->table . ':' . $hiddenFieldName)
+            ) {
+                return (int)$this->record[$hiddenFieldName] === (int)$value;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Record is locked if page is locked or page is not locked but record is
+     *
+     * @return bool
+     */
+    protected function isRecordLocked(): bool
+    {
+        return (int)$this->pageRecord['editlock'] === 1
+            || isset($GLOBALS['TCA'][$this->table]['ctrl']['editlock'])
+            && (int)$this->record[$GLOBALS['TCA'][$this->table]['ctrl']['editlock']] === 1;
+    }
+
+    /**
+     * Returns true is a current record is a delete placeholder
+     *
+     * @return bool
+     */
+    protected function isDeletePlaceholder(): bool
+    {
+        if (!isset($this->record['t3ver_state'])) {
+            return false;
+        }
+        return VersionState::cast($this->record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER);
+    }
+
+    /**
+     * Checks if current record is in the "normal" pad of the clipboard
+     *
+     * @param string $mode "copy", "cut" or '' for any mode
+     * @return bool
+     */
+    protected function isRecordInClipboard(string $mode = ''): bool
+    {
+        $isSelected = '';
+        if ($this->clipboard->current === 'normal') {
+            $isSelected = $this->clipboard->isSelected($this->table, $this->record['uid']);
+        }
+        return $mode === '' ? !empty($isSelected) : $isSelected === $mode;
+    }
+
+    /**
+     * Returns true is a record ia a translation
+     *
+     * @return bool
+     */
+    protected function isRecordATranslation(): bool
+    {
+        return BackendUtility::isTableLocalizable($this->table) && (int)$this->record[$GLOBALS['TCA'][$this->table]['ctrl']['transOrigPointerField']] !== 0;
+    }
+
+    /**
+     * @return string
+     */
+    protected function getIdentifier(): string
+    {
+        return $this->record['uid'];
+    }
+}
diff --git a/typo3/sysext/backend/Classes/ContextMenu/Pagetree/ContextMenuDataProvider.php b/typo3/sysext/backend/Classes/ContextMenu/Pagetree/ContextMenuDataProvider.php
deleted file mode 100644 (file)
index 497b45b..0000000
+++ /dev/null
@@ -1,334 +0,0 @@
-<?php
-namespace TYPO3\CMS\Backend\ContextMenu\Pagetree;
-
-/*
- * 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\ContextMenu\ContextMenuAction;
-use TYPO3\CMS\Backend\ContextMenu\ContextMenuActionCollection;
-use TYPO3\CMS\Backend\Tree\TreeNode;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Imaging\Icon;
-use TYPO3\CMS\Core\Imaging\IconFactory;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * Context Menu Data Provider for the Page Tree
- */
-class ContextMenuDataProvider
-{
-    /**
-     * List of actions that are generally disabled
-     *
-     * @var array
-     */
-    protected $disableItems = [];
-
-    /**
-     * Context Menu Type (e.g. table.pages, table.tt_content)
-     *
-     * @var string
-     */
-    protected $contextMenuType = '';
-
-    /**
-     * Old Context Menu Options (access mapping)
-     *
-     * Note: Only option with different namings are mapped!
-     *
-     * @var array
-     */
-    protected $legacyContextMenuMapping = [
-        'hide' => 'disable',
-        'paste' => 'pasteInto,pasteAfter',
-        'mount_as_treeroot' => 'mountAsTreeroot'
-    ];
-
-    /**
-     * Returns the context menu type
-     *
-     * @return string
-     */
-    public function getContextMenuType()
-    {
-        return $this->contextMenuType;
-    }
-
-    /**
-     * Sets the context menu type
-     *
-     * @param string $contextMenuType
-     * @return void
-     */
-    public function setContextMenuType($contextMenuType)
-    {
-        $this->contextMenuType = $contextMenuType;
-    }
-
-    /**
-     * Returns the configuration of the specified context menu type
-     *
-     * @return array
-     */
-    protected function getConfiguration()
-    {
-        $contextMenuActions = $this->getBackendUser()
-            ->getTSConfig('options.contextMenu.' . $this->contextMenuType . '.items');
-        return $contextMenuActions['properties'];
-    }
-
-    /**
-     * Evaluates a given display condition and returns TRUE if the condition matches
-     *
-     * Examples:
-     * getContextInfo|inCutMode:1 || isInCopyMode:1
-     * isLeafNode:1
-     * isLeafNode:1 && isInCutMode:1
-     *
-     * @param TreeNode $node
-     * @param string $displayCondition
-     * @return bool
-     */
-    protected function evaluateDisplayCondition(TreeNode $node, $displayCondition)
-    {
-        if ($displayCondition === '') {
-            return true;
-        }
-        // Parse condition string
-        $conditions = [];
-        preg_match_all('/(.+?)(>=|<=|!=|=|>|<)(.+?)(\\|\\||&&|$)/is', $displayCondition, $conditions);
-        $lastResult = false;
-        $chainType = '';
-        $amountOfConditions = count($conditions[0]);
-        for ($i = 0; $i < $amountOfConditions; ++$i) {
-            // Check method for existence
-            $method = trim($conditions[1][$i]);
-            list($method, $index) = explode('|', $method);
-            if (!method_exists($node, $method)) {
-                continue;
-            }
-            // Fetch compare value
-            $returnValue = call_user_func([$node, $method]);
-            if (is_array($returnValue)) {
-                $returnValue = $returnValue[$index];
-            }
-            // Compare fetched and expected values
-            $operator = trim($conditions[2][$i]);
-            $expected = trim($conditions[3][$i]);
-            if ($operator === '=') {
-                $returnValue = $returnValue == $expected;
-            } elseif ($operator === '>') {
-                $returnValue = $returnValue > $expected;
-            } elseif ($operator === '<') {
-                $returnValue = $returnValue < $expected;
-            } elseif ($operator === '>=') {
-                $returnValue = $returnValue >= $expected;
-            } elseif ($operator === '<=') {
-                $returnValue = $returnValue <= $expected;
-            } elseif ($operator === '!=') {
-                $returnValue = $returnValue != $expected;
-            } else {
-                $returnValue = false;
-                $lastResult = false;
-            }
-            // Chain last result and the current if requested
-            if ($chainType === '||') {
-                $lastResult = $lastResult || $returnValue;
-            } elseif ($chainType === '&&') {
-                $lastResult = $lastResult && $returnValue;
-            } else {
-                $lastResult = $returnValue;
-            }
-            // Save chain type for the next condition
-            $chainType = trim($conditions[4][$i]);
-        }
-        return $lastResult;
-    }
-
-    /**
-     * Returns the next context menu level
-     *
-     * @param array $actions
-     * @param TreeNode $node
-     * @param int $level
-     * @return ContextMenuActionCollection
-     */
-    protected function getNextContextMenuLevel(array $actions, TreeNode $node, $level = 0)
-    {
-        /** @var $actionCollection ContextMenuActionCollection */
-        $actionCollection = GeneralUtility::makeInstance(ContextMenuActionCollection::class);
-        /** @var $iconFactory IconFactory */
-        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-        if ($level > 5) {
-            return $actionCollection;
-        }
-        $type = '';
-        foreach ($actions as $index => $actionConfiguration) {
-            if (substr($index, -1) !== '.') {
-                $type = $actionConfiguration;
-                if ($type !== 'DIVIDER') {
-                    continue;
-                }
-            }
-            if (!in_array($type, ['DIVIDER', 'SUBMENU', 'ITEM'], true)) {
-                continue;
-            }
-            /** @var $action ContextMenuAction */
-            $action = GeneralUtility::makeInstance(ContextMenuAction::class);
-            $action->setId($index);
-            if ($type === 'DIVIDER') {
-                $action->setType('divider');
-            } else {
-                if (in_array($actionConfiguration['name'], $this->disableItems, true)
-                    || isset($actionConfiguration['displayCondition'])
-                    && trim($actionConfiguration['displayCondition']) !== ''
-                    && !$this->evaluateDisplayCondition($node, $actionConfiguration['displayCondition'])
-                ) {
-                    unset($action);
-                    continue;
-                }
-                $label = htmlspecialchars($this->getLanguageService()->sL($actionConfiguration['label']));
-                if ($type === 'SUBMENU') {
-                    $action->setType('submenu');
-                    $action->setChildActions($this->getNextContextMenuLevel($actionConfiguration, $node, $level + 1));
-                } else {
-                    $action->setType('action');
-                    $action->setCallbackAction($actionConfiguration['callbackAction']);
-                    if (is_array($actionConfiguration['customAttributes.'])) {
-                        if (!empty($actionConfiguration['customAttributes.']['contentUrl'])) {
-                            $actionConfiguration['customAttributes.']['contentUrl'] = $this
-                                ->replaceModuleTokenInContentUrl(
-                                    $actionConfiguration['customAttributes.']['contentUrl']
-                                );
-                        }
-                        $action->setCustomAttributes($actionConfiguration['customAttributes.']);
-                    }
-                }
-                $action->setLabel($label);
-                if (!isset($actionConfiguration['iconName'])) {
-                    $actionConfiguration['iconName'] = 'miscellaneous-placeholder';
-                }
-                $action->setIcon($iconFactory->getIcon($actionConfiguration['iconName'], Icon::SIZE_SMALL)->render());
-            }
-            $actionCollection->offsetSet($level . (int)$index, $action);
-        }
-        $actionCollection->ksort();
-        return $actionCollection;
-    }
-
-    /**
-     * Add the CSRF token to the module URL if a "M" parameter is found
-     *
-     * @param string $contentUrl
-     * @return string
-     */
-    protected function replaceModuleTokenInContentUrl($contentUrl)
-    {
-        $parsedUrl = parse_url($contentUrl);
-        parse_str($parsedUrl['query'], $urlParameters);
-        if (!empty($urlParameters['M'])) {
-            $moduleName = $urlParameters['M'];
-            unset($urlParameters['M']);
-            $contentUrl = BackendUtility::getModuleUrl($moduleName, $urlParameters);
-        }
-        return $contentUrl;
-    }
-
-    /**
-     * Fetches the items that should be disabled from the context menu
-     *
-     * @return array
-     */
-    protected function getDisableActions()
-    {
-        $tsConfig = $this->getBackendUser()
-            ->getTSConfig('options.contextMenu.' . $this->getContextMenuType() . '.disableItems');
-        $disableItems = [];
-        if (trim($tsConfig['value']) !== '') {
-            $disableItems = GeneralUtility::trimExplode(',', $tsConfig['value']);
-        }
-        $tsConfig = $this->getBackendUser()->getTSConfig('options.contextMenu.pageTree.disableItems');
-        $oldDisableItems = [];
-        if (trim($tsConfig['value']) !== '') {
-            $oldDisableItems = GeneralUtility::trimExplode(',', $tsConfig['value']);
-        }
-        $additionalItems = [];
-        foreach ($oldDisableItems as $item) {
-            if (empty($this->legacyContextMenuMapping[$item])) {
-                $additionalItems[] = $item;
-                continue;
-            }
-            if (strpos($this->legacyContextMenuMapping[$item], ',')) {
-                $actions = GeneralUtility::trimExplode(',', $this->legacyContextMenuMapping[$item]);
-                $additionalItems = array_merge($additionalItems, $actions);
-            } else {
-                $additionalItems[] = $item;
-            }
-        }
-        $disableItems = array_merge($disableItems, $additionalItems);
-
-        // Further manipulation of disableItems array via hook
-        // @internal: This is an internal hook for extension impexp only, this hook may change without further notice
-        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['backend']['contextMenu']['disableItems'])
-            && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['backend']['contextMenu']['disableItems'])
-        ) {
-            $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['backend']['contextMenu']['disableItems'];
-            foreach ($hooks as $hook) {
-                $parameterArray = [
-                    'disableItems' => &$disableItems,
-                ];
-                $null = null;
-                GeneralUtility::callUserFunction($hook, $parameterArray, $null);
-            }
-        }
-
-        return $disableItems;
-    }
-
-    /**
-     * Returns the actions for the node
-     * @param TreeNode $node
-     *
-     * @return array|ContextMenuActionCollection
-     */
-    public function getActionsForNode(TreeNode $node)
-    {
-        $this->disableItems = $this->getDisableActions();
-        $configuration = $this->getConfiguration();
-        $contextMenuActions = [];
-        if (is_array($configuration)) {
-            $contextMenuActions = $this->getNextContextMenuLevel($configuration, $node);
-        }
-        return $contextMenuActions;
-    }
-
-    /**
-     * Returns LanguageService
-     *
-     * @return \TYPO3\CMS\Lang\LanguageService
-     */
-    protected function getLanguageService()
-    {
-        return $GLOBALS['LANG'];
-    }
-
-    /**
-     * Returns the current BE user.
-     *
-     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
-     */
-    protected function getBackendUser()
-    {
-        return $GLOBALS['BE_USER'];
-    }
-}
diff --git a/typo3/sysext/backend/Classes/ContextMenu/Pagetree/Extdirect/ContextMenuConfiguration.php b/typo3/sysext/backend/Classes/ContextMenu/Pagetree/Extdirect/ContextMenuConfiguration.php
deleted file mode 100644 (file)
index fb6964d..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-namespace TYPO3\CMS\Backend\ContextMenu\Pagetree\Extdirect;
-
-/*
- * 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\ContextMenu\ContextMenuActionCollection;
-use TYPO3\CMS\Backend\ContextMenu\Pagetree\ContextMenuDataProvider;
-use TYPO3\CMS\Backend\Tree\Pagetree\Commands;
-use TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNode;
-use TYPO3\CMS\Backend\Tree\TreeNode;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * Context Menu of the Page Tree
- */
-class ContextMenuConfiguration
-{
-    /**
-     * Data Provider
-     *
-     * @var ContextMenuDataProvider
-     */
-    protected $dataProvider = null;
-
-    /**
-     * @param ContextMenuDataProvider $dataProvider
-     * @return void
-     */
-    public function setDataProvider(ContextMenuDataProvider $dataProvider)
-    {
-        $this->dataProvider = $dataProvider;
-    }
-
-    /**
-     * @return ContextMenuDataProvider
-     */
-    public function getDataProvider()
-    {
-        return $this->dataProvider;
-    }
-    /**
-     * Sets the data provider
-     *
-     * @return void
-     */
-    protected function initDataProvider()
-    {
-        $dataProvider = GeneralUtility::makeInstance(ContextMenuDataProvider::class);
-        $this->setDataProvider($dataProvider);
-    }
-
-    /**
-     * Returns the actions for the given node information's
-     *
-     * @param \stdClass $nodeData
-     * @return array
-     */
-    public function getActionsForNodeArray($nodeData)
-    {
-        $node = GeneralUtility::makeInstance(PagetreeNode::class, (array)$nodeData);
-        $node->setRecord(Commands::getNodeRecord($node->getId()));
-        $this->initDataProvider();
-        $this->dataProvider->setContextMenuType('table.' . $node->getType());
-        $actionCollection = $this->dataProvider->getActionsForNode($node);
-        $actions = [];
-        if ($actionCollection instanceof ContextMenuActionCollection) {
-            $actions = $actionCollection->toArray();
-        }
-        return $actions;
-    }
-
-    /**
-     * Unused for this implementation
-     *
-     * @see getActionsForNodeArray()
-     * @param TreeNode $node
-     * @return array
-     */
-    public function getActionsForNode(TreeNode $node)
-    {
-    }
-}
index ba7be90..a8570b0 100644 (file)
@@ -148,6 +148,9 @@ class BackendController
         // load Modals
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
 
+        // load ContextMenu
+        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
+
         // load the storage API and fill the UC into the PersistentStorage, so no additional AJAX call is needed
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Storage', 'function(Storage) {
                        Storage.Persistent.load(' . json_encode($this->getBackendUser()->uc) . ');
@@ -407,6 +410,8 @@ class BackendController
             $this->pageRenderer->addInlineSetting('RecordHistory', 'moduleUrl', BackendUtility::getModuleUrl('record_history'));
             $this->pageRenderer->addInlineSetting('NewRecord', 'moduleUrl', BackendUtility::getModuleUrl('db_new'));
             $this->pageRenderer->addInlineSetting('FormEngine', 'moduleUrl', BackendUtility::getModuleUrl('record_edit'));
+            $this->pageRenderer->addInlineSetting('RecordCommit', 'moduleUrl', BackendUtility::getModuleUrl('tce_db'));
+            $this->pageRenderer->addInlineSetting('WebLayout', 'moduleUrl', BackendUtility::getModuleUrl('web_layout'));
         }
     }
 
diff --git a/typo3/sysext/backend/Classes/Controller/ClickMenuController.php b/typo3/sysext/backend/Classes/Controller/ClickMenuController.php
deleted file mode 100644 (file)
index dc689a1..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-namespace TYPO3\CMS\Backend\Controller;
-
-/*
- * 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 Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
-use TYPO3\CMS\Backend\ClickMenu\ClickMenu;
-use TYPO3\CMS\Backend\Clipboard\Clipboard;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * Script Class for the Context Sensitive Menu in TYPO3 (rendered in top frame, normally writing content dynamically to list frames).
- * @see \TYPO3\CMS\Backend\Template\DocumentTemplate::getContextMenuCode()
- */
-class ClickMenuController
-{
-    /**
-     * Constructor
-     */
-    public function __construct()
-    {
-        $this->getLanguageService()->includeLLFile('EXT:lang/Resources/Private/Language/locallang_misc.xlf');
-        $GLOBALS['SOBE'] = $this;
-        // Setting pseudo module name
-        $this->MCONF['name'] = 'xMOD_alt_clickmenu.php';
-    }
-
-    /**
-     * this is an intermediate clickmenu handler
-     *
-     * @param ServerRequestInterface $request
-     * @param ResponseInterface $response
-     * @return ResponseInterface
-     */
-    public function getContextMenuAction(ServerRequestInterface $request, ResponseInterface $response)
-    {
-        /** @var Clipboard $clipObj */
-        $clipObj = GeneralUtility::makeInstance(Clipboard::class);
-        $clipObj->initializeClipboard();
-        // This locks the clipboard to the Normal for this request.
-        $clipObj->lockToNormal();
-        // Update clipboard if some actions are sent.
-        $clipObj->setCmd($request->getQueryParams()['CB']);
-        $clipObj->cleanCurrent();
-        // Saves
-        $clipObj->endClipboard();
-        // Create clickmenu object
-        $clickMenu = GeneralUtility::makeInstance(ClickMenu::class);
-        // Set internal vars in clickmenu object:
-        $clickMenu->clipObj = $clipObj;
-        // Setting internal array of classes for extending the clickmenu:
-        $clickMenu->extClassArray = $GLOBALS['TBE_MODULES_EXT']['xMOD_alt_clickmenu']['extendCMclasses'];
-
-        $content = $clickMenu->init();
-        if (!is_array($content)) {
-            $content = [];
-        }
-        $response->getBody()->write(json_encode($content));
-        return $response;
-    }
-
-    /**
-     * Returns LanguageService
-     *
-     * @return \TYPO3\CMS\Lang\LanguageService
-     */
-    protected function getLanguageService()
-    {
-        return $GLOBALS['LANG'];
-    }
-}
index f2b17b6..2899feb 100644 (file)
@@ -165,7 +165,7 @@ class NewContentElementController extends AbstractModule
         // We keep this here in case somebody relies on it in a hook or alike
         $this->doc = GeneralUtility::makeInstance(DocumentTemplate::class);
         // Setting up the context sensitive menu:
-        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
         // Getting the current page and receiving access information (used in main())
         $perms_clause = $this->getBackendUser()->getPagePermsClause(1);
         $this->pageInfo = BackendUtility::readPageAccess($this->id, $perms_clause);
diff --git a/typo3/sysext/backend/Classes/Controller/ContextMenuController.php b/typo3/sysext/backend/Classes/Controller/ContextMenuController.php
new file mode 100644 (file)
index 0000000..a9fa905
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Backend\Controller;
+
+/*
+ * 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 Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\ContextMenu\ContextMenu;
+use TYPO3\CMS\Backend\Clipboard\Clipboard;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Lang\LanguageService;
+
+/**
+ * Script Class for the Context Sensitive Menu in TYPO3
+ */
+class ContextMenuController
+{
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->getLanguageService()->includeLLFile('EXT:lang/Resources/Private/Language/locallang_misc.xlf');
+    }
+
+    /**
+     * Renders a context menu
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function getContextMenuAction(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
+    {
+        $contextMenu = GeneralUtility::makeInstance(ContextMenu::class);
+
+        $params = $request->getQueryParams();
+        $context = isset($params['context']) ? $params['context'] : '';
+        $items = $contextMenu->getItems($params['table'], $params['uid'], $context);
+        if (!is_array($items)) {
+            $items = [];
+        }
+        $response->getBody()->write(json_encode($items));
+        return $response;
+    }
+
+    /**
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function clipboardAction(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
+    {
+        /** @var Clipboard $clipboard */
+        $clipboard = GeneralUtility::makeInstance(Clipboard::class);
+        $clipboard->initializeClipboard();
+        $clipboard->lockToNormal();
+
+        $clipboard->setCmd($request->getQueryParams()['CB']);
+        $clipboard->cleanCurrent();
+
+        $clipboard->endClipboard();
+        $response->getBody()->write(json_encode([]));
+        return $response;
+    }
+
+    /**
+     * Returns LanguageService
+     *
+     * @return LanguageService
+     */
+    protected function getLanguageService(): LanguageService
+    {
+        return $GLOBALS['LANG'];
+    }
+}
index a7b280d..02ee7c9 100644 (file)
@@ -796,7 +796,7 @@ class EditDocumentController extends AbstractModule
             $javascript . $previewCode
         );
         // Setting up the context sensitive menu:
-        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
 
         $this->emitFunctionAfterSignal(__FUNCTION__);
     }
index 17d7682..694657e 100644 (file)
@@ -125,7 +125,7 @@ class CreateFolderController extends AbstractModule
             'combined_identifier' => $this->folderObject->getCombinedIdentifier(),
         ];
         $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pathInfo);
-        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
         $this->moduleTemplate->addJavaScriptCode(
             'CreateFolderInlineJavaScript',
             'var path = "' . $this->target . '";
index e257284..31c45f2 100644 (file)
@@ -109,7 +109,7 @@ class FileUploadController extends AbstractModule
         }
 
         // Setting up the context sensitive menu
-        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
 
         // building pathInfo for metaInformation
         $pathInfo = [
index ead4607..d0122d6 100644 (file)
@@ -118,7 +118,7 @@ class RenameFileController extends AbstractModule
         $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pathInfo);
 
         // Setting up the context sensitive menu
-        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
 
         // Add javaScript
         $this->moduleTemplate->addJavaScriptCode(
index 2087879..73e45c3 100644 (file)
@@ -136,7 +136,7 @@ class ReplaceFileController extends AbstractModule
             'combined_identifier' => $this->fileOrFolderObject->getCombinedIdentifier(),
         ];
         $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pathInfo);
-        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
         $this->moduleTemplate->addJavaScriptCode(
             'ReplaceFileOnlineJavaScript',
             'function backToList() {top.goToModule("file_FilelistList");}'
index 2e8c852..d117353 100644 (file)
@@ -152,7 +152,7 @@ class FileSystemNavigationFrameController
 
         // Adding javascript for drag & drop activation and highlighting
         $pageRenderer = $this->moduleTemplate->getPageRenderer();
-        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
         $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LegacyTree', 'function() {
             DragDrop.table = "folders";
                        Tree.registerDragDropHandlers();
index b5c5490..e460e7b 100644 (file)
@@ -184,7 +184,7 @@ class NewRecordController extends AbstractModule
         $this->returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
         $this->pagesOnly = GeneralUtility::_GP('pagesOnly');
         // Setting up the context sensitive menu:
-        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
         $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
         // Creating content
         $this->content = '';
index 8de4e4f..d2f0c41 100644 (file)
@@ -825,7 +825,7 @@ class PageLayoutController
     public function renderContent()
     {
         $this->moduleTemplate->getPageRenderer()->loadJquery();
-        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
         /** @var $dbList \TYPO3\CMS\Backend\View\PageLayoutView */
         $dbList = GeneralUtility::makeInstance(PageLayoutView::class);
         $dbList->thumbs = $this->imagemode;
index ca5a066..c80b755 100644 (file)
@@ -85,7 +85,7 @@ class OuterWrapContainer extends AbstractContainer
                 $pageTitle = sprintf($label, $tableTitle, $pageTitle);
             }
         } else {
-            $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid'], 1, '', '+copy,info,edit,view');
+            $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']);
             $newOrUid = ' <span class="typo3-TCEforms-recUid">[' . htmlspecialchars($row['uid']) . ']</span>';
 
             // @todo: getRecordTitlePrep applies an htmlspecialchars here
index 58f48c0..8452515 100644 (file)
@@ -90,7 +90,7 @@ class MetaInformation
     }
 
     /**
-     * Setting page icon with clickMenu + uid for docheader
+     * Setting page icon with context menu + uid for docheader
      *
      * @return string Record info
      */
@@ -105,7 +105,7 @@ class MetaInformation
         $uid = '';
         $title = '';
         $additionalInfo = (!empty($pageRecord['_additional_info']) ? $pageRecord['_additional_info'] : '');
-        // Add icon with clickMenu, etc:
+        // Add icon with context menu, etc:
         // If there IS a real page
         if (is_array($pageRecord) && $pageRecord['uid']) {
             $toolTip = BackendUtility::getRecordToolTip($pageRecord, 'pages');
@@ -134,7 +134,7 @@ class MetaInformation
                         Icon::SIZE_SMALL
                     )->render() . '</span>';
                 }
-                $theIcon = BackendUtility::wrapClickMenuOnIcon($iconImg, $pageRecord['combined_identifier']);
+                $theIcon = BackendUtility::wrapClickMenuOnIcon($iconImg, 'sys_file', $pageRecord['combined_identifier']);
             } catch (ResourceDoesNotExistException $e) {
                 $theIcon = '';
             }
@@ -153,7 +153,7 @@ class MetaInformation
             $uid = '0';
             $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
         }
-        // Setting icon with clickMenu + uid
+        // Setting icon with context menu + uid
         return $theIcon .
             ' <strong>' . htmlspecialchars($title) . ($uid !== '' ? '&nbsp;[' . $uid . ']' : '') . '</strong>' .
             (!empty($additionalInfo) ? ' ' . htmlspecialchars($additionalInfo) : '');
index 1468ce2..d8e5416 100644 (file)
@@ -1125,7 +1125,7 @@ function jumpToUrl(URL) {
     {
         GeneralUtility::logDeprecatedFunction();
         $this->pageRenderer->loadJquery();
-        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ClickMenu');
+        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
     }
 
     /**
@@ -1453,14 +1453,14 @@ function jumpToUrl(URL) {
     }
 
     /**
-     * Setting page icon with clickmenu + uid for docheader
+     * Setting page icon with context menu + uid for docheader
      *
      * @param array $pageRecord Current page
      * @return string Page info
      */
     protected function getPageInfo($pageRecord)
     {
-        // Add icon with clickmenu, etc:
+        // Add icon with context menu, etc:
         // If there IS a real page
         if (is_array($pageRecord) && $pageRecord['uid']) {
             $alttext = BackendUtility::getRecordIconAltText($pageRecord, 'pages');
@@ -1481,7 +1481,7 @@ function jumpToUrl(URL) {
             $uid = '0';
             $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
         }
-        // Setting icon with clickmenu + uid
+        // Setting icon with context menu + uid
         $pageInfo = $theIcon . '<strong>' . htmlspecialchars($title) . '&nbsp;[' . $uid . ']</strong>';
         return $pageInfo;
     }
index bc99db5..8087ea1 100644 (file)
@@ -179,7 +179,9 @@ class FolderTreeView extends AbstractTreeView
         if (!$this->ext_IconMode) {
             // Check storage access to wrap with click menu
             if (!$folderObject instanceof InaccessibleFolder) {
-                $theFolderIcon = BackendUtility::wrapClickMenuOnIcon($icon, $folderObject->getCombinedIdentifier(), '', 0);
+                $tableName = $this->getTableNameForClickMenu($folderObject);
+                $theFolderIcon = BackendUtility::wrapClickMenuOnIcon($icon, $tableName, $folderObject->getCombinedIdentifier(), 'tree');
+
             }
         } elseif ($this->ext_IconMode === 'titlelink') {
             $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($folderObject)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($folderObject)) . ',' . $this->bank . ');';
@@ -205,7 +207,8 @@ class FolderTreeView extends AbstractTreeView
             return $title;
         }
         $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($folderObject)) . ', this, ' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($folderObject)) . ', ' . $bank . ');';
-        $clickMenuParts = BackendUtility::wrapClickMenuOnIcon('', $folderObject->getCombinedIdentifier(), '', 0, ('&bank=' . $this->bank), '', true);
+        $tableName = $this->getTableNameForClickMenu($folderObject);
+        $clickMenuParts = BackendUtility::wrapClickMenuOnIcon('', $tableName, $folderObject->getCombinedIdentifier(), 'tree', '', '', true);
 
         return '<a href="#" title="' . htmlspecialchars(strip_tags($title)) . '" onclick="' . htmlspecialchars($aOnClick) . '" ' . GeneralUtility::implodeAttributes($clickMenuParts) . '>' . $title . '</a>';
     }
@@ -555,6 +558,24 @@ class FolderTreeView extends AbstractTreeView
     }
 
     /**
+     * Returns table name for click menu
+     *
+     * @param Folder $folderObject
+     * @return string
+     */
+    protected function getTableNameForClickMenu(Folder $folderObject)
+    {
+        if (strpos($folderObject->getRole(), FolderInterface::ROLE_MOUNT) !== false) {
+            $tableName = 'sys_filemounts';
+        } elseif ($folderObject->getIdentifier() === $folderObject->getStorage()->getRootLevelFolder()->getIdentifier()) {
+            $tableName = 'sys_file_storage';
+        } else {
+            $tableName = 'sys_file';
+        }
+        return $tableName;
+    }
+
+    /**
      * Counts the number of directories in a file path.
      *
      * @param Folder $folderObject File path.
index 1b8dd36..cd57fbf 100644 (file)
@@ -3037,21 +3037,17 @@ class BackendUtility
      * Returns $str wrapped in a link which will activate the context sensitive
      * menu for the record ($table/$uid) or file ($table = file)
      * The link will load the top frame with the parameter "&item" which is the table, uid
-     * and listFrame arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$listFr)
+     * and context arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$context)
      *
      * @param string $content String to be wrapped in link, typ. image tag.
      * @param string $table Table name/File path. If the icon is for a database
      * record, enter the tablename from $GLOBALS['TCA']. If a file then enter
      * the absolute filepath
-     * @param int $uid If icon is for database record this is the UID for the
-     * record from $table
-     * @param bool $listFrame Tells the top frame script that the link is coming
-     * from a "list" frame which means a frame from within the backend content frame.
-     * @param string $addParams Additional GET parameters for the link to the
-     * ClickMenu AJAX request
-     * @param string $enDisItems Enable / Disable click menu items.
-     * Example: "+new,view" will display ONLY these two items (and any spacers
-     * in between), "new,view" will display all BUT these two items.
+     * @param int|string $uid If icon is for database record this is the UID for the
+     * record from $table or identifier for sys_file record
+     * @param string $context Set tree if menu is called from tree view
+     * @param string $_addParams NOT IN USE
+     * @param string $_enDisItems NOT IN USE
      * @param bool $returnTagParameters If set, will return only the onclick
      * JavaScript, not the whole link.
      *
@@ -3061,18 +3057,16 @@ class BackendUtility
         $content,
         $table,
         $uid = 0,
-        $listFrame = true,
-        $addParams = '',
-        $enDisItems = '',
+        $context = '',
+        $_addParams = '',
+        $_enDisItems = '',
         $returnTagParameters = false
     ) {
         $tagParameters = [
-            'class' => 't3-js-clickmenutrigger',
+            'class' => 't3js-contextmenutrigger',
             'data-table' => $table,
-            'data-uid' => (int)$uid !== 0 ? (int)$uid : '',
-            'data-listframe' => $listFrame,
-            'data-iteminfo' => str_replace('+', '%2B', $enDisItems),
-            'data-parameters' => $addParams,
+            'data-uid' => $uid,
+            'data-context' => $context
         ];
 
         if ($returnTagParameters) {
index b62eaf2..09951be 100644 (file)
@@ -1620,10 +1620,9 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
         $allowDragAndDrop = $this->isDragAndDropAllowed($row);
         $additionalIcons = [];
         if ($row['sys_language_uid'] > 0 && $this->checkIfTranslationsExistInLanguage([], (int)$row['sys_language_uid'])) {
-            $disabledClickMenuItems = 'new,move';
             $allowDragAndDrop = false;
         }
-        $additionalIcons[] = $this->getIcon('tt_content', $row, $disabledClickMenuItems) . ' ';
+        $additionalIcons[] = $this->getIcon('tt_content', $row) . ' ';
         $additionalIcons[] = $langMode ? $this->languageFlag($row['sys_language_uid'], false) : '';
         // Get record locking status:
         if ($lockInfo = BackendUtility::isRecordLocked('tt_content', $row['uid'])) {
@@ -1791,10 +1790,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                 $icon = BackendUtility::wrapClickMenuOnIcon(
                                     $icon,
                                     $tableName,
-                                    $shortcutRecord['uid'],
-                                    1,
-                                    '',
-                                    '+copy,info,edit,view'
+                                    $shortcutRecord['uid']
                                 );
                                 $shortcutContent[] = $icon
                                     . htmlspecialchars(BackendUtility::getRecordTitle($tableName, $shortcutRecord));
@@ -2218,8 +2214,8 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
      * @return void
      *
      * @see \TYPO3\CMS\Recordlist\RecordList::main()
-     * @see \TYPO3\CMS\Backend\Controller\ClickMenuController::main()
-     * @see \TYPO3\CMS\Filelist\Controller\FileListController::main()
+     * @see \TYPO3\CMS\Backend\Controller\ContextMenuController::clipboardAction()
+     * @see \TYPO3\CMS\Filelist\Controller\FileListController::indexAction()
      */
     protected function initializeClipboard()
     {
@@ -2334,7 +2330,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
         $this->counter++;
         // The icon with link
         if ($this->getBackendUser()->recordEditAccessInternals($table, $row)) {
-            $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid'], true, '', $enabledClickMenuItems);
+            $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']);
         }
         return $icon;
     }
index 3ec080a..e39ca1b 100644 (file)
@@ -67,7 +67,7 @@ class PageTreeView extends BrowseTreeView
         }
         // Wrap icon in click-menu link.
         if (!$this->ext_IconMode) {
-            $thePageIcon = BackendUtility::wrapClickMenuOnIcon($thePageIcon, 'pages', $row['uid'], 0, '&bank=' . $this->bank);
+            $thePageIcon = BackendUtility::wrapClickMenuOnIcon($thePageIcon, 'pages', $row['uid'], 'tree');
         } elseif ($this->ext_IconMode === 'titlelink') {
             $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->treeName) . ');';
             $thePageIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $thePageIcon . '</a>';
@@ -110,7 +110,7 @@ class PageTreeView extends BrowseTreeView
             unset($_params);
         }
         $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($row)) . ',' . $bank . ');';
-        $clickMenuParts = BackendUtility::wrapClickMenuOnIcon('', 'pages', $row['uid'], 0, ('&bank=' . $this->bank), '', true);
+        $clickMenuParts = BackendUtility::wrapClickMenuOnIcon('', 'pages', $row['uid'], 'tree', '', '', true);
 
         $thePageTitle = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '"' . GeneralUtility::implodeAttributes($clickMenuParts) . '>' . $title . '</a>';
         // Wrap title in a drag/drop span.
index b8a8747..2e7a611 100644 (file)
@@ -165,7 +165,13 @@ return [
     // Load context menu for
     'contextmenu' => [
         'path' => '/context-menu',
-        'target' => Controller\ClickMenuController::class . '::getContextMenuAction'
+        'target' => Controller\ContextMenuController::class . '::getContextMenuAction'
+    ],
+
+    // Load context menu for
+    'contextmenu_clipboard' => [
+        'path' => '/context-menu/clipboard',
+        'target' => Controller\ContextMenuController::class . '::clipboardAction'
     ],
 
     // Process data handler commands
index 782f3f0..7aee3a2 100644 (file)
@@ -9909,6 +9909,7 @@ div#contentMenu1 {
   min-width: 150px;
 }
 .context-menu .list-group-item {
+  cursor: pointer;
   padding: 5px;
   border-bottom-color: transparent;
   border-top-color: transparent;
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ClickMenu.js b/typo3/sysext/backend/Resources/Public/JavaScript/ClickMenu.js
deleted file mode 100644 (file)
index 150c020..0000000
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * 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!
- */
-
-/**
- * Module: TYPO3/CMS/Backend/ClickMenu
- * Javascript container used to load the clickmenu via AJAX
- * to render the result in a layer next to the mouse cursor
- */
-define(['jquery'], function($) {
-
-       /**
-        *
-        * @type {{mousePos: {X: null, Y: null}, delayClickMenuHide: boolean}}
-        * @exports TYPO3/CMS/Backend/ClickMenu
-        */
-       var ClickMenu = {
-               mousePos: {
-                       X: null,
-                       Y: null
-               },
-               delayClickMenuHide: false
-       };
-
-       /**
-        * Initialize events
-        */
-       ClickMenu.initializeEvents = function() {
-               $(document).on('click contextmenu', '.t3-js-clickmenutrigger', function(event) {
-                       // if there is an other "inline" onclick setting, clickmenu is not triggered
-                       // usually this is the case for the foldertree
-                       if ($(this).prop('onclick') && event.type === 'click') {
-                               return;
-                       }
-                       event.preventDefault();
-                       ClickMenu.show(
-                               $(this).data('table'),
-                               $(this).data('uid'),
-                               $(this).data('listframe'),
-                               $(this).data('iteminfo'),
-                               $(this).data('parameters')
-                       );
-               });
-
-               // register mouse movement inside the document
-               $(document).on('mousemove', ClickMenu.storeMousePositionEvent);
-       };
-
-       /**
-        * Main function, called from most clickmenu links
-        *
-        * @param {String} table Table from where info should be fetched
-        * @param {(String|Number)} uid The UID of the item
-        * @param {String} listFr list Frame?
-        * @param {String} enDisItems Items to disable / enable
-        * @param {String} addParams Additional params
-        * @return void
-        */
-       ClickMenu.show = function(table, uid, listFr, enDisItems, addParams) {
-               var parameters = '';
-
-               if (typeof table !== 'undefined') {
-                       parameters += 'table=' + encodeURIComponent(table);
-               }
-               if (typeof uid !== 'undefined') {
-                       parameters += (parameters.length > 0 ? '&' : '') + 'uid=' + uid;
-               }
-               if (typeof listFr !== 'undefined') {
-                       parameters += (parameters.length > 0 ? '&' : '') + 'listFr=' + listFr;
-               }
-               if (typeof enDisItems !== 'undefined') {
-                       parameters += (parameters.length > 0 ? '&' : '') + 'enDisItems=' + enDisItems;
-               }
-               if (typeof addParams !== 'undefined') {
-                       parameters += (parameters.length > 0 ? '&' : '') + 'addParams=' + addParams;
-               }
-               this.fetch(parameters);
-       };
-
-       /**
-        * Make the AJAX request
-        *
-        * @param {array} parameters Parameters sent to the server
-        * @return void
-        */
-       ClickMenu.fetch = function(parameters) {
-               var url = TYPO3.settings.ajaxUrls['contextmenu'];
-               if (parameters) {
-                       url += ((url.indexOf('?') == -1) ? '?' : '&') + parameters;
-               }
-               $.ajax(url).done(function(response) {
-                       // No response content, usually because a copy/paste action was triggered and just the frame needs
-                       // to be reloaded.
-                       if (typeof response.items === "undefined") {
-                               var res = parameters.match(/&reloadListFrame=(0|1|2)(&|$)/);
-                               if (res !== null && parseInt(res[1], 0)) {
-                                       top.list_frame.location.reload(true);
-                               }
-                       } else {
-                               ClickMenu.populateData(response.items, response.level);
-                       }
-               });
-       };
-
-       /**
-        * fills the clickmenu with content and displays it correctly
-        * depending on the mouse position
-        *
-        * @param {array} items The data that will be put in the menu
-        * @param {Number} level The depth of the clickmenu
-        */
-       ClickMenu.populateData = function(items, level) {
-               this.initializeClickMenuContainer();
-
-               level = parseInt(level, 10) || 0;
-               var $obj = $('#contentMenu' + level);
-
-               if ($obj.length && (level === 0 || $('#contentMenu' + (level-1)).is(':visible'))) {
-                       $obj.html('<div class="list-group">' + items.join('') + '</div>');
-                       var x = this.mousePos.X;
-                       var y = this.mousePos.Y;
-                       var dimsWindow = {
-                               width: $(document).width()-20, // saving margin for scrollbars
-                               height: $(document).height()
-                       };
-
-                       // dimensions for the clickmenu
-                       var dims = {
-                               width: $obj.width(),
-                               height: $obj.height()
-                       };
-
-                       var relative = {
-                               X: this.mousePos.X - $(document).scrollLeft(),
-                               Y: this.mousePos.Y - $(document).scrollTop()
-                       };
-
-                       // adjusting the Y position of the layer to fit it into the window frame
-                       // if there is enough space above then put it upwards,
-                       // otherwise adjust it to the bottom of the window
-                       if (dimsWindow.height - dims.height < relative.Y) {
-                               if (relative.Y > dims.height) {
-                                       y -= (dims.height - 10);
-                               } else {
-                                       y += (dimsWindow.height - dims.height - relative.Y);
-                               }
-                       }
-                       // adjusting the X position like Y above, but align it to the left side of the viewport if it does not fit completely
-                       if (dimsWindow.width - dims.width < relative.X) {
-                               if (relative.X > dims.width) {
-                                       x -= (dims.width - 10);
-                               } else if ((dimsWindow.width - dims.width - relative.X) < $(document).scrollLeft()) {
-                                       x = $(document).scrollLeft();
-                               } else {
-                                       x += (dimsWindow.width - dims.width - relative.X);
-                               }
-                       }
-
-                       $obj.css({left: x + 'px', top: y + 'px'}).show();
-               }
-       };
-
-       /**
-        * event handler function that saves the
-        * actual position of the mouse
-        * in the Clickmenu object
-        *
-        * @param {Event} event The event object
-        */
-       ClickMenu.storeMousePositionEvent = function(event) {
-               ClickMenu.mousePos.X = event.pageX;
-               ClickMenu.mousePos.Y = event.pageY;
-               ClickMenu.mouseOutFromMenu('#contentMenu0');
-               ClickMenu.mouseOutFromMenu('#contentMenu1');
-       };
-
-       /**
-        * hides a visible menu if the mouse has moved outside
-        * of the object
-        *
-        * @param {Object} obj The object to hide
-        */
-       ClickMenu.mouseOutFromMenu = function(obj) {
-               var $element = $(obj);
-
-               if ($element.length > 0 && $element.is(':visible') && !this.within($element, this.mousePos.X, this.mousePos.Y)) {
-                       this.hide($element);
-               } else if ($element.length > 0 && $element.is(':visible')) {
-                       this.delayClickMenuHide = true;
-               }
-       };
-
-       /**
-        *
-        * @param {Object} $element
-        * @param {Number} x
-        * @param {Number} y
-        * @returns {Boolean}
-        */
-       ClickMenu.within = function($element, x, y) {
-               var offset = $element.offset();
-           return (y >= offset.top &&
-                               y <  offset.top + $element.height() &&
-                               x >= offset.left &&
-                               x <  offset.left + $element.width());
-       };
-
-       /**
-        * hides a clickmenu
-        *
-        * @param {Object} obj The clickmenu object to hide
-        */
-       ClickMenu.hide = function(obj) {
-               this.delayClickMenuHide = false;
-               window.setTimeout(function() {
-                       if (!ClickMenu.delayClickMenuHide) {
-                               $(obj).hide();
-                       }
-               }, 500);
-       };
-
-       /**
-        * hides all clickmenus
-        */
-       ClickMenu.hideAll = function() {
-               this.hide('#contentMenu0');
-               this.hide('#contentMenu1');
-       };
-
-       /**
-        * manipulates the DOM to add the divs needed for clickmenu at the bottom of the <body>-tag
-        */
-       ClickMenu.initializeClickMenuContainer = function() {
-               if ($('#contentMenu0').length === 0) {
-                       var code = '<div id="contentMenu0" class="context-menu"></div><div id="contentMenu1" class="context-menu" style="display: block;"></div>';
-                       $('body').append(code);
-               }
-       };
-
-       ClickMenu.initializeEvents();
-
-       // make it globally available
-       TYPO3.ClickMenu = ClickMenu;
-       return ClickMenu;
-});
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenu.js b/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenu.js
new file mode 100644 (file)
index 0000000..057c275
--- /dev/null
@@ -0,0 +1,323 @@
+/*
+ * 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!
+ */
+
+/**
+ * Module: TYPO3/CMS/Backend/ContextMenu
+ * Javascript container used to load the context menu via AJAX
+ * to render the result in a layer next to the mouse cursor
+ */
+define(['jquery', 'TYPO3/CMS/Backend/ContextMenuActions'], function ($, ContextMenuActions) {
+
+    /**
+     *
+     * @type {{mousePos: {X: null, Y: null}, delayContextMenuHide: boolean}}
+     * @exports TYPO3/CMS/Backend/ContextMenu
+     */
+    var ContextMenu = {
+        mousePos: {
+            X: null,
+            Y: null
+        },
+        delayContextMenuHide: false,
+        record: {
+            uid: null,
+            table: null
+        }
+    };
+
+    /**
+     * Initialize events
+     */
+    ContextMenu.initializeEvents = function () {
+        $(document).on('click contextmenu', '.t3js-contextmenutrigger', function (event) {
+            // if there is an other "inline" onclick setting, context menu is not triggered
+            // usually this is the case for the foldertree
+            if ($(this).prop('onclick') && event.type === 'click') {
+                return;
+            }
+            event.preventDefault();
+            ContextMenu.show(
+                $(this).data('table'),
+                $(this).data('uid'),
+                $(this).data('context'),
+                $(this).data('iteminfo'),
+                $(this).data('parameters')
+            );
+        });
+
+        // register mouse movement inside the document
+        $(document).on('mousemove', ContextMenu.storeMousePositionEvent);
+    };
+
+    /**
+     * Main function, called from most context menu links
+     *
+     * @param {String} table Table from where info should be fetched
+     * @param {(String|Number)} uid The UID of the item
+     * @param {String} context Context of the item
+     * @param {String} enDisItems Items to disable / enable
+     * @param {String} addParams Additional params
+     * @return void
+     */
+    ContextMenu.show = function (table, uid, context, enDisItems, addParams) {
+        ContextMenu.record = null;
+        ContextMenu.record = {table: table, uid: uid};
+
+        var parameters = '';
+
+        if (typeof table !== 'undefined') {
+            parameters += 'table=' + encodeURIComponent(table);
+        }
+        if (typeof uid !== 'undefined') {
+            parameters += (parameters.length > 0 ? '&' : '') + 'uid=' + uid;
+        }
+        if (typeof context !== 'undefined') {
+            parameters += (parameters.length > 0 ? '&' : '') + 'context=' + context;
+        }
+        if (typeof enDisItems !== 'undefined') {
+            parameters += (parameters.length > 0 ? '&' : '') + 'enDisItems=' + enDisItems;
+        }
+        if (typeof addParams !== 'undefined') {
+            parameters += (parameters.length > 0 ? '&' : '') + 'addParams=' + addParams;
+        }
+        this.fetch(parameters);
+    };
+
+    /**
+     * Make the AJAX request
+     *
+     * @param {array} parameters Parameters sent to the server
+     * @return void
+     */
+    ContextMenu.fetch = function (parameters) {
+        var url = TYPO3.settings.ajaxUrls['contextmenu'];
+        if (parameters) {
+            url += ((url.indexOf('?') == -1) ? '?' : '&') + parameters;
+        }
+        $.ajax(url).done(function (response) {
+            if (typeof response !== "undefined" && Object.keys(response).length > 0) {
+                ContextMenu.populateData(response, 0);
+            }
+        });
+    };
+
+    /**
+     * fills the context menu with content and displays it correctly
+     * depending on the mouse position
+     *
+     * @param {array} items The data that will be put in the menu
+     * @param {Number} level The depth of the context menu
+     */
+    ContextMenu.populateData = function (items, level) {
+        this.initializeContextMenuContainer();
+
+        level = parseInt(level, 10) || 0;
+        var $obj = $('#contentMenu' + level);
+
+        if ($obj.length && (level === 0 || $('#contentMenu' + (level - 1)).is(':visible'))) {
+            var elements = ContextMenu.drawMenu(items, level);
+            $obj.html('<div class="list-group">' + elements + '</div>');
+
+            $('a.list-group-item', $obj).click(function (event) {
+                event.preventDefault();
+
+                if ($(this).hasClass('list-group-item-submenu')) {
+                    ContextMenu.openSubmenu(level, $(this));
+                    return;
+                }
+
+                var callbackName = $(this).data('callback-action');
+                var callbackModule = $(this).data('callback-module');
+                var clickItem = $(this);
+                if (callbackModule) {
+                    require([callbackModule], function (callbackModule) {
+                        callbackModule[callbackName].bind(clickItem)(ContextMenu.record.table, ContextMenu.record.uid);
+                    });
+                } else if (ContextMenuActions && ContextMenuActions[callbackName]) {
+                    ContextMenuActions[callbackName].bind(clickItem)(ContextMenu.record.table, ContextMenu.record.uid);
+                } else {
+                    console.log('action: ' + callbackName + ' not found');
+                }
+                ContextMenu.hideAll();
+            });
+
+            $obj.css(ContextMenu.getPosition($obj)).show();
+        }
+    };
+
+    ContextMenu.openSubmenu = function (level, $item) {
+        var $obj = $('#contentMenu' + (level + 1)).html('');
+        $item.next().find('.list-group').clone(true).appendTo($obj);
+        $obj.css(ContextMenu.getPosition($obj)).show();
+    };
+
+    ContextMenu.getPosition = function ($obj) {
+        var x = this.mousePos.X;
+        var y = this.mousePos.Y;
+        var dimsWindow = {
+            width: $(document).width() - 20, // saving margin for scrollbars
+            height: $(document).height()
+        };
+
+        // dimensions for the context menu
+        var dims = {
+            width: $obj.width(),
+            height: $obj.height()
+        };
+
+        var relative = {
+            X: this.mousePos.X - $(document).scrollLeft(),
+            Y: this.mousePos.Y - $(document).scrollTop()
+        };
+
+        // adjusting the Y position of the layer to fit it into the window frame
+        // if there is enough space above then put it upwards,
+        // otherwise adjust it to the bottom of the window
+        if (dimsWindow.height - dims.height < relative.Y) {
+            if (relative.Y > dims.height) {
+                y -= (dims.height - 10);
+            } else {
+                y += (dimsWindow.height - dims.height - relative.Y);
+            }
+        }
+        // adjusting the X position like Y above, but align it to the left side of the viewport if it does not fit completely
+        if (dimsWindow.width - dims.width < relative.X) {
+            if (relative.X > dims.width) {
+                x -= (dims.width - 10);
+            } else if ((dimsWindow.width - dims.width - relative.X) < $(document).scrollLeft()) {
+                x = $(document).scrollLeft();
+            } else {
+                x += (dimsWindow.width - dims.width - relative.X);
+            }
+        }
+        return {left: x + 'px', top: y + 'px'};
+    };
+
+    /**
+     * fills the context menu with content and displays it correctly
+     * depending on the mouse position
+     *
+     * @param {array} items The data that will be put in the menu
+     * @param {Number} level The depth of the context menu
+     */
+    ContextMenu.drawMenu = function (items, level) {
+        var elements = '';
+        $.each(items, function (key, value) {
+            if (value.type === 'item') {
+                elements += ContextMenu.drawActionItem(value);
+            } else if (value.type === 'divider') {
+                elements += '<a class="list-group-item list-group-item-divider"></a>';
+            } else if (value.type === 'submenu' || value.childItems) {
+                elements += '<a class="list-group-item list-group-item-submenu"><span class="list-group-item-icon">' + value.icon + '</span> ' + value.label + '&nbsp;&nbsp;<span class="fa fa-caret-right"></span></a>';
+                var childElements = ContextMenu.drawMenu(value.childItems, 1);
+                elements += '<div class="context-menu contentMenu' + (level + 1) + '" style="display:none;"><div class="list-group">' + childElements + '</div></div>';
+
+            }
+        });
+        return elements;
+    };
+
+    ContextMenu.drawActionItem = function (value) {
+        var attributes = value.additionalAttributes || [];
+        $attributesString = '';
+        for (var attribute in attributes) {
+            $attributesString += ' ' + attribute + '="' + attributes[attribute] + '"';
+        }
+
+        return '<a class="list-group-item"'
+            + ' data-callback-action="' + value.callbackAction + '"'
+            + $attributesString + '><span class="list-group-item-icon">' + value.icon + '</span> ' + value.label + '</a>';
+    };
+    /**
+     * event handler function that saves the
+     * actual position of the mouse
+     * in the context menu object
+     *
+     * @param {Event} event The event object
+     */
+    ContextMenu.storeMousePositionEvent = function (event) {
+        ContextMenu.mousePos.X = event.pageX;
+        ContextMenu.mousePos.Y = event.pageY;
+        ContextMenu.mouseOutFromMenu('#contentMenu0');
+        ContextMenu.mouseOutFromMenu('#contentMenu1');
+    };
+
+    /**
+     * hides a visible menu if the mouse has moved outside
+     * of the object
+     *
+     * @param {Object} obj The object to hide
+     */
+    ContextMenu.mouseOutFromMenu = function (obj) {
+        var $element = $(obj);
+
+        if ($element.length > 0 && $element.is(':visible') && !this.within($element, this.mousePos.X, this.mousePos.Y)) {
+            this.hide($element);
+        } else if ($element.length > 0 && $element.is(':visible')) {
+            this.delayContextMenuHide = true;
+        }
+    };
+
+    /**
+     *
+     * @param {Object} $element
+     * @param {Number} x
+     * @param {Number} y
+     * @returns {Boolean}
+     */
+    ContextMenu.within = function ($element, x, y) {
+        var offset = $element.offset();
+        return (
+            y >= offset.top &&
+            y < offset.top + $element.height() &&
+            x >= offset.left &&
+            x < offset.left + $element.width()
+        );
+    };
+
+    /**
+     * hides a context menu
+     *
+     * @param {Object} obj The context menu object to hide
+     */
+    ContextMenu.hide = function (obj) {
+        this.delayContextMenuHide = false;
+        window.setTimeout(function () {
+            if (!ContextMenu.delayContextMenuHide) {
+                $(obj).hide();
+            }
+        }, 500);
+    };
+
+    /**
+     * hides all context menus
+     */
+    ContextMenu.hideAll = function () {
+        this.hide('#contentMenu0');
+        this.hide('#contentMenu1');
+    };
+
+    /**
+     * manipulates the DOM to add the divs needed for context menu the bottom of the <body>-tag
+     */
+    ContextMenu.initializeContextMenuContainer = function () {
+        if ($('#contentMenu0').length === 0) {
+            var code = '<div id="contentMenu0" class="context-menu"></div><div id="contentMenu1" class="context-menu" style="display: block;"></div>';
+            $('body').append(code);
+        }
+    };
+
+    ContextMenu.initializeEvents();
+
+    return ContextMenu;
+});
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenuActions.js b/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenuActions.js
new file mode 100644 (file)
index 0000000..4593927
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+ * 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!
+ */
+
+/**
+ * Module: TYPO3/CMS/Backend/ContextMenuActions
+ * Click menu actions for db records including tt_content and pages
+ */
+define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity'], function ($, Modal, Severity) {
+    /**
+     *
+     * @exports TYPO3/CMS/Backend/ContextMenuActions
+     */
+    var ContextMenuActions = {};
+
+    ContextMenuActions.getReturnUrl = function () {
+       return top.rawurlencode(top.list_frame.document.location.pathname + top.list_frame.document.location.search);
+    };
+
+    ContextMenuActions.editRecord = function (table, uid) {
+        top.TYPO3.Backend.ContentContainer.setUrl(
+            top.TYPO3.settings.FormEngine.moduleUrl + '&edit[' + table + '][' + uid + ']=edit&returnUrl=' + ContextMenuActions.getReturnUrl()
+        );
+    };
+
+    ContextMenuActions.viewRecord = function (table, uid) {
+        var $viewUrl = $(this).data('preview-url');
+        if ($viewUrl) {
+            var previewWin = window.open($viewUrl, 'newTYPO3frontendWindow');
+            previewWin.focus();
+        }
+    };
+
+    ContextMenuActions.openInfoPopUp = function (table, uid) {
+        top.launchView(table, uid);
+    };
+
+    ContextMenuActions.mountAsTreeRoot = function (table, uid) {
+        // see actions.js -> mountAsTreeRoot
+        if (table === 'pages' && typeof top.Ext.getCmp('typo3-pagetree') !== 'undefined') {
+            var node = top.Ext.getCmp('typo3-pagetree-tree').app.getSelected();
+            // var node = top.TYPO3.Backend.NavigationContainer.PageTree.getSelected();
+            if (node === null) {
+                return false;
+            }
+
+            var useNode = {
+                attributes: {
+                    nodeData: {
+                        id: uid
+                    }
+                }
+            };
+            top.TYPO3.Components.PageTree.Actions.mountAsTreeRoot(useNode, node.ownerTree);
+        }
+    };
+
+    ContextMenuActions.newPageWizard = function (table, uid) {
+        top.TYPO3.Backend.ContentContainer.setUrl(
+            top.TYPO3.settings.NewRecord.moduleUrl + '&id=' + uid + '&pagesOnly=1'
+        );
+    };
+
+    ContextMenuActions.newContentWizard = function (table, uid) {
+        var $wizardUrl = $(this).data('new-wizard-url');
+        if ($wizardUrl) {
+            top.TYPO3.Backend.ContentContainer.setUrl($wizardUrl);
+        }
+    };
+
+    ContextMenuActions.newRecord = function (table, uid) {
+        top.TYPO3.Backend.ContentContainer.setUrl(
+            top.TYPO3.settings.FormEngine.moduleUrl + '&edit[' + table + '][-' + uid + ']=new&returnUrl='+ ContextMenuActions.getReturnUrl()
+        );
+    };
+
+    ContextMenuActions.openHistoryPopUp = function (table, uid) {
+        top.TYPO3.Backend.ContentContainer.setUrl(
+            top.TYPO3.settings.RecordHistory.moduleUrl + '&element=' + table + ':' + uid
+        );
+    };
+
+    ContextMenuActions.openListModule = function (table, uid) {
+        var pageId = table === 'pages' ? uid : $(this).data('page-uid');
+        top.TYPO3.ModuleMenu.App.showModule('web_list', 'id='.pageId);
+    };
+
+    ContextMenuActions.disableRecord = function (table, uid) {
+        top.TYPO3.Backend.ContentContainer.setUrl(
+            top.TYPO3.settings.RecordCommit.moduleUrl + '&data[' + table + '][' + uid + '][hidden]=1&prErr=1&redirect=' + ContextMenuActions.getReturnUrl()
+        );
+        top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree.defer(500);
+    };
+
+    ContextMenuActions.enableRecord = function (table, uid) {
+        top.TYPO3.Backend.ContentContainer.setUrl(
+            top.TYPO3.settings.RecordCommit.moduleUrl + '&data[' + table + '][' + uid + '][hidden]=0&prErr=1&redirect=' + ContextMenuActions.getReturnUrl()
+        );
+        top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree.defer(500);
+    };
+
+    ContextMenuActions.deleteRecord = function (table, uid) {
+        var $anchorElement = $(this);
+        var $modal = Modal.confirm(
+            $anchorElement.data('title'),
+            $anchorElement.data('message'),
+            Severity.warning, [
+                {
+                    text: $(this).data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel',
+                    active: true,
+                    btnClass: 'btn-default',
+                    name: 'cancel'
+                },
+                {
+                    text: $(this).data('button-ok-text') || TYPO3.lang['button.delete'] || 'Delete',
+                    btnClass: 'btn-warning',
+                    name: 'delete'
+                }
+            ]);
+
+        $modal.on('button.clicked', function (e) {
+            if (e.target.name === 'delete') {
+                top.TYPO3.Backend.ContentContainer.setUrl(
+                    top.TYPO3.settings.RecordCommit.moduleUrl + '&redirect=' + ContextMenuActions.getReturnUrl() + '&cmd[' + table + '][' + uid + '][delete]=1&prErr=1'
+                );
+                if (table === 'pages' && top.TYPO3.Backend.NavigationContainer.PageTree) {
+                    top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree.defer(500);
+                }
+            }
+            Modal.dismiss();
+        });
+    };
+
+    ContextMenuActions.copy = function (table, uid) {
+        var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
+        url += '&CB[el][' + table + '%7C' + uid + ']=1'+ '&CB[setCopyMode]=1';
+        $.ajax(url).always(function () {
+            top.list_frame.location.reload(true);
+        });
+    };
+
+    ContextMenuActions.clipboardRelease = function (table, uid) {
+        var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
+        url += '&CB[el][' + table + '%7C' + uid + ']=0';
+        $.ajax(url).always(function () {
+            top.list_frame.location.reload(true);
+        });
+    };
+
+    ContextMenuActions.cut = function (table, uid) {
+        var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
+        url += '&CB[el][' + table + '%7C' + uid + ']=1'+ '&CB[setCopyMode]=0';
+        $.ajax(url).always(function () {
+            top.list_frame.location.reload(true);
+        });
+    };
+
+    /**
+     * Clear cache for given page uid
+     *
+     * @param {string} table pages table
+     * @param {int} uid of the page
+     */
+    ContextMenuActions.clearCache = function (table, uid) {
+        var url = top.TYPO3.settings.WebLayout.moduleUrl;
+        url += '&id=' + uid + '&clear_cache=1';
+        $.ajax(url);
+    };
+
+    /**
+     * Paste db record after another
+     *
+     * @param {string} table any db table except sys_file
+     * @param {int} uid of the record after which record from the cliboard will be pasted
+     */
+    ContextMenuActions.pasteAfter = function (table, uid) {
+        ContextMenuActions.pasteInto.bind($(this))(table, -uid);
+    };
+
+    /**
+     * Paste page into another page
+     *
+     * @param {string} table any db table except sys_file
+     * @param {int} uid of the record after which record from the cliboard will be pasted
+     */
+    ContextMenuActions.pasteInto = function (table, uid) {
+        var $anchorElement = $(this);
+        var title = $anchorElement.data('title');
+        var performPaste = function () {
+            var url = '&CB[paste]=' + table + '%7C' + uid
+                + '&CB[pad]=normal&prErr=1&uPT=1'
+                + '&redirect=' + ContextMenuActions.getReturnUrl();
+
+            top.TYPO3.Backend.ContentContainer.setUrl(
+                top.TYPO3.settings.RecordCommit.moduleUrl + url
+            );
+            if (table === 'pages' && top.TYPO3.Backend.NavigationContainer.PageTree) {
+                top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree.defer(500);
+            }
+        };
+        if (!$anchorElement.data('title')) {
+            performPaste();
+            return;
+        }
+        var $modal = Modal.confirm(
+            $anchorElement.data('title'),
+            $anchorElement.data('message'),
+            Severity.warning, [
+                {
+                    text: $(this).data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel',
+                    active: true,
+                    btnClass: 'btn-default',
+                    name: 'cancel'
+                },
+                {
+                    text: $(this).data('button-ok-text') || TYPO3.lang['button.ok'] || 'OK',
+                    btnClass: 'btn-warning',
+                    name: 'ok'
+                }
+            ]);
+
+        $modal.on('button.clicked', function (e) {
+            if (e.target.name === 'ok') {
+                performPaste();
+            }
+            Modal.dismiss();
+        });
+
+    };
+
+    return ContextMenuActions;
+});
\ No newline at end of file
index 6f48c47..9e575dd 100644 (file)
@@ -474,7 +474,7 @@ define(['jquery',
 
                                // replace file icon
                                if (data.upload[0].icon) {
-                                       me.$iconCol.html('<a href="#" class="t3-js-clickmenutrigger" data-table="' + data.upload[0].id + '" data-listframe="1">' + data.upload[0].icon + '&nbsp;</span></a>');
+                                       me.$iconCol.html('<a href="#" class="t3js-contextmenutrigger" data-uid="' + data.upload[0].id + '" data-table="sys_file">' + data.upload[0].icon + '&nbsp;</span></a>');
                                }
 
                                if (me.dragUploader.irreObjectUid) {
index e976350..13074e4 100644 (file)
@@ -62,10 +62,17 @@ define(['jquery'], function($) {
        DragDrop.dropElement = function(event) {
                var dropID = DragDrop.getIdFromEvent(event);
                if ((DragDrop.dragID) && (DragDrop.dragID !== dropID)) {
-                       var parameters = 'dragDrop=' + DragDrop.table +
-                                       '&srcId=' + DragDrop.dragID +
-                                       '&dstId=' + dropID;
-                       TYPO3.ClickMenu.fetch(parameters);
+                       var dragID = DragDrop.dragID;
+                       var table = DragDrop.table;
+                       var parameters = 'table=' + table + '-drag' +
+                               '&uid=' + dragID +
+                               '&dragDrop=' + table +
+                               '&srcId=' + dragID +
+                               '&dstId=' + dropID;
+                       require(['TYPO3/CMS/Backend/ContextMenu'], function (ContextMenu) {
+                               ContextMenu.record = {table: decodeURIComponent(table), uid: decodeURIComponent(dragID)};
+                               ContextMenu.fetch(parameters);
+                       });
                }
                DragDrop.cancelDragEvent();
                return false;
index 092ed56..8398fa8 100644 (file)
@@ -96,7 +96,6 @@ TYPO3.Components.PageTree.App = Ext.extend(Ext.Panel, {
                                autoWidth: true,
                                plugins: [new Ext.ux.state.TreePanel()],
                                commandProvider: TYPO3.Components.PageTree.Actions,
-                               contextMenuProvider: TYPO3.Components.PageTree.ContextMenuDataProvider,
                                treeDataProvider: TYPO3.Components.PageTree.DataProvider,
                                monitorResize: true,
                                app: this,
@@ -119,7 +118,6 @@ TYPO3.Components.PageTree.App = Ext.extend(Ext.Panel, {
                                autoScroll: true,
                                autoHeight: false,
                                commandProvider: TYPO3.Components.PageTree.Actions,
-                               contextMenuProvider: TYPO3.Components.PageTree.ContextMenuDataProvider,
                                treeDataProvider: TYPO3.Components.PageTree.DataProvider,
                                app: this
                        }).hide();
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/contextmenu.js b/typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/contextmenu.js
deleted file mode 100644 (file)
index dd14834..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * 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!
- */
-Ext.namespace('TYPO3.Components.PageTree');
-
-/**
- * @class TYPO3.Components.PageTree.ContextMenu
- *
- * Context menu implementation
- *
- * @namespace TYPO3.Components.PageTree
- * @extends Ext.menu.Menu
- */
-TYPO3.Components.PageTree.ContextMenu = Ext.extend(Ext.menu.Menu, {
-       /**
-        * Context menu node
-        *
-        * @cfg {Ext.tree.TreeNode}
-        */
-       node: null,
-
-       /**
-        * Page Tree
-        *
-        * @cfg {TYPO3.Components.PageTree.Tree}
-        */
-       pageTree: null,
-
-       /**
-        * Component Id
-        *
-        * @type {String}
-        */
-       id: 'typo3-pagetree-contextmenu',
-
-       /**
-        * Parent clicks should be ignored
-        *
-        * @type {Boolean}
-        */
-       ignoreParentClicks: true,
-
-       /**
-        * Listeners
-        *
-        * The itemclick event triggers the configured single click action
-        */
-       listeners: {
-               itemclick: {
-                       fn: function (item) {
-                               if (this.pageTree.commandProvider[item.callbackAction]) {
-                                       if (item.parentMenu.pageTree.stateHash) {
-                                               fsMod.recentIds['web'] = item.parentMenu.node.attributes.nodeData.id;
-                                               item.parentMenu.pageTree.stateHash['lastSelectedNode'] = item.parentMenu.node.id;
-                                       }
-
-                                       this.pageTree.commandProvider[item.callbackAction](
-                                               item.parentMenu.node,
-                                               item.parentMenu.pageTree,
-                                               item
-                                       );
-                               }
-                       }
-               }
-       },
-
-       /**
-        * Fills the menu with the actions
-        *
-        * @param {Ext.tree.TreeNode} node
-        * @param {TYPO3.Components.PageTree.Tree} pageTree
-        * @param {Object} contextMenuConfiguration
-        * @return {void}
-        */
-       fill: function(node, pageTree, contextMenuConfiguration) {
-               this.node = node;
-               this.pageTree = pageTree;
-
-               var components = this.preProcessContextMenuConfiguration(contextMenuConfiguration);
-               if (components.length) {
-                       for (var component in components) {
-                               if (components[component] === '-') {
-                                       this.addSeparator();
-                               } else if (typeof components[component] === 'object') {
-                                       this.addItem(components[component]);
-                               }
-                       }
-               }
-       },
-
-       /**
-        * Parses the context menu actions array recursively and generates the
-        * components for the context menu including separators/dividers and sub menus
-        *
-        * @param {Object} contextMenuConfiguration
-        * @param {int} level
-        * @return {Object}
-        */
-       preProcessContextMenuConfiguration: function(contextMenuConfiguration, level) {
-               level = level || 0;
-               if (level > 5) {
-                       return [];
-               }
-
-               var components = [];
-               var index = 0;
-
-               var modulesInsideGroup = false;
-               var subMenus = 0;
-               for (var singleAction in contextMenuConfiguration) {
-                       if (contextMenuConfiguration[singleAction]['type'] === 'submenu') {
-                               var subMenuComponents = this.preProcessContextMenuConfiguration(
-                                       contextMenuConfiguration[singleAction]['childActions'],
-                                       level + 1
-                               );
-
-                               if (subMenuComponents.length) {
-                                       var subMenu = new TYPO3.Components.PageTree.ContextMenu({
-                                               id: this.id + '-sub' + ++subMenus,
-                                               items: subMenuComponents,
-                                               node: this.node,
-                                               pageTree: this.pageTree
-                                       });
-
-                                       components[index++] = {
-                                               text: contextMenuConfiguration[singleAction]['label'],
-                                               cls: 'contextMenu-subMenu',
-                                               menu: subMenu,
-                                               icon: contextMenuConfiguration[singleAction]['icon']
-                                       };
-                               }
-                       } else if (contextMenuConfiguration[singleAction]['type'] === 'divider') {
-                               if (modulesInsideGroup) {
-                                       components[index++] = '-';
-                                       modulesInsideGroup = false;
-                               }
-                       } else {
-                               modulesInsideGroup = true;
-
-                               if (typeof contextMenuConfiguration[singleAction] === 'object') {
-                                       var component = {
-                                               'text': contextMenuConfiguration[singleAction]['label'],
-                                               'icon': contextMenuConfiguration[singleAction]['icon'],
-                                               'callbackAction': contextMenuConfiguration[singleAction]['callbackAction'],
-                                               'customAttributes': contextMenuConfiguration[singleAction]['customAttributes']
-                                       };
-                                       component.itemTpl = Ext.menu.Item.prototype.itemTpl = new Ext.XTemplate(
-                                               '<a id="{id}" class="{cls}" hidefocus="true" unselectable="on" href="{href}">',
-                                                       '<span class="x-menu-item-icon" unselectable="on">{icon}</span>',
-                                                       '<span class="x-menu-item-text">{text}</span>',
-                                               '</a>'
-                                       );
-
-                                       components[index++] = component;
-                               }
-                       }
-               }
-
-                       // remove divider if it's the last item of the context menu
-               if (components[components.length - 1] === '-') {
-                       components[components.length - 1] = '';
-               }
-
-               return components;
-       }
-});
-
-// XTYPE Registration
-Ext.reg('TYPO3.Components.PageTree.ContextMenu', TYPO3.Components.PageTree.ContextMenu);
index 78d850f..21ca826 100644 (file)
@@ -55,7 +55,7 @@ TYPO3.Components.PageTree.PageTreeNodeUI = Ext.extend(Ext.tree.TreeNodeUI, {
                        '<span class="x-tree-node-indent">', this.indentMarkup, "</span>",
                        '<img alt="" src="', this.emptyIcon, '" class="x-tree-ec-icon x-tree-elbow" />',
 //            '<img alt="" src="', a.icon || this.emptyIcon, '" class="x-tree-node-icon',(a.icon ? " x-tree-node-inline-icon" : ""),(a.iconCls ? " "+a.iconCls : ""),'" unselectable="on" />',
-                       a.spriteIconCode, // TYPO3: add sprite icon code
+                       '<span class="t3js-contextmenutrigger" data-table="pages" data-uid="' + a.nodeData.id + '" data-context="tree" >' + a.spriteIconCode + '</span>', // TYPO3: add sprite icon code
                        (a.nodeData.stopPageTree ? '<span class="text-danger">+</span>' : ''),
                        cb ? ('<input class="x-tree-node-cb" type="checkbox" ' + (a.checked ? 'checked="checked" />' : '/>')) : '',
                        '<a hidefocus="on" class="x-tree-node-anchor" href="',href,'" tabIndex="1" ',
@@ -86,16 +86,6 @@ TYPO3.Components.PageTree.PageTreeNodeUI = Ext.extend(Ext.tree.TreeNodeUI, {
                }
                this.anchor = cs[index];
                this.textNode = cs[index].firstChild;
-
-                       // TYPO3: call the context menu on a single click (Beware of drag&drop!)
-               if (!TYPO3.Components.PageTree.Configuration.disableIconLinkToContextmenu
-                       || TYPO3.Components.PageTree.Configuration.disableIconLinkToContextmenu === '0'
-               ) {
-                       Ext.fly(this.iconNode).on('click', function(event) {
-                               this.getOwnerTree().fireEvent('contextmenu', this, event);
-                               event.stopEvent();
-                       }, n);
-               }
        },
 
        /**
index abcbd0d..c9d2f44 100644 (file)
@@ -92,13 +92,6 @@ TYPO3.Components.PageTree.Tree = Ext.extend(Ext.tree.TreePanel, {
        commandProvider : null,
 
        /**
-        * Context menu provider
-        *
-        * @cfg {Object}
-        */
-       contextMenuProvider: null,
-
-       /**
         * Id of the deletion drop zone if any
         *
         * @cfg {String}
@@ -180,6 +173,11 @@ TYPO3.Components.PageTree.Tree = Ext.extend(Ext.tree.TreePanel, {
                                        return false;
                                }
 
+                               if (event.target.tagName === 'IMG') {
+                                       // Stop this event if click event was triggered via the node icon to prevent loading the action of the node
+                                       return false;
+                               }
+
                                this.clicksRegistered = 0;
                                if (this.commandProvider.singleClick) {
                                        this.commandProvider.singleClick(node, this);
@@ -198,12 +196,20 @@ TYPO3.Components.PageTree.Tree = Ext.extend(Ext.tree.TreePanel, {
                        // prevents label edit on a selected node
                beforeclick: {
                        fn: function(node, event) {
-                               if (!this.clicksRegistered && this.getSelectionModel().isSelected(node)) {
-                                       node.fireEvent('click', node, event);
+                               if (event.target.tagName === 'IMG') {
+                                       // Node icon was clicked
+                                       // This is a workaround to prevent TreeEditor being triggered, which also requires a patch within TreeEditor
+                                       node.attributes.editable = false;
+                               } else {
+                                       node.attributes.editable = true;
+
+                                       if (!this.clicksRegistered && this.getSelectionModel().isSelected(node)) {
+                                               node.fireEvent('click', node, event);
+                                               ++this.clicksRegistered;
+                                               return false;
+                                       }
                                        ++this.clicksRegistered;
-                                       return false;
                                }
-                               ++this.clicksRegistered;
                        }
                }
        },
@@ -230,10 +236,6 @@ TYPO3.Components.PageTree.Tree = Ext.extend(Ext.tree.TreePanel, {
                        this.enableDragAndDrop();
                }
 
-               if (this.contextMenuProvider) {
-                       this.enableContextMenu();
-               }
-
                TYPO3.Components.PageTree.Tree.superclass.initComponent.apply(this, arguments);
        },
 
@@ -321,45 +323,6 @@ TYPO3.Components.PageTree.Tree = Ext.extend(Ext.tree.TreePanel, {
        },
 
        /**
-        * Enables the context menu feature
-        *
-        * return {void}
-        */
-       enableContextMenu: function() {
-               this.contextMenu = new TYPO3.Components.PageTree.ContextMenu();
-
-               this.on('contextmenu', function(node, event) {
-                       this.openContextMenu(node, event);
-               });
-       },
-
-       /**
-        * Open a context menu for the given node
-        *
-        * @param {Ext.tree.TreeNode} node
-        * @param {Ext.EventObject} event
-        * return {void}
-        */
-       openContextMenu: function(node, event) {
-               var attributes = Ext.apply(node.attributes.nodeData, {
-                       t3ContextInfo: node.ownerTree.t3ContextInfo
-               });
-
-               this.contextMenuProvider.getActionsForNodeArray(
-                       attributes,
-                       function(configuration) {
-                               this.contextMenu.removeAll();
-                               this.contextMenu.fill(node, this, configuration);
-                               if (this.contextMenu.items.length) {
-                                       this.contextMenu.showAt(event.getXY());
-
-                               }
-                       },
-                       this
-               );
-       },
-
-       /**
         * Initialize the inline editor for the given tree.
         *
         * @return {void}
diff --git a/typo3/sysext/beuser/Classes/ContextMenu/ItemProvider.php b/typo3/sysext/beuser/Classes/ContextMenu/ItemProvider.php
new file mode 100644 (file)
index 0000000..ba1b896
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Beuser\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\Backend\ContextMenu\ItemProviders\PageProvider;
+
+class ItemProvider extends PageProvider
+{
+    /**
+     * @var array
+     */
+    protected $itemsConfiguration = [
+        'permissions' => [
+            'type' => 'item',
+            'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_perms',
+            'iconIdentifier' => 'status-status-locked',
+            'callbackAction' => 'openPermissionsModule'
+        ],
+    ];
+
+    /**
+     * @param string $itemName
+     * @param string $type
+     * @return bool
+     */
+    protected function canRender(string $itemName, string $type): bool
+    {
+        if (in_array($itemName, $this->disabledItems, true)) {
+            return false;
+        }
+        return $this->canShowPermissionsModule();
+    }
+
+    /**
+     * @param array $items
+     * @return array
+     */
+    public function addItems(array $items): array
+    {
+        $this->initDisabledItems();
+        $localItems = $this->prepareItems($this->itemsConfiguration);
+        if (isset($items['more']['childItems'])) {
+            $items['more']['childItems'] = $items['more']['childItems'] + $localItems;
+        } else {
+            $items += $localItems;
+        }
+        return $items;
+    }
+
+    /**
+     * This priority should be lower than priority of the PageProvider, so it's evaluated after the PageProvider
+     *
+     * @return int
+     */
+    public function getPriority(): int
+    {
+        return 60;
+    }
+
+    /**
+     * @param string $itemName
+     * @return array
+     */
+    protected function getAdditionalAttributes(string $itemName): array
+    {
+        return ['data-callback-module' => 'TYPO3/CMS/Beuser/ContextMenuActions'];
+    }
+
+    /**
+     * Checks if the page is allowed to show permission module
+     *
+     * @return bool
+     */
+    protected function canShowPermissionsModule(): bool
+    {
+        return $this->canBeEdited() && $this->backendUser->check('modules', 'system_BeuserTxPermission');
+    }
+}
diff --git a/typo3/sysext/beuser/Classes/Hook/BackendControllerHook.php b/typo3/sysext/beuser/Classes/Hook/BackendControllerHook.php
new file mode 100644 (file)
index 0000000..fe1a63e
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+namespace TYPO3\CMS\Beuser\Hook;
+
+/*
+ * 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\Controller\BackendController;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * This class adds import export related JavaScript to the backend
+ */
+class BackendControllerHook
+{
+    /**
+     * Adds beuser permission specific JavaScript
+     *
+     * @param array $configuration
+     * @param BackendController $backendController
+     * @return void
+     */
+    public function addJavaScript(array $configuration, BackendController $backendController)
+    {
+        $this->getPageRenderer()->addInlineSetting('AccessPermissions', 'moduleUrl', BackendUtility::getModuleUrl('system_BeuserTxPermission'));
+    }
+
+    /**
+     * @return PageRenderer
+     */
+    protected function getPageRenderer()
+    {
+        return GeneralUtility::makeInstance(PageRenderer::class);
+    }
+}
index 56b003f..451d6bd 100644 (file)
@@ -2,7 +2,7 @@
        loadJQuery="true"
        jQueryNamespace="none"
        includeRequireJsModules="{
-               0:'TYPO3/CMS/Backend/ClickMenu'
+               0:'TYPO3/CMS/Backend/ContextMenu'
        }"
 />
 
index 348eec4..040a939 100644 (file)
@@ -4,7 +4,7 @@
 
 <tr>
        <td>
-               <a href="#" class="t3-js-clickmenutrigger" data-table="be_users" data-uid="{backendUser.uid}" data-listframe="1" title="{f:if(condition: '{backendUser.description}', then: '{backendUser.description} ')}(id={backendUser.uid})">
+               <a href="#" class="t3js-contextmenutrigger" data-table="be_users" data-uid="{backendUser.uid}" title="{f:if(condition: '{backendUser.description}', then: '{backendUser.description} ')}(id={backendUser.uid})">
                        <be:avatar backendUser="{backendUser.uid}" showIcon="TRUE" />
                </a>
        </td>
index 78bca57..0c4100b 100644 (file)
@@ -7,7 +7,7 @@
                <f:if condition="{it.isFirst}">
                        <f:then>
                                <td>
-                                       <a href="#" class="t3-js-clickmenutrigger" data-table="be_users" data-uid="{onlineUser.backendUser.uid}" data-listframe="1" title="{f:if(condition: '{onlineUser.backendUser.description}', then: '{onlineUser.backendUser.description} ')}(id={onlineUser.backendUser.uid})">
+                                       <a href="#" class="t3js-contextmenutrigger" data-table="be_users" data-uid="{onlineUser.backendUser.uid}" title="{f:if(condition: '{onlineUser.backendUser.description}', then: '{onlineUser.backendUser.description} ')}(id={onlineUser.backendUser.uid})">
                                                <be:avatar backendUser="{onlineUser.backendUser.uid}" showIcon="true" />
                                        </a>
                                </td>
index 813ea0d..f49d8b9 100644 (file)
@@ -3,7 +3,7 @@
 
 <tr>
        <td class="col-icon">
-               <a href="#" class="t3-js-clickmenutrigger" data-table="be_groups" data-uid="{backendUserGroup.uid}" data-listframe="1" title="{f:if(condition: '{backendUserGroup.description}', then: '{backendUserGroup.description} ')}(id={backendUserGroup.uid})">
+               <a href="#" class="t3js-contextmenutrigger" data-table="be_groups" data-uid="{backendUserGroup.uid}" title="{f:if(condition: '{backendUserGroup.description}', then: '{backendUserGroup.description} ')}(id={backendUserGroup.uid})">
                        <bu:spriteIconForRecord table="be_groups" object="{backendUserGroup}" />
                </a>
        </td>
index d7d383c..0bdfe41 100644 (file)
@@ -16,7 +16,7 @@
                                        <th></th>
                                        <f:for each="{compareUserList}" as="compareUser">
                                                <th>
-                                                       <a href="#" class="t3-js-clickmenutrigger" data-table="be_users" data-uid="{compareUser.uid}" data-listframe="1" title="id={compareUser.uid}">
+                                                       <a href="#" class="t3js-contextmenutrigger" data-table="be_users" data-uid="{compareUser.uid}" title="id={compareUser.uid}">
                                                                <be:avatar backendUser="{compareUser.uid}" showIcon="true" />
                                                        </a>
                                                        {compareUser.userName}
                                        <f:for each="{compareUserList}" as="compareUser">
                                                <td>
                                                        <f:for each="{compareUser.BackendUserGroups}" as="backendUserGroup">
-                                                               <a href="#" class="t3-js-clickmenutrigger" data-table="be_groups" data-uid="{backendUserGroup.uid}" data-listframe="1" title="id={backendUserGroup.uid}">
+                                                               <a href="#" class="t3js-contextmenutrigger" data-table="be_groups" data-uid="{backendUserGroup.uid}" title="id={backendUserGroup.uid}">
                                                                        <bu:spriteIconForRecord table="be_users" object="{backendUserGroup}" />
                                                                </a>
                                                                {backendUserGroup.title}<br />
index 335a11a..97ad80b 100644 (file)
@@ -21,7 +21,7 @@
                                        <f:for each="{compareUserList}" as="compareUser">
                                                <tr>
                                                        <td>
-                                                               <a href="#" class="t3-js-clickmenutrigger" data-table="be_users" data-uid="{compareUser.uid}" data-listframe="1" title="id={compareUser.uid}">
+                                                               <a href="#" class="t3js-contextmenutrigger" data-table="be_users" data-uid="{compareUser.uid}" title="id={compareUser.uid}">
                                                                        <be:avatar backendUser="{compareUser.uid}" showIcon="TRUE" />
                                                                </a>
                                                        </td>
diff --git a/typo3/sysext/beuser/Resources/Public/JavaScript/ContextMenuActions.js b/typo3/sysext/beuser/Resources/Public/JavaScript/ContextMenuActions.js
new file mode 100644 (file)
index 0000000..b8e14f9
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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!
+ */
+
+/**
+ * Module: TYPO3/CMS/Beuser/ContextMenuActions
+ *
+ * JavaScript to handle permissions module from context menu
+ * @exports TYPO3/CMS/Beuser/ContextMenuActions
+ */
+define(function () {
+    'use strict';
+
+    /**
+     * @exports TYPO3/CMS/Beuser/ContextMenuActions
+     */
+    var ContextMenuActions = {};
+
+    /**
+     * Open permission module for given uid
+     *
+     * @param {string} table
+     * @param {int} uid of the page
+     */
+    ContextMenuActions.openPermissionsModule = function (table, uid) {
+        if (table === 'pages') {
+            top.TYPO3.Backend.ContentContainer.setUrl(
+                top.TYPO3.settings.AccessPermissions.moduleUrl +
+                    '&id=' + uid +
+                    '&tx_beuser_system_beusertxpermission[id]=' + uid +
+                    '&tx_beuser_system_beusertxpermission[action]=edit' +
+                    '&tx_beuser_system_beusertxpermission[controller]=Permission'
+            );
+        }
+    };
+
+    return ContextMenuActions;
+});
index 2bcbf61..eebb392 100644 (file)
@@ -2,3 +2,5 @@
 defined('TYPO3_MODE') or die();
 
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'][] = \TYPO3\CMS\Beuser\Hook\SwitchBackUserHook::class . '->switchBack';
+
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructPostProcess'][] = \TYPO3\CMS\Beuser\Hook\BackendControllerHook::class . '->addJavaScript';
index a152b82..3fb949c 100644 (file)
@@ -35,4 +35,6 @@ if (TYPO3_MODE === 'BE') {
             'navigationComponentId' => 'typo3-pagetree'
         ]
     );
+
+    $GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders'][1486418730] = \TYPO3\CMS\Beuser\ContextMenu\ItemProvider::class;
 }
index 2ad8ab6..158306c 100644 (file)
@@ -540,10 +540,6 @@ class Bootstrap
                 'TYPO3.Components.PageTree.Commands',
                 \TYPO3\CMS\Backend\Tree\Pagetree\ExtdirectTreeCommands::class
             );
-            ExtensionManagementUtility::registerExtDirectComponent(
-                'TYPO3.Components.PageTree.ContextMenuDataProvider',
-                \TYPO3\CMS\Backend\ContextMenu\Pagetree\Extdirect\ContextMenuConfiguration::class
-            );
         }
         return $this;
     }
index 1891113..e2af156 100644 (file)
@@ -797,243 +797,17 @@ return [
 
                        options.contextMenu {
                                table {
-                                       virtual_root {
+                                       pages {
                                                disableItems =
-
-                                               items {
-                                                       100 = ITEM
-                                                       100 {
-                                                               name = history
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_history
-                                                               iconName = actions-document-history-open
-                                                               displayCondition = canShowHistory != 0
-                                                               callbackAction = openHistoryPopUp
-                                                       }
-                                               }
+                                               tree.disableItems =
                                        }
-
-                                       pages_root {
+                                       sys_file {
                                                disableItems =
-
-                                               items {
-                                                       100 = ITEM
-                                                       100 {
-                                                               name = view
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.view
-                                                               iconName = actions-document-view
-                                                               displayCondition = canBeViewed != 0
-                                                               callbackAction = viewPage
-                                                       }
-
-                                                       200 = ITEM
-                                                       200 {
-                                                               name = new
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.new
-                                                               iconName = actions-page-new
-                                                               displayCondition = canCreateNewPages != 0
-                                                               callbackAction = newPageWizard
-                                                       }
-
-                                                       300 = DIVIDER
-
-                                                       400 = ITEM
-                                                       400 {
-                                                               name = history
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_history
-                                                               iconName = actions-document-history-open
-                                                               displayCondition = canShowHistory != 0
-                                                               callbackAction = openHistoryPopUp
-                                                       }
-                                               }
+                                               tree.disableItems =
                                        }
-
-                                       pages {
+                                       sys_filemounts {
                                                disableItems =
-
-                                               items {
-                                                       100 = ITEM
-                                                       100 {
-                                                               name = view
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.view
-                                                               iconName = actions-document-view
-                                                               displayCondition = canBeViewed != 0
-                                                               callbackAction = viewPage
-                                                       }
-
-                                                       200 = DIVIDER
-
-                                                       300 = ITEM
-                                                       300 {
-                                                               name = disable
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:disable
-                                                               iconName = actions-edit-hide
-                                                               displayCondition = getRecord|hidden = 0 && canBeDisabledAndEnabled != 0
-                                                               callbackAction = disablePage
-                                                       }
-
-                                                       400 = ITEM
-                                                       400 {
-                                                               name = enable
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:enable
-                                                               iconName = actions-edit-unhide
-                                                               displayCondition = getRecord|hidden = 1 && canBeDisabledAndEnabled != 0
-                                                               callbackAction = enablePage
-                                                       }
-
-                                                       500 = ITEM
-                                                       500 {
-                                                               name = edit
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.edit
-                                                               iconName = actions-page-open
-                                                               displayCondition = canBeEdited != 0
-                                                               callbackAction = editPageProperties
-                                                       }
-
-                                                       600 = ITEM
-                                                       600 {
-                                                               name = info
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.info
-                                                               iconName = actions-document-info
-                                                               displayCondition = canShowInfo != 0
-                                                               callbackAction = openInfoPopUp
-                                                       }
-
-                                                       700 = ITEM
-                                                       700 {
-                                                               name = history
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_misc.xlf:CM_history
-                                                               iconName = actions-document-history-open
-                                                               displayCondition = canShowHistory != 0
-                                                               callbackAction = openHistoryPopUp
-                                                       }
-
-                                                       800 = DIVIDER
-
-                                                       900 = SUBMENU
-                                                       900 {
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.copyPasteActions
-
-                                                               100 = ITEM
-                                                               100 {
-                                                                       name = new
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.new
-                                                                       iconName = actions-page-new
-                                                                       displayCondition = canCreateNewPages != 0
-                                                                       callbackAction = newPageWizard
-                                                               }
-
-                                                               200 = DIVIDER
-
-                                                               300 = ITEM
-                                                               300 {
-                                                                       name = cut
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.cut
-                                                                       iconName = actions-edit-cut
-                                                                       displayCondition = isInCutMode = 0 && canBeCut != 0 && isMountPoint != 1
-                                                                       callbackAction = enableCutMode
-                                                               }
-
-                                                               400 = ITEM
-                                                               400 {
-                                                                       name = cut
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.cut
-                                                                       iconName = actions-edit-cut-release
-                                                                       displayCondition = isInCutMode = 1 && canBeCut != 0
-                                                                       callbackAction = disableCutMode
-                                                               }
-
-                                                               500 = ITEM
-                                                               500 {
-                                                                       name = copy
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.copy
-                                                                       iconName = actions-edit-copy
-                                                                       displayCondition = isInCopyMode = 0
-                                                                       callbackAction = enableCopyMode
-                                                               }
-
-                                                               600 = ITEM
-                                                               600 {
-                                                                       name = copy
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.copy
-                                                                       iconName = actions-edit-copy-release
-                                                                       displayCondition = isInCopyMode = 1
-                                                                       callbackAction = disableCopyMode
-                                                               }
-
-                                                               700 = ITEM
-                                                               700 {
-                                                                       name = pasteInto
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.pasteinto
-                                                                       iconName = actions-document-paste-into
-                                                                       displayCondition = getContextInfo|inCopyMode = 1 || getContextInfo|inCutMode = 1 && canBePastedInto != 0
-                                                                       callbackAction = pasteIntoNode
-                                                               }
-
-                                                               800 = ITEM
-                                                               800 {
-                                                                       name = pasteAfter
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.pasteafter
-                                                                       iconName = actions-document-paste-after
-                                                                       displayCondition = getContextInfo|inCopyMode = 1 || getContextInfo|inCutMode = 1 && canBePastedAfter != 0
-                                                                       callbackAction = pasteAfterNode
-                                                               }
-
-                                                               900 = DIVIDER
-
-                                                               1000 = ITEM
-                                                               1000 {
-                                                                       name = delete
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.delete
-                                                                       iconName = actions-edit-delete
-                                                                       displayCondition = canBeRemoved != 0 && isMountPoint != 1
-                                                                       callbackAction = removeNode
-                                                               }
-
-                                                               1100 = DIVIDER
-
-                                                               1200 = ITEM
-                                                               1200 {
-                                                                       name = clearCache
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.clear_cache
-                                                                       iconName = actions-system-cache-clear
-                                                                       callbackAction = clearCacheOfPage
-                                                               }
-                                                       }
-
-                                                       1000 = SUBMENU
-                                                       1000 {
-                                                               label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.branchActions
-
-                                                               100 = ITEM
-                                                               100 {
-                                                                       name = mountAsTreeroot
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.tempMountPoint
-                                                                       iconName = actions-pagetree-mountroot
-                                                                       displayCondition = canBeTemporaryMountPoint != 0 && isMountPoint = 0
-                                                                       callbackAction = mountAsTreeRoot
-                                                               }
-
-                                                               200 = DIVIDER
-
-                                                               300 = ITEM
-                                                               300 {
-                                                                       name = expandBranch
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.expandBranch
-                                                                       iconName = actions-pagetree-expand
-                                                                       displayCondition =
-                                                                       callbackAction = expandBranch
-                                                               }
-
-                                                               400 = ITEM
-                                                               400 {
-                                                                       name = collapseBranch
-                                                                       label = LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:cm.collapseBranch
-                                                                       iconName = actions-pagetree-collapse
-                                                                       displayCondition =
-                                                                       callbackAction = collapseBranch
-                                                               }
-                                                       }
-                                               }
+                                               tree.disableItems =
                                        }
                                }
                        }
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-78192-RefactorClickMenuContextMenu.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-78192-RefactorClickMenuContextMenu.rst
new file mode 100644 (file)
index 0000000..d5881c9
--- /dev/null
@@ -0,0 +1,126 @@
+.. include:: ../../Includes.txt
+
+=====================================================
+Breaking: #78192 - Refactor click menu (context menu)
+=====================================================
+
+See :issue:`78192`
+
+Description
+===========
+
+Due to the refactoring and unification of the click/context menu handling in the TYPO3 Backend, a few breaking changes have been introduced.
+
+Classes removed
+---------------
+
+- :php:`\TYPO3\CMS\Backend\ClickMenu\ClickMenu`
+- :php:`\TYPO3\CMS\Backend\ContextMenu\ContextMenuAction`
+- :php:`\TYPO3\CMS\Backend\ContextMenu\ContextMenuActionCollection`
+- :php:`\TYPO3\CMS\Backend\ContextMenu\Pagetree\ContextMenuDataProvider`
+- :php:`\TYPO3\CMS\Backend\ContextMenu\Pagetree\Extdirect\ContextMenuConfiguration`
+- :php:`\TYPO3\CMS\Backend\Controller\ClickMenuController`
+- :php:`\TYPO3\CMS\Impexp\Clickmenu` (replaced by new hook implementation: :php:`TYPO3\CMS\Impexp\Hook\ContextMenuModifyItemsHook`)
+- :php:`\TYPO3\CMS\Impexp\Hook\ContextMenuDisableItemsHook`
+- :php:`\TYPO3\CMS\Version\ClickMenu\VersionClickMenu`
+
+ExtJS component removed
+-----------------------
+
+- The :js:`TYPO3.Components.PageTree.ContextMenu` component defined in contextmenu.js has been removed.
+- The `contextMenuProvider` property as well as `enableContextMenu` and `openContextMenu` methods of the :js:`TYPO3.Components.PageTree.Tree` component has been removed.
+
+Migration
+^^^^^^^^^
+Migrate your code to require js module for custom click menu actions.
+
+ClickMenu requireJS component removed
+-------------------------------------
+
+The `TYPO3/CMS/Backend/ClickMenu` requireJS component (ClickMenu.js) has been removed.
+
+Migration
+^^^^^^^^^
+
+Use the new requireJS component: `TYPO3/CMS/Backend/ContextMenu`.
+
+
+Page TSConfig change
+--------------------
+
+The page tree context menu configuration in Page TSConfig was removed (except for the `disableItems` part).
+The list of available menu items is now provided by `ItemProviders` e.g. :php:`\TYPO3\CMS\Backend\ContextMenu\ItemProviders\PageProvider`.
+
+The TSConfig options for disabling Clickmenu items has been streamlined.
+Also some items names has been changed (e.g. `new_wizard` is now called `newWizard`, `db_list` is now `openListModule`). Refer to the provider class for correct naming.
+
+Migration
+^^^^^^^^^
+
+Migrate TSConfig from:
+
+:typoscript:`options.contextMenu.folderList.disableItems` to :typoscript:`options.contextMenu.sys_file.disableItems`
+:typoscript:`options.contextMenu.folderTree.disableItems` to :typoscript:`options.contextMenu.sys_file.tree.disableItems`
+:typoscript:`options.contextMenu.pageList.disableItems` to :typoscript:`options.contextMenu.pages.disableItems`
+:typoscript:`options.contextMenu.pageTree.disableItems` to :typoscript:`options.contextMenu.pages.tree.disableItems`
+
+
+
+Hooks removed
+-------------
+
+The following two hooks have been removed:
+
+- :php:`$GLOBALS['TBE_MODULES_EXT']['xMOD_alt_clickmenu']['extendCMclasses']`
+- :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['backend']['contextMenu']['disableItems']`
+
+
+Migration
+^^^^^^^^^
+
+Use new ItemsProvider API for adding or modifying click menu items.
+See existing usage of thi API in the core :php:`TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileProvider` or :php:`\TYPO3\CMS\Beuser\ContextMenu\ItemProvider`.
+
+
+Legacy Tree
+-----------
+
+Support for drag & drop menu for LegacyTree.js of pages has been dropped.
+
+
+Changed markup (data attributes) for click menu
+-----------------------------------------------
+
+- `data-listFrame` has been replaced with optional attribute `data-context` attribute. Context is set to "tree" for click menu triggered from trees.
+- for files `data-table` now contains real table name "sys_file" while before it contained combined identifier e.g. `1:/fileadmin/file.jpg`.
+the `data-uid` attribute contains now the combined identifier of the file (before it was empty).
+Thus `data-uid` attribute value is not always an int.
+- the class which triggers context menu has changed from :js:`t3-js-clickmenutrigger` to :js:`t3js-contextmenutrigger`
+
+
+Migration
+^^^^^^^^^
+
+To trigger click menu for files, use correct class as well as table and uid in data attributes. Replace `data-listFrame="0"` with `data-context="tree"`, `data-listFrame="1"` can be removed (it's a default context now).
+
+
+Impact
+======
+
+Instantiating the PHP class will result in a fatal PHP error.
+Accessing removed JavaScript properties will result in a JavaScript error.
+
+Removed hooks will not influence menu rendering process.
+
+Affected Installations
+======================
+
+Any installation using removed PHP classes, JS components or hooks.
+
+Migration
+=========
+
+Adapt code to new click menu API.
+
+
+.. index:: Backend, JavaScript, PHP-API, TSConfig
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-78192-RefactorClickMenuContextMenu.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-78192-RefactorClickMenuContextMenu.rst
new file mode 100644 (file)
index 0000000..3b5a3fd
--- /dev/null
@@ -0,0 +1,97 @@
+.. include:: ../../Includes.txt
+
+====================================================
+Feature: #78192 - Refactor click menu (context menu)
+====================================================
+
+See :issue:`78192`
+
+Description
+===========
+
+Click menu (context menu) handling has been refactored and unified.
+The ExtJS/ExtDirect click menu used on the page tree has been replaced with jQuery based implementation.
+The same context menu implementation is used in all places in the Backend (page tree, page module, list module, file list, folder tree...).
+
+Context menu rendering flow
+---------------------------
+
+Context menu is shown after click on the HTML element which has `class="t3js-contextmenutrigger"` together with `data-table`, `data-uid` and optional `data-context` attributes.
+
+JavaScript click event handler is implemented in `TYPO3/CMS/Backend/ContextMenu` requireJS module. It takes data attributes mentioned above and executes an ajax call to the :php:`\TYPO3\CMS\Backend\Controller\ContextMenuController->getContextMenuAction()`.
+
+:php:`ContextMenuController` asks :php:`\TYPO3\CMS\Backend\ContextMenu\ContextMenu` to generate an array of items. ContextMenu builds a list of available item providers by asking each whether it can provide items (:php:`->canHandle()`), and what priority it has (:php:`->getPriority()`).
+
+Custom item providers can be registered in :php:`$GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders']`. They must implement :php:`\TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface` and can extend :php:`\TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider`.
+
+A list of providers is sorted by priority, and then each provider is asked to add items. The generated array of items is passed from an item provider with higher priority to a provider with lower priority.
+
+After that, a compiled list of items is returned to the :php:`ContextMenuController` which passes it as JSON back to the ContextMenu.js.
+
+Example of the JSON response:
+
+.. code:: javascript
+
+    {
+       "view":{
+          "type":"item",
+          "label":"Show",
+          "icon":"<span class=\"t3js-icon icon icon-size-small icon-state-default icon-actions-document-view\" data-identifier=\"actions-document-view\">\n\t<span class=\"icon-markup\">\n<img src=\"\/typo3\/sysext\/core\/Resources\/Public\/Icons\/T3Icons\/actions\/actions-document-view.svg\" width=\"16\" height=\"16\" \/>\n\t<\/span>\n\t\n<\/span>",
+          "additionalAttributes":{
+             "data-preview-url":"http:\/\/typo37.local\/index.php?id=47"
+          },
+          "callbackAction":"viewRecord"
+       },
+       "edit":{
+          "type":"item",
+          "label":"Edit",
+          "icon":"",
+          "additionalAttributes":[
+          ],
+          "callbackAction":"editRecord"
+       },
+       "divider1":{
+          "type":"divider",
+          "label":"",
+          "icon":"",
+          "additionalAttributes":[
+
+          ],
+          "callbackAction":""
+       },
+       "more":{
+          "type":"submenu",
+          "label":"More options...",
+          "icon":"",
+          "additionalAttributes":[
+
+          ],
+          "callbackAction":"openSubmenu",
+          "childItems":{
+             "newWizard":{
+                "type":"item",
+                "label":"'Create New' wizard",
+                "icon":"",
+                "additionalAttributes":{
+                },
+                "callbackAction":"newContentWizard"
+             }
+          }
+       }
+    }
+
+
+Based on the JSON data ContextMenu.js is rendering a context menu. If one of the items is clicked, the according JS `callbackAction` is executed on the :js:`TYPO3/CMS/Backend/ContextMenuActions` JS module or other module defined in the `additionalAttributes['data-callback-module']`.
+
+For example usage of this API see:
+
+ - Beuser item provider :php:`\TYPO3\CMS\Beuser\ContextMenu\ItemProvider` and requireJS module :js:`TYPO3/CMS/Beuser/ContextMenuActions`
+ - Impexp item provider :php:`\TYPO3\CMS\Impexp\ContextMenu\ItemProvider` and requireJS module :js:`TYPO3/CMS/Impexp/ContextMenuActions`
+ - Version item provider :php:`\TYPO3\CMS\Version\ContextMenu\ItemProvider` and requireJS module :js:`TYPO3/CMS/Version/ContextMenuActions`
+ - Version item provider :php:`\TYPO3\CMS\Version\ContextMenu\ItemProvider` and requireJS module :js:`TYPO3/CMS/Version/ContextMenuActions`
+ - Filelist item providers :php:`\TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileDragProvider`, :php:`\TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileProvider`,
+   :php:`\TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FileStorageProvider`, :php:`\TYPO3\CMS\Filelist\ContextMenu\ItemProviders\FilemountsProvider`
+   and requireJS module :js:`TYPO3/CMS/Filelist/ContextMenuActions`
+
+
+.. index:: Backend, JavaScript, PHP-API, TSConfig
\ No newline at end of file
index 3ec6698..1ec1d91 100644 (file)
@@ -37289,7 +37289,6 @@ Ext.extend(Ext.tree.TreeEditor, Ext.Editor, {
     beforeNodeClick : function(node, e){
         clearTimeout(this.autoEditTimer);
         if(this.tree.getSelectionModel().isSelected(node)){
-            e.stopEvent();
             return this.triggerEdit(node);
         }
     },
index b25016d..01baacd 100644 (file)
@@ -18,4 +18,4 @@ at http://www.sencha.com/contact.
 
 Build date: 2013-04-03 15:07:25
 */
-(function(){var h=Ext.util,j=Ext.each,g=true,i=false;h.Observable=function(){var k=this,l=k.events;if(k.listeners){k.on(k.listeners);delete k.listeners}k.events=l||{}};h.Observable.prototype={filterOptRe:/^(?:scope|delay|buffer|single)$/,fireEvent:function(){var k=Array.prototype.slice.call(arguments,0),m=k[0].toLowerCase(),n=this,l=g,p=n.events[m],s,o,r;if(n.eventsSuspended===g){if(o=n.eventQueue){o.push(k)}}else{if(typeof p=="object"){if(p.bubble){if(p.fire.apply(p,k.slice(1))===i){return i}r=n.getBubbleTarget&&n.getBubbleTarget();if(r&&r.enableBubble){s=r.events[m];if(!s||typeof s!="object"||!s.bubble){r.enableBubble(m)}return r.fireEvent.apply(r,k)}}else{k.shift();l=p.fire.apply(p,k)}}}return l},addListener:function(k,m,l,r){var n=this,q,s,p;if(typeof k=="object"){r=k;for(q in r){s=r[q];if(!n.filterOptRe.test(q)){n.addListener(q,s.fn||s,s.scope||r.scope,s.fn?s:r)}}}else{k=k.toLowerCase();p=n.events[k]||g;if(typeof p=="boolean"){n.events[k]=p=new h.Event(n,k)}p.addListener(m,l,typeof r=="object"?r:{})}},removeListener:function(k,m,l){var n=this.events[k.toLowerCase()];if(typeof n=="object"){n.removeListener(m,l)}},purgeListeners:function(){var m=this.events,k,l;for(l in m){k=m[l];if(typeof k=="object"){k.clearListeners()}}},addEvents:function(n){var m=this;m.events=m.events||{};if(typeof n=="string"){var k=arguments,l=k.length;while(l--){m.events[k[l]]=m.events[k[l]]||g}}else{Ext.applyIf(m.events,n)}},hasListener:function(k){var l=this.events[k.toLowerCase()];return typeof l=="object"&&l.listeners.length>0},suspendEvents:function(k){this.eventsSuspended=g;if(k&&!this.eventQueue){this.eventQueue=[]}},resumeEvents:function(){var k=this,l=k.eventQueue||[];k.eventsSuspended=i;delete k.eventQueue;j(l,function(m){k.fireEvent.apply(k,m)})}};var d=h.Observable.prototype;d.on=d.addListener;d.un=d.removeListener;h.Observable.releaseCapture=function(k){k.fireEvent=d.fireEvent};function e(l,m,k){return function(){if(m.target==arguments[0]){l.apply(k,Array.prototype.slice.call(arguments,0))}}}function b(n,p,k,m){k.task=new h.DelayedTask();return function(){k.task.delay(p.buffer,n,m,Array.prototype.slice.call(arguments,0))}}function c(m,n,l,k){return function(){n.removeListener(l,k);return m.apply(k,arguments)}}function a(n,p,k,m){return function(){var l=new h.DelayedTask(),o=Array.prototype.slice.call(arguments,0);if(!k.tasks){k.tasks=[]}k.tasks.push(l);l.delay(p.delay||10,function(){k.tasks.remove(l);n.apply(m,o)},m)}}h.Event=function(l,k){this.name=k;this.obj=l;this.listeners=[]};h.Event.prototype={addListener:function(o,n,m){var p=this,k;n=n||p.obj;if(!p.isListening(o,n)){k=p.createListener(o,n,m);if(p.firing){p.listeners=p.listeners.slice(0)}p.listeners.push(k)}},createListener:function(p,n,q){q=q||{};n=n||this.obj;var k={fn:p,scope:n,options:q},m=p;if(q.target){m=e(m,q,n)}if(q.delay){m=a(m,q,k,n)}if(q.single){m=c(m,this,p,n)}if(q.buffer){m=b(m,q,k,n)}k.fireFn=m;return k},findListener:function(o,n){var p=this.listeners,m=p.length,k;n=n||this.obj;while(m--){k=p[m];if(k){if(k.fn==o&&k.scope==n){return m}}}return -1},isListening:function(l,k){return this.findListener(l,k)!=-1},removeListener:function(r,q){var p,m,n,s=this,o=i;if((p=s.findListener(r,q))!=-1){if(s.firing){s.listeners=s.listeners.slice(0)}m=s.listeners[p];if(m.task){m.task.cancel();delete m.task}n=m.tasks&&m.tasks.length;if(n){while(n--){m.tasks[n].cancel()}delete m.tasks}s.listeners.splice(p,1);o=g}return o},clearListeners:function(){var n=this,k=n.listeners,m=k.length;while(m--){n.removeListener(k[m].fn,k[m].scope)}},fire:function(){var q=this,p=q.listeners,k=p.length,o=0,m;if(k>0){q.firing=g;var n=Array.prototype.slice.call(arguments,0);for(;o<k;o++){m=p[o];if(m&&m.fireFn.apply(m.scope||q.obj||window,n)===i){return(q.firing=i)}}}q.firing=i;return g}}})();Ext.DomHelper=function(){var x=null,k=/^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i,m=/^table|tbody|tr|td$/i,d=/tag|children|cn|html$/i,t=/td|tr|tbody/i,o=/([a-z0-9-]+)\s*:\s*([^;\s]+(?:\s*[^;\s]+)*);?/gi,v=/end/i,r,n="afterbegin",p="afterend",c="beforebegin",q="beforeend",a="<table>",i="</table>",b=a+"<tbody>",j="</tbody>"+i,l=b+"<tr>",w="</tr>"+j;function h(B,D,C,E,A,y){var z=r.insertHtml(E,Ext.getDom(B),u(D));return C?Ext.get(z,true):z}function u(D){var z="",y,C,B,E;if(typeof D=="string"){z=D}else{if(Ext.isArray(D)){for(var A=0;A<D.length;A++){if(D[A]){z+=u(D[A])}}}else{z+="<"+(D.tag=D.tag||"div");for(y in D){C=D[y];if(!d.test(y)){if(typeof C=="object"){z+=" "+y+'="';for(B in C){z+=B+":"+C[B]+";"}z+='"'}else{z+=" "+({cls:"class",htmlFor:"for"}[y]||y)+'="'+C+'"'}}}if(k.test(D.tag)){z+="/>"}else{z+=">";if((E=D.children||D.cn)){z+=u(E)}else{if(D.html){z+=D.html}}z+="</"+D.tag+">"}}}return z}function g(F,C,B,D){x.innerHTML=[C,B,D].join("");var y=-1,A=x,z;while(++y<F){A=A.firstChild}if(z=A.nextSibling){var E=document.createDocumentFragment();while(A){z=A.nextSibling;E.appendChild(A);A=z}A=E}return A}function e(y,z,B,A){var C,D;x=x||document.createElement("div");if(y=="td"&&(z==n||z==q)||!t.test(y)&&(z==c||z==p)){return}D=z==c?B:z==p?B.nextSibling:z==n?B.firstChild:null;if(z==c||z==p){B=B.parentNode}if(y=="td"||(y=="tr"&&(z==q||z==n))){C=g(4,l,A,w)}else{if((y=="tbody"&&(z==q||z==n))||(y=="tr"&&(z==c||z==p))){C=g(3,b,A,j)}else{C=g(2,a,A,i)}}B.insertBefore(C,D);return C}function s(A){var D=document.createElement("div"),y=document.createDocumentFragment(),z=0,B,C;D.innerHTML=A;C=D.childNodes;B=C.length;for(;z<B;z++){y.appendChild(C[z].cloneNode(true))}return y}r={markup:function(y){return u(y)},applyStyles:function(y,z){if(z){var A;y=Ext.fly(y);if(typeof z=="function"){z=z.call()}if(typeof z=="string"){o.lastIndex=0;while((A=o.exec(z))){y.setStyle(A[1],A[2])}}else{if(typeof z=="object"){y.setStyle(z)}}}},insertHtml:function(D,y,E){var B={},A,F,C,G,H,z;D=D.toLowerCase();B[c]=["BeforeBegin","previousSibling"];B[p]=["AfterEnd","nextSibling"];if(y.insertAdjacentHTML){if(m.test(y.tagName)&&(z=e(y.tagName.toLowerCase(),D,y,E))){return z}B[n]=["AfterBegin","firstChild"];B[q]=["BeforeEnd","lastChild"];if((A=B[D])){y.insertAdjacentHTML(A[0],E);return y[A[1]]}}else{F=y.ownerDocument.createRange();G="setStart"+(v.test(D)?"After":"Before");if(B[D]){F[G](y);if(!F.createContextualFragment){H=s(E)}else{H=F.createContextualFragment(E)}y.parentNode.insertBefore(H,D==c?y:y.nextSibling);return y[(D==c?"previous":"next")+"Sibling"]}else{C=(D==n?"first":"last")+"Child";if(y.firstChild){F[G](y[C]);if(!F.createContextualFragment){H=s(E)}else{H=F.createContextualFragment(E)}if(D==n){y.insertBefore(H,y.firstChild)}else{y.appendChild(H)}}else{y.innerHTML=E}return y[C]}}throw'Illegal insertion point -> "'+D+'"'},insertBefore:function(y,A,z){return h(y,A,z,c)},insertAfter:function(y,A,z){return h(y,A,z,p,"nextSibling")},insertFirst:function(y,A,z){return h(y,A,z,n,"firstChild")},append:function(y,A,z){return h(y,A,z,q,"",true)},overwrite:function(y,A,z){y=Ext.getDom(y);y.innerHTML=u(A);return z?Ext.get(y.firstChild):y.firstChild},createHtml:u};return r}();Ext.Template=function(h){var j=this,c=arguments,e=[],d;if(Ext.isArray(h)){h=h.join("")}else{if(c.length>1){for(var g=0,b=c.length;g<b;g++){d=c[g];if(typeof d=="object"){Ext.apply(j,d)}else{e.push(d)}}h=e.join("")}}j.html=h;if(j.compiled){j.compile()}};Ext.Template.prototype={re:/\{([\w\-]+)\}/g,applyTemplate:function(a){var b=this;return b.compiled?b.compiled(a):b.html.replace(b.re,function(c,d){return a[d]!==undefined?a[d]:""})},set:function(a,c){var b=this;b.html=a;b.compiled=null;return c?b.compile():b},compile:function(){var me=this,sep=Ext.isGecko?"+":",";function fn(m,name){name="values['"+name+"']";return"'"+sep+"("+name+" == undefined ? '' : "+name+")"+sep+"'"}eval("this.compiled = function(values){ return "+(Ext.isGecko?"'":"['")+me.html.replace(/\\/g,"\\\\").replace(/(\r\n|\n)/g,"\\n").replace(/'/g,"\\'").replace(this.re,fn)+(Ext.isGecko?"';};":"'].join('');};"));return me},insertFirst:function(b,a,c){return this.doInsert("afterBegin",b,a,c)},insertBefore:function(b,a,c){return this.doInsert("beforeBegin",b,a,c)},insertAfter:function(b,a,c){return this.doInsert("afterEnd",b,a,c)},append:function(b,a,c){return this.doInsert("beforeEnd",b,a,c)},doInsert:function(c,e,b,a){e=Ext.getDom(e);var d=Ext.DomHelper.insertHtml(c,e,this.applyTemplate(b));return a?Ext.get(d,true):d},overwrite:function(b,a,c){b=Ext.getDom(b);b.innerHTML=this.applyTemplate(a);return c?Ext.get(b.firstChild,true):b.firstChild}};Ext.Template.prototype.apply=Ext.Template.prototype.applyTemplate;Ext.Template.from=function(b,a){b=Ext.getDom(b);return new Ext.Template(b.value||b.innerHTML,a||"")};Ext.DomQuery=function(){var cache={},simpleCache={},valueCache={},nonSpace=/\S/,trimRe=/^\s+|\s+$/g,tplRe=/\{(\d+)\}/g,modeRe=/^(\s?[\/>+~]\s?|\s|$)/,tagTokenRe=/^(#)?([\w\-\*]+)/,nthRe=/(\d*)n\+?(\d*)/,nthRe2=/\D/,isIE=window.ActiveXObject?true:false,key=30803;eval("var batch = 30803;");function child(parent,index){var i=0,n=parent.firstChild;while(n){if(n.nodeType==1){if(++i==index){return n}}n=n.nextSibling}return null}function next(n){while((n=n.nextSibling)&&n.nodeType!=1){}return n}function prev(n){while((n=n.previousSibling)&&n.nodeType!=1){}return n}function children(parent){var n=parent.firstChild,nodeIndex=-1,nextNode;while(n){nextNode=n.nextSibling;if(n.nodeType==3&&!nonSpace.test(n.nodeValue)){parent.removeChild(n)}else{n.nodeIndex=++nodeIndex}n=nextNode}return this}function byClassName(nodeSet,cls){if(!cls){return nodeSet}var result=[],ri=-1;for(var i=0,ci;ci=nodeSet[i];i++){if((" "+ci.className+" ").indexOf(cls)!=-1){result[++ri]=ci}}return result}function attrValue(n,attr){if(!n.tagName&&typeof n.length!="undefined"){n=n[0]}if(!n){return null}if(attr=="for"){return n.htmlFor}if(attr=="class"||attr=="className"){return n.className}return n.getAttribute(attr)||n[attr]}function getNodes(ns,mode,tagName){var result=[],ri=-1,cs;if(!ns){return result}tagName=tagName||"*";if(typeof ns.getElementsByTagName!="undefined"){ns=[ns]}if(!mode){for(var i=0,ni;ni=ns[i];i++){cs=ni.getElementsByTagName(tagName);for(var j=0,ci;ci=cs[j];j++){result[++ri]=ci}}}else{if(mode=="/"||mode==">"){var utag=tagName.toUpperCase();for(var i=0,ni,cn;ni=ns[i];i++){cn=ni.childNodes;for(var j=0,cj;cj=cn[j];j++){if(cj.nodeName==utag||cj.nodeName==tagName||tagName=="*"){result[++ri]=cj}}}}else{if(mode=="+"){var utag=tagName.toUpperCase();for(var i=0,n;n=ns[i];i++){while((n=n.nextSibling)&&n.nodeType!=1){}if(n&&(n.nodeName==utag||n.nodeName==tagName||tagName=="*")){result[++ri]=n}}}else{if(mode=="~"){var utag=tagName.toUpperCase();for(var i=0,n;n=ns[i];i++){while((n=n.nextSibling)){if(n.nodeName==utag||n.nodeName==tagName||tagName=="*"){result[++ri]=n}}}}}}}return result}function concat(a,b){if(b.slice){return a.concat(b)}for(var i=0,l=b.length;i<l;i++){a[a.length]=b[i]}return a}function byTag(cs,tagName){if(cs.tagName||cs==document){cs=[cs]}if(!tagName){return cs}var result=[],ri=-1;tagName=tagName.toLowerCase();for(var i=0,ci;ci=cs[i];i++){if(ci.nodeType==1&&ci.tagName.toLowerCase()==tagName){result[++ri]=ci}}return result}function byId(cs,id){if(cs.tagName||cs==document){cs=[cs]}if(!id){return cs}var result=[],ri=-1;for(var i=0,ci;ci=cs[i];i++){if(ci&&ci.id==id){result[++ri]=ci;return result}}return result}function byAttribute(cs,attr,value,op,custom){var result=[],ri=-1,useGetStyle=custom=="{",fn=Ext.DomQuery.operators[op],a,xml,hasXml;for(var i=0,ci;ci=cs[i];i++){if(ci.nodeType!=1){continue}if(!hasXml){xml=Ext.DomQuery.isXml(ci);hasXml=true}if(!xml){if(useGetStyle){a=Ext.DomQuery.getStyle(ci,attr)}else{if(attr=="class"||attr=="className"){a=ci.className}else{if(attr=="for"){a=ci.htmlFor}else{if(attr=="href"){a=ci.getAttribute("href",2)}else{a=ci.getAttribute(attr)}}}}}else{a=ci.getAttribute(attr)}if((fn&&fn(a,value))||(!fn&&a)){result[++ri]=ci}}return result}function byPseudo(cs,name,value){return Ext.DomQuery.pseudos[name](cs,value)}function nodupIEXml(cs){var d=++key,r;cs[0].setAttribute("_nodup",d);r=[cs[0]];for(var i=1,len=cs.length;i<len;i++){var c=cs[i];if(!c.getAttribute("_nodup")!=d){c.setAttribute("_nodup",d);r[r.length]=c}}for(var i=0,len=cs.length;i<len;i++){cs[i].removeAttribute("_nodup")}return r}function nodup(cs){if(!cs){return[]}var len=cs.length,c,i,r=cs,cj,ri=-1;if(!len||typeof cs.nodeType!="undefined"||len==1){return cs}if(isIE&&typeof cs[0].selectSingleNode!="undefined"){return nodupIEXml(cs)}var d=++key;cs[0]._nodup=d;for(i=1;c=cs[i];i++){if(c._nodup!=d){c._nodup=d}else{r=[];for(var j=0;j<i;j++){r[++ri]=cs[j]}for(j=i+1;cj=cs[j];j++){if(cj._nodup!=d){cj._nodup=d;r[++ri]=cj}}return r}}return r}function quickDiffIEXml(c1,c2){var d=++key,r=[];for(var i=0,len=c1.length;i<len;i++){c1[i].setAttribute("_qdiff",d)}for(var i=0,len=c2.length;i<len;i++){if(c2[i].getAttribute("_qdiff")!=d){r[r.length]=c2[i]}}for(var i=0,len=c1.length;i<len;i++){c1[i].removeAttribute("_qdiff")}return r}function quickDiff(c1,c2){var len1=c1.length,d=++key,r=[];if(!len1){return c2}if(isIE&&typeof c1[0].selectSingleNode!="undefined"){return quickDiffIEXml(c1,c2)}for(var i=0;i<len1;i++){c1[i]._qdiff=d}for(var i=0,len=c2.length;i<len;i++){if(c2[i]._qdiff!=d){r[r.length]=c2[i]}}return r}function quickId(ns,mode,root,id){if(ns==root){var d=root.ownerDocument||root;return d.getElementById(id)}ns=getNodes(ns,mode,"*");return byId(ns,id)}return{getStyle:function(el,name){return Ext.fly(el).getStyle(name)},compile:function(path,type){type=type||"select";var fn=["var f = function(root){\n var mode; ++batch; var n = root || document;\n"],mode,lastPath,matchers=Ext.DomQuery.matchers,matchersLn=matchers.length,modeMatch,lmode=path.match(modeRe);if(lmode&&lmode[1]){fn[fn.length]='mode="'+lmode[1].replace(trimRe,"")+'";';path=path.replace(lmode[1],"")}while(path.substr(0,1)=="/"){path=path.substr(1)}while(path&&lastPath!=path){lastPath=path;var tokenMatch=path.match(tagTokenRe);if(type=="select"){if(tokenMatch){if(tokenMatch[1]=="#"){fn[fn.length]='n = quickId(n, mode, root, "'+tokenMatch[2]+'");'}else{fn[fn.length]='n = getNodes(n, mode, "'+tokenMatch[2]+'");'}path=path.replace(tokenMatch[0],"")}else{if(path.substr(0,1)!="@"){fn[fn.length]='n = getNodes(n, mode, "*");'}}}else{if(tokenMatch){if(tokenMatch[1]=="#"){fn[fn.length]='n = byId(n, "'+tokenMatch[2]+'");'}else{fn[fn.length]='n = byTag(n, "'+tokenMatch[2]+'");'}path=path.replace(tokenMatch[0],"")}}while(!(modeMatch=path.match(modeRe))){var matched=false;for(var j=0;j<matchersLn;j++){var t=matchers[j];var m=path.match(t.re);if(m){fn[fn.length]=t.select.replace(tplRe,function(x,i){return m[i]});path=path.replace(m[0],"");matched=true;break}}if(!matched){throw'Error parsing selector, parsing failed at "'+path+'"'}}if(modeMatch[1]){fn[fn.length]='mode="'+modeMatch[1].replace(trimRe,"")+'";';path=path.replace(modeMatch[1],"")}}fn[fn.length]="return nodup(n);\n}";eval(fn.join(""));return f},jsSelect:function(path,root,type){root=root||document;if(typeof root=="string"){root=document.getElementById(root)}var paths=path.split(","),results=[];for(var i=0,len=paths.length;i<len;i++){var subPath=paths[i].replace(trimRe,"");if(!cache[subPath]){cache[subPath]=Ext.DomQuery.compile(subPath);if(!cache[subPath]){throw subPath+" is not a valid selector"}}var result=cache[subPath](root);if(result&&result!=document){results=results.concat(result)}}if(paths.length>1){return nodup(results)}return results},isXml:function(el){var docEl=(el?el.ownerDocument||el:0).documentElement;return docEl?docEl.nodeName!=="HTML":false},select:document.querySelectorAll?function(path,root,type){root=root||document;if(!Ext.DomQuery.isXml(root)){try{var cs=root.querySelectorAll(path);return Ext.toArray(cs)}catch(ex){}}return Ext.DomQuery.jsSelect.call(this,path,root,type)}:function(path,root,type){return Ext.DomQuery.jsSelect.call(this,path,root,type)},selectNode:function(path,root){return Ext.DomQuery.select(path,root)[0]},selectValue:function(path,root,defaultValue){path=path.replace(trimRe,"");if(!valueCache[path]){valueCache[path]=Ext.DomQuery.compile(path,"select")}var n=valueCache[path](root),v;n=n[0]?n[0]:n;if(typeof n.normalize=="function"){n.normalize()}v=(n&&n.firstChild?n.firstChild.nodeValue:null);return((v===null||v===undefined||v==="")?defaultValue:v)},selectNumber:function(path,root,defaultValue){var v=Ext.DomQuery.selectValue(path,root,defaultValue||0);return parseFloat(v)},is:function(el,ss){if(typeof el=="string"){el=document.getElementById(el)}var isArray=Ext.isArray(el),result=Ext.DomQuery.filter(isArray?el:[el],ss);return isArray?(result.length==el.length):(result.length>0)},filter:function(els,ss,nonMatches){ss=ss.replace(trimRe,"");if(!simpleCache[ss]){simpleCache[ss]=Ext.DomQuery.compile(ss,"simple")}var result=simpleCache[ss](els);return nonMatches?quickDiff(result,els):result},matchers:[{re:/^\.([\w\-]+)/,select:'n = byClassName(n, " {1} ");'},{re:/^\:([\w\-]+)(?:\(((?:[^\s>\/]*|.*?))\))?/,select:'n = byPseudo(n, "{1}", "{2}");'},{re:/^(?:([\[\{])(?:@)?([\w\-]+)\s?(?:(=|.=)\s?(["']?)(.*?)\4)?[\]\}])/,select:'n = byAttribute(n, "{2}", "{5}", "{3}", "{1}");'},{re:/^#([\w\-]+)/,select:'n = byId(n, "{1}");'},{re:/^@([\w\-]+)/,select:'return {firstChild:{nodeValue:attrValue(n, "{1}")}};'}],operators:{"=":function(a,v){return a==v},"!=":function(a,v){return a!=v},"^=":function(a,v){return a&&a.substr(0,v.length)==v},"$=":function(a,v){return a&&a.substr(a.length-v.length)==v},"*=":function(a,v){return a&&a.indexOf(v)!==-1},"%=":function(a,v){return(a%v)==0},"|=":function(a,v){return a&&(a==v||a.substr(0,v.length+1)==v+"-")},"~=":function(a,v){return a&&(" "+a+" ").indexOf(" "+v+" ")!=-1}},pseudos:{"first-child":function(c){var r=[],ri=-1,n;for(var i=0,ci;ci=n=c[i];i++){while((n=n.previousSibling)&&n.nodeType!=1){}if(!n){r[++ri]=ci}}return r},"last-child":function(c){var r=[],ri=-1,n;for(var i=0,ci;ci=n=c[i];i++){while((n=n.nextSibling)&&n.nodeType!=1){}if(!n){r[++ri]=ci}}return r},"nth-child":function(c,a){var r=[],ri=-1,m=nthRe.exec(a=="even"&&"2n"||a=="odd"&&"2n+1"||!nthRe2.test(a)&&"n+"+a||a),f=(m[1]||1)-0,l=m[2]-0;for(var i=0,n;n=c[i];i++){var pn=n.parentNode;if(batch!=pn._batch){var j=0;for(var cn=pn.firstChild;cn;cn=cn.nextSibling){if(cn.nodeType==1){cn.nodeIndex=++j}}pn._batch=batch}if(f==1){if(l==0||n.nodeIndex==l){r[++ri]=n}}else{if((n.nodeIndex+l)%f==0){r[++ri]=n}}}return r},"only-child":function(c){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(!prev(ci)&&!next(ci)){r[++ri]=ci}}return r},empty:function(c){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){var cns=ci.childNodes,j=0,cn,empty=true;while(cn=cns[j]){++j;if(cn.nodeType==1||cn.nodeType==3){empty=false;break}}if(empty){r[++ri]=ci}}return r},contains:function(c,v){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if((ci.textContent||ci.innerText||"").indexOf(v)!=-1){r[++ri]=ci}}return r},nodeValue:function(c,v){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(ci.firstChild&&ci.firstChild.nodeValue==v){r[++ri]=ci}}return r},checked:function(c){var r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(ci.checked==true){r[++ri]=ci}}return r},not:function(c,ss){return Ext.DomQuery.filter(c,ss,true)},any:function(c,selectors){var ss=selectors.split("|"),r=[],ri=-1,s;for(var i=0,ci;ci=c[i];i++){for(var j=0;s=ss[j];j++){if(Ext.DomQuery.is(ci,s)){r[++ri]=ci;break}}}return r},odd:function(c){return this["nth-child"](c,"odd")},even:function(c){return this["nth-child"](c,"even")},nth:function(c,a){return c[a-1]||[]},first:function(c){return c[0]||[]},last:function(c){return c[c.length-1]||[]},has:function(c,ss){var s=Ext.DomQuery.select,r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){if(s(ss,ci).length>0){r[++ri]=ci}}return r},next:function(c,ss){var is=Ext.DomQuery.is,r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){var n=next(ci);if(n&&is(n,ss)){r[++ri]=ci}}return r},prev:function(c,ss){var is=Ext.DomQuery.is,r=[],ri=-1;for(var i=0,ci;ci=c[i];i++){var n=prev(ci);if(n&&is(n,ss)){r[++ri]=ci}}return r}}}}();Ext.query=Ext.DomQuery.select;Ext.util.DelayedTask=function(d,c,a){var e=this,g,b=function(){clearInterval(g);g=null;d.apply(c,a||[])};e.delay=function(i,k,j,h){e.cancel();d=k||d;c=j||c;a=h||a;g=setInterval(b,i)};e.cancel=function(){if(g){clearInterval(g);g=null}}};(function(){var h=document;Ext.Element=f