[TASK] Rewrite Module Menu 76/31876/8
authorBenjamin Mack <benni@typo3.org>
Mon, 28 Jul 2014 06:33:47 +0000 (08:33 +0200)
committerBenjamin Mack <benni@typo3.org>
Mon, 8 Sep 2014 21:03:01 +0000 (23:03 +0200)
Currently rendering of the module menu of the backend left side is
quite complex. This is because there are several parts introduced
in recent TYPO3 versions, that have only substituted parts of the
existing code. So the "old" code was still in use. To give a better
understanding I will first introduce all the parts relevant to the
rendering of the module menu.

PHP side
 * The mother of all classes is ModuleLoader.php, originally made by
   Kasper, which takes TBE_MODULES and compiles a long list of all
   modules into an array. (ModuleLoader->load()). Currently this class
   is kept as is for the time being.
 * For 4.1/4.2, the ModuleMenuView was introduced, a wrapper class to
   access the data of ModuleLoader and also does some other stuff like
   saving the Open/Collapsed state of the modules via AJAX (note: this
   will be handled browser-internally via LocalStorage from now on, as
   it saves HTTP requests).
   It does one other thing: Rendering of the Logout Button.
   This is completely in the wrong place right now, as the Logout
   button isn't within the module menu frame anymore since 4.x,
   but on the top right area of the backend.
 * The BackendController class is responsible for adding all HTML and
   JS code code to the backend.php page so it can be rendered. It
   instantiates the ModuleMenuView class to call the LogoutButton
   function.
 * As one of the first steps of rewriting the module menu a couple of
   versions ago, there was a ModuleController, which was again a
   wrapper for ModuleLoader while putting everything together in a
   ModuleRepository, along with Module objects and a singleton object
   ModuleStorage. This new version allows for nesting of up to three
   levels (incl. modfuncs), along with clear objects instead of arrays.

JS side
 * The module menu isn't output with a single line of PHP code, as it
   is defined as a ExtJS storage which fetches the module menu
   asynchronously. The HTML code is built in a ExtJS template, which
   leads to the funny effect that the raw backend.php only zero HTML
   code output for the module menu.
   An extra AJAX request is fired to load up the module, which then is
   prepared and templated by ExtJS.

The changes:
 * The logout button rendering is now done in the BackendController
   and not in the ModuleMenuView class.
 * The menu is now outputted in the BackendController at the same
   time as backend.php by a standalone Fluid template. The menu is
   built by a simple call to the ModuleRepository which returns a
   ModuleStorage.
 * ModuleMenuView, ModuleMenuController are not needed anymore, and
   are deprecated.
 * The JS is rewritten so that it works as before. These parts are filled
   up with jQuery code.
 * The collapsed/open state of the main modules is now done via the
   local storage JS functionality of the browsers to avoid further
   unnecessary HTTP loading operations.

Resolves: #60633
Releases: 6.3
Change-Id: I5402c1345a2931340d7ec9ef1881877b39d1bfc3
Reviewed-on: http://review.typo3.org/31876
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Benjamin Mack <benni@typo3.org>
Tested-by: Benjamin Mack <benni@typo3.org>
typo3/js/extjs/viewportConfiguration.js
typo3/sysext/backend/Classes/Controller/BackendController.php
typo3/sysext/backend/Classes/Domain/Model/Module/BackendModule.php
typo3/sysext/backend/Classes/Domain/Repository/Module/BackendModuleRepository.php
typo3/sysext/backend/Classes/Module/ModuleController.php
typo3/sysext/backend/Classes/View/ModuleMenuView.php
typo3/sysext/backend/Resources/Private/Templates/ModuleMenu/Main.html [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/modulemenu.js
typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/t3skin/Resources/Public/Css/structure/module_menu.css

index 5628338..5d53907 100644 (file)
@@ -68,6 +68,7 @@ TYPO3.Viewport.configuration = {
                        layout: 'fit',
                        region: 'west',
                        id: 'typo3-module-menu',
+                       contentEl: 'typo3-menu',
                        collapsible: false,
                        collapseMode: null,
                        floatable: true,
index e36084d..cfb0b91 100644 (file)
@@ -54,13 +54,6 @@ class BackendController {
        protected $moduleLoader;
 
        /**
-        * module menu generating object
-        *
-        * @var \TYPO3\CMS\Backend\View\ModuleMenuView
-        */
-       protected $moduleMenu;
-
-       /**
         * Pagerenderer
         *
         * @var \TYPO3\CMS\Core\Page\PageRenderer
@@ -83,10 +76,10 @@ class BackendController {
                // Initializes the backend modules structure for use later.
                $this->moduleLoader = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleLoader');
                $this->moduleLoader->load($GLOBALS['TBE_MODULES']);
-               $this->moduleMenu = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\View\\ModuleMenuView');
                $this->pageRenderer = $GLOBALS['TBE_TEMPLATE']->getPageRenderer();
                $this->pageRenderer->loadScriptaculous('builder,effects,controls,dragdrop');
                $this->pageRenderer->loadExtJS();
+               $this->pageRenderer->loadJquery(NULL, NULL, \TYPO3\CMS\Core\Page\PageRenderer::JQUERY_NAMESPACE_DEFAULT_NOCONFLICT);
                $this->pageRenderer->enableExtJSQuickTips();
                $this->pageRenderer->addJsInlineCode('consoleOverrideWithDebugPanel', '//already done', FALSE);
                $this->pageRenderer->addExtDirectCode();
@@ -169,7 +162,8 @@ class BackendController {
                <div id="typo3-top-container" class="x-hide-display">
                        <div id="typo3-logo">' . $logo->render() . '</div>
                        <div id="typo3-top" class="typo3-top-toolbar">' . $this->renderToolbar() . '</div>
-               </div>';
+               </div>
+               ' . $this->generateModuleMenu();
                /******************************************************
                 * Now put the complete backend document together
                 ******************************************************/
@@ -189,6 +183,7 @@ class BackendController {
                $this->generateJavascript();
                $this->pageRenderer->addJsInlineCode('BackendInlineJavascript', $this->js, FALSE);
                $this->loadResourcesForRegisteredNavigationComponents();
+
                // Add state provider
                $GLOBALS['TBE_TEMPLATE']->setExtDirectStateProvider();
                $states = $GLOBALS['BE_USER']->uc['BackendComponents']['States'];
@@ -199,9 +194,11 @@ class BackendController {
                                autoRead: false
                        }));
                ';
+
                if ($states) {
                        $extOnReadyCode .= 'Ext.state.Manager.getProvider().initState(' . json_encode($states) . ');';
                }
+
                $extOnReadyCode .= '
                        TYPO3.Backend = new TYPO3.Viewport(TYPO3.Viewport.configuration);
                        if (typeof console === "undefined") {
@@ -281,7 +278,7 @@ class BackendController {
                }
                $toolbar = '<ul id="typo3-toolbar">';
                $toolbar .= '<li>' . $this->getLoggedInUserLabel() . '</li>';
-               $toolbar .= '<li class="separator"><div id="logout-button" class="toolbar-item no-separator">' . $this->moduleMenu->renderLogoutButton() . '</div></li>';
+               $toolbar .= '<li class="separator"><div id="logout-button" class="toolbar-item no-separator">' . $this->renderLogoutButton() . '</div></li>';
                $i = 0;
                $numberOfToolbarItems = count($this->toolbarItems);
                foreach ($this->toolbarItems as $key => $toolbarItem) {
@@ -719,4 +716,49 @@ class BackendController {
                }
        }
 
+       /**
+        * renders the logout button form
+        *
+        * @return string Html code snippet displaying the logout button
+        */
+       protected function renderLogoutButton() {
+               // show logout or "exit" (from switch user mode) label
+               $buttonLabel = 'LLL:EXT:lang/locallang_core.xlf:' . ($GLOBALS['BE_USER']->user['ses_backuserid'] ? 'buttons.exit' : 'buttons.logout');
+               $buttonForm = '
+               <form action="logout.php" target="_top">
+                       <input type="submit" id="logout-submit-button" value="' . $GLOBALS['LANG']->sL($buttonLabel, TRUE) . '" />
+               </form>';
+               return $buttonForm;
+       }
+
+       /**
+        * loads all modules from the repository
+        * and renders it with a template
+        *
+        * @return string
+        */
+       protected function generateModuleMenu() {
+               /** @var $moduleRepository \TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository */
+               $moduleRepository = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Domain\\Repository\\Module\\BackendModuleRepository');
+               $moduleStorage = $moduleRepository->loadAllowedModules();
+
+               /** @var $view \TYPO3\CMS\Fluid\View\StandaloneView */
+               $view = GeneralUtility::makeInstance('TYPO3\\CMS\\Fluid\\View\\StandaloneView');
+               $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ModuleMenu/Main.html'));
+               $view->assign('modules', $moduleStorage);
+               return $view->render();
+       }
+
+       /**
+        * Returns the Module menu for the AJAX API
+        *
+        * @param array $params
+        * @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxRequestHandler
+        * @return void
+        */
+       public function getModuleMenuForReload($params, $ajaxRequestHandler) {
+               $content = $this->generateModuleMenu();
+               $ajaxRequestHandler->addContent('menu', $content);
+               $ajaxRequestHandler->setContentFormat('json');
+       }
 }
index 522ae9d..e61c99a 100644 (file)
@@ -22,42 +22,52 @@ namespace TYPO3\CMS\Backend\Domain\Model\Module;
 class BackendModule {
 
        /**
-        * @var string $title
+        * @var string
         */
        protected $title = '';
 
        /**
-        * @var string $name
+        * @var string
         */
        protected $name = '';
 
        /**
-        * @var array $icon
+        * @var array
         */
        protected $icon = array();
 
        /**
-        * @var string $link
+        * @var string
         */
        protected $link = '';
 
        /**
-        * @var string $onClick
+        * @var string
         */
        protected $onClick = '';
 
        /**
-        * @var string $description
+        * @var string
         */
        protected $description = '';
 
        /**
-        * @var string $navigationComponentId
+        * @var string
         */
        protected $navigationComponentId = '';
 
        /**
-        * @var \SplObjectStorage $children
+        * @var string
+        */
+       protected $navigationFrameScript = '';
+
+       /**
+        * @var string
+        */
+       protected $navigationFrameScriptParameters = '';
+
+       /**
+        * @var \SplObjectStorage
         */
        protected $children;
 
@@ -212,6 +222,34 @@ class BackendModule {
        }
 
        /**
+        * @param string $navigationFrameScript
+        */
+       public function setNavigationFrameScript($navigationFrameScript) {
+               $this->navigationFrameScript = $navigationFrameScript;
+       }
+
+       /**
+        * @return string
+        */
+       public function getNavigationFrameScript() {
+               return $this->navigationFrameScript;
+       }
+
+       /**
+        * @param string $navigationFrameScriptParameters
+        */
+       public function setNavigationFrameScriptParameters($navigationFrameScriptParameters) {
+               $this->navigationFrameScriptParameters = $navigationFrameScriptParameters;
+       }
+
+       /**
+        * @return string
+        */
+       public function getNavigationFrameScriptParameters() {
+               return $this->navigationFrameScriptParameters;
+       }
+
+       /**
         * Set onClick
         *
         * @param string $onClick
index 6dea3fa..c4a2fa9 100644 (file)
@@ -14,23 +14,40 @@ namespace TYPO3\CMS\Backend\Domain\Repository\Module;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+
 /**
  * Repository for backend module menu
+ * compiles all data from $GLOBALS[TBE_MODULES]
  *
  * @author Susanne Moog <typo3@susannemoog.de>
  */
 class BackendModuleRepository implements \TYPO3\CMS\Core\SingletonInterface {
 
        /**
-        * @var \TYPO3\CMS\Backend\Module\ModuleStorage $moduleMenu
+        * @var \TYPO3\CMS\Backend\Module\ModuleStorage
         */
-       protected $moduleMenu;
+       protected $moduleStorage;
 
        /**
         * Constructs the module menu and gets the Singleton instance of the menu
         */
        public function __construct() {
-               $this->moduleMenu = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleStorage');
+               $this->moduleStorage = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleStorage');
+       }
+
+       /**
+        * loads all module information in the module storage
+        *
+        * @return \SplObjectStorage
+        */
+       public function loadAllowedModules() {
+               $rawData = $this->getRawModuleMenuData();
+
+               $this->convertRawModuleDataToModuleMenuObject($rawData);
+               $this->createMenuEntriesForTbeModulesExt();
+               return $this->moduleStorage->getEntries();
        }
 
        /**
@@ -40,7 +57,7 @@ class BackendModuleRepository implements \TYPO3\CMS\Core\SingletonInterface {
         * @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule|boolean
         */
        public function findByModuleName($name) {
-               $entries = $this->moduleMenu->getEntries();
+               $entries = $this->moduleStorage->getEntries();
                $entry = $this->findByModuleNameInGivenEntries($name, $entries);
                return $entry;
        }
@@ -68,4 +85,287 @@ class BackendModuleRepository implements \TYPO3\CMS\Core\SingletonInterface {
                return FALSE;
        }
 
+       /**
+        * Creates the module menu object structure from the raw data array
+        *
+        * @param array $rawModuleData
+        * @return void
+        */
+       protected function convertRawModuleDataToModuleMenuObject(array $rawModuleData) {
+               foreach ($rawModuleData as $module) {
+                       $entry = $this->createEntryFromRawData($module);
+                       if (isset($module['subitems']) && !empty($module['subitems'])) {
+                               foreach ($module['subitems'] as $subitem) {
+                                       $subEntry = $this->createEntryFromRawData($subitem);
+                                       $entry->addChild($subEntry);
+                               }
+                       }
+                       $this->moduleStorage->attachEntry($entry);
+               }
+       }
+
+       /**
+        * Creates a menu entry object from an array
+        *
+        * @param array $module
+        * @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule
+        */
+       protected function createEntryFromRawData(array $module) {
+               /** @var $entry \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule */
+               $entry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Domain\\Model\\Module\\BackendModule');
+               if (!empty($module['name']) && is_string($module['name'])) {
+                       $entry->setName($module['name']);
+               }
+               if (!empty($module['title']) && is_string($module['title'])) {
+                       $entry->setTitle($this->getLanguageService()->sL($module['title']));
+               }
+               if (!empty($module['onclick']) && is_string($module['onclick'])) {
+                       $entry->setOnClick($module['onclick']);
+               }
+               if (!empty($module['link']) && is_string($module['link'])) {
+                       $entry->setLink($module['link']);
+               } elseif (empty($module['link']) && !empty($module['path']) && is_string($module['path'])) {
+                       $entry->setLink($module['path']);
+               }
+               if (!empty($module['description']) && is_string($module['description'])) {
+                       $entry->setDescription($module['description']);
+               }
+               if (!empty($module['icon']) && is_array($module['icon'])) {
+                       $entry->setIcon($module['icon']);
+               }
+               if (!empty($module['navigationComponentId']) && is_string($module['navigationComponentId'])) {
+                       $entry->setNavigationComponentId($module['navigationComponentId']);
+               }
+               if (!empty($module['navigationFrameScript']) && is_string($module['navigationFrameScript'])) {
+                       $entry->setNavigationFrameScript($module['navigationFrameScript']);
+               } elseif (!empty($module['parentNavigationFrameScript']) && is_string($module['parentNavigationFrameScript'])) {
+                       $entry->setNavigationFrameScript($module['parentNavigationFrameScript']);
+               }
+               if (!empty($module['navigationFrameScriptParam']) && is_string($module['navigationFrameScriptParam'])) {
+                       $entry->setNavigationFrameScriptParameters($module['navigationFrameScriptParam']);
+               }
+               return $entry;
+       }
+
+       /**
+        * Creates the "third level" menu entries (submodules for the info module for
+        * example) from the TBE_MODULES_EXT array
+        *
+        * @return void
+        */
+       protected function createMenuEntriesForTbeModulesExt() {
+               foreach ($GLOBALS['TBE_MODULES_EXT'] as $mainModule => $tbeModuleExt) {
+                       list($main) = explode('_', $mainModule);
+                       $mainEntry = $this->findByModuleName($main);
+                       if ($mainEntry === FALSE) {
+                               continue;
+                       }
+
+                       $subEntries = $mainEntry->getChildren();
+                       if (empty($subEntries)) {
+                               continue;
+                       }
+                       $matchingSubEntry = $this->findByModuleName($mainModule);
+                       if ($matchingSubEntry !== FALSE) {
+                               if (isset($tbeModuleExt['MOD_MENU']) && isset($tbeModuleExt['MOD_MENU']['function'])) {
+                                       foreach ($tbeModuleExt['MOD_MENU']['function'] as $subModule) {
+                                               $entry = $this->createEntryFromRawData($subModule);
+                                               $matchingSubEntry->addChild($entry);
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Return language service instance
+        *
+        * @return \TYPO3\CMS\Lang\LanguageService
+        */
+       protected function getLanguageService() {
+               return $GLOBALS['LANG'];
+       }
+
+       /**
+        * loads the module menu from the moduleloader based on $GLOBALS['TBE_MODULES']
+        * and compiles an array with all the data needed for menu etc.
+        *
+        * @return array
+        */
+       public function getRawModuleMenuData() {
+               // Loads the backend modules available for the logged in user.
+               $moduleLoader = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleLoader');
+               $moduleLoader->observeWorkspaces = TRUE;
+               $moduleLoader->load($GLOBALS['TBE_MODULES']);
+               $loadedModules = $moduleLoader->modules;
+
+               $modules = array();
+
+               // Unset modules that are meant to be hidden from the menu.
+               $loadedModules = $this->removeHiddenModules($loadedModules);
+
+               foreach ($loadedModules as $moduleName => $moduleData) {
+                       $moduleLink = '';
+                       if (!is_array($moduleData['sub'])) {
+                               $moduleLink = $moduleData['script'];
+                       }
+                       $moduleLink = GeneralUtility::resolveBackPath($moduleLink);
+                       $moduleKey = 'modmenu_' . $moduleName;
+                       $moduleIcon = $this->getModuleIcon($moduleKey);
+                       $modules[$moduleKey] = array(
+                               'name' => $moduleName,
+                               'title' => $GLOBALS['LANG']->moduleLabels['tabs'][$moduleName . '_tab'],
+                               'onclick' => 'top.goToModule(\'' . $moduleName . '\');',
+                               'icon' => $moduleIcon,
+                               'link' => $moduleLink,
+                               'description' => $GLOBALS['LANG']->moduleLabels['labels'][$moduleKey . 'label']
+                       );
+                       if (!is_array($moduleData['sub']) && $moduleData['script'] !== 'dummy.php') {
+                               // Work around for modules with own main entry, but being self the only submodule
+                               $modules[$moduleKey]['subitems'][$moduleKey] = array(
+                                       'name' => $moduleName,
+                                       'title' => $GLOBALS['LANG']->moduleLabels['tabs'][$moduleName . '_tab'],
+                                       'onclick' => 'top.goToModule(\'' . $moduleName . '\');',
+                                       'icon' => $this->getModuleIcon($moduleName . '_tab'),
+                                       'link' => $moduleLink,
+                                       'originalLink' => $moduleLink,
+                                       'description' => $GLOBALS['LANG']->moduleLabels['labels'][$moduleKey . 'label'],
+                                       'navigationFrameScript' => NULL,
+                                       'navigationFrameScriptParam' => NULL,
+                                       'navigationComponentId' => NULL
+                               );
+                       } elseif (is_array($moduleData['sub'])) {
+                               foreach ($moduleData['sub'] as $submoduleName => $submoduleData) {
+                                       if (isset($submoduleData['script'])) {
+                                               $submoduleLink = GeneralUtility::resolveBackPath($submoduleData['script']);
+                                       } else {
+                                               $submoduleLink = BackendUtility::getModuleUrl($submoduleData['name']);
+                                       }
+                                       $submoduleKey = $moduleName . '_' . $submoduleName . '_tab';
+                                       $submoduleIcon = $this->getModuleIcon($submoduleKey);
+                                       $submoduleDescription = $GLOBALS['LANG']->moduleLabels['labels'][$submoduleKey . 'label'];
+                                       $originalLink = $submoduleLink;
+                                       if (isset($submoduleData['navigationFrameModule'])) {
+                                               $navigationFrameScript = BackendUtility::getModuleUrl(
+                                                       $submoduleData['navigationFrameModule'],
+                                                       isset($submoduleData['navigationFrameModuleParameters'])
+                                                               ? $submoduleData['navigationFrameModuleParameters']
+                                                               : array()
+                                               );
+                                       } else {
+                                               $navigationFrameScript = $submoduleData['navFrameScript'];
+                                       }
+                                       $modules[$moduleKey]['subitems'][$submoduleKey] = array(
+                                               'name' => $moduleName . '_' . $submoduleName,
+                                               'title' => $GLOBALS['LANG']->moduleLabels['tabs'][$submoduleKey],
+                                               'onclick' => 'top.goToModule(\'' . $moduleName . '_' . $submoduleName . '\');',
+                                               'icon' => $submoduleIcon,
+                                               'link' => $submoduleLink,
+                                               'originalLink' => $originalLink,
+                                               'description' => $submoduleDescription,
+                                               'navigationFrameScript' => $navigationFrameScript,
+                                               'navigationFrameScriptParam' => $submoduleData['navFrameScriptParam'],
+                                               'navigationComponentId' => $submoduleData['navigationComponentId']
+                                       );
+                                       // if the main module has a navframe script, inherit to the submodule,
+                                       // but only if it is not disabled explicitly (option is set to FALSE)
+                                       if ($moduleData['navFrameScript'] && $submoduleData['inheritNavigationComponentFromMainModule'] !== FALSE) {
+                                               $modules[$moduleKey]['subitems'][$submoduleKey]['parentNavigationFrameScript'] = $moduleData['navFrameScript'];
+                                       }
+                               }
+                       }
+               }
+               return $modules;
+       }
+
+       /**
+        * Reads User configuration from options.hideModules and removes
+        * modules accordingly.
+        *
+        * @param array $loadedModules
+        * @return array
+        */
+       protected function removeHiddenModules($loadedModules) {
+               $hiddenModules = $GLOBALS['BE_USER']->getTSConfig('options.hideModules');
+
+               // Hide modules if set in userTS.
+               if (!empty($hiddenModules['value'])) {
+                       $hiddenMainModules = explode(',', $hiddenModules['value']);
+                       foreach ($hiddenMainModules as $hiddenMainModule) {
+                               unset($loadedModules[trim($hiddenMainModule)]);
+                       }
+               }
+
+               // Hide sub-modules if set in userTS.
+               if (!empty($hiddenModules['properties']) && is_array($hiddenModules['properties'])) {
+                       foreach ($hiddenModules['properties'] as $mainModuleName => $subModules) {
+                               $hiddenSubModules = explode(',', $subModules);
+                               foreach ($hiddenSubModules as $hiddenSubModule) {
+                                       unset($loadedModules[$mainModuleName]['sub'][trim($hiddenSubModule)]);
+                               }
+                       }
+               }
+
+               return $loadedModules;
+       }
+
+       /**
+        * gets the module icon and its size
+        *
+        * @param string $moduleKey Module key
+        * @return array Icon data array with 'filename', 'size', and 'html'
+        */
+       protected function getModuleIcon($moduleKey) {
+               $icon = array(
+                       'filename' => '',
+                       'size' => '',
+                       'title' => '',
+                       'html' => ''
+               );
+
+               if (!empty($GLOBALS['LANG']->moduleLabels['tabs_images'][$moduleKey])) {
+                       $imageReference = $GLOBALS['LANG']->moduleLabels['tabs_images'][$moduleKey];
+                       $iconFileRelative = $this->getModuleIconRelative($imageReference);
+                       if (!empty($iconFileRelative)) {
+                               $iconTitle = $GLOBALS['LANG']->moduleLabels['tabs'][$moduleKey];
+                               $iconFileAbsolute = $this->getModuleIconAbsolute($imageReference);
+                               $iconSizes = @getimagesize($iconFileAbsolute);
+                               $icon['filename'] = $iconFileRelative;
+                               $icon['size'] = $iconSizes[3];
+                               $icon['title'] = htmlspecialchars($iconTitle);
+                               $icon['html'] = '<img src="' . $iconFileRelative . '" ' . $iconSizes[3] . ' title="' . htmlspecialchars($iconTitle) . '" alt="' . htmlspecialchars($iconTitle) . '" />';
+                       }
+               }
+               return $icon;
+       }
+
+       /**
+        * Returns the filename readable for the script from PATH_typo3.
+        * That means absolute names are just returned while relative names are
+        * prepended with the path pointing back to typo3/ dir
+        *
+        * @param string $iconFilename Icon filename
+        * @return string Icon filename with absolute path
+        * @see getModuleIconRelative()
+        */
+       protected function getModuleIconAbsolute($iconFilename) {
+               if (!GeneralUtility::isAbsPath($iconFilename)) {
+                       $iconFilename = $GLOBALS['BACK_PATH'] . $iconFilename;
+               }
+               return $iconFilename;
+       }
+
+       /**
+        * Returns relative path to the icon filename for use in img-tags
+        *
+        * @param string $iconFilename Icon filename
+        * @return string Icon filename with relative path
+        * @see getModuleIconAbsolute()
+        */
+       protected function getModuleIconRelative($iconFilename) {
+               if (GeneralUtility::isAbsPath($iconFilename)) {
+                       $iconFilename = '../' . \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix($iconFilename);
+               }
+               return $iconFilename;
+       }
 }
index af8c5d8..dc20f58 100644 (file)
@@ -33,8 +33,10 @@ class ModuleController {
 
        /**
         * Constructor
+        * @deprecated since TYPO3 CMS 6.3, not in use, as everything can be done via the ModuleMenuRepository directly
         */
        public function __construct() {
+               \TYPO3\CMS\Core\Utility\GeneralUtility::logDeprecatedFunction();
                $this->moduleMenu = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleStorage');
                $this->moduleMenuRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Domain\\Repository\\Module\\BackendModuleRepository');
        }
index d1a0c84..2d80477 100644 (file)
@@ -39,8 +39,10 @@ class ModuleMenuView {
 
        /**
         * Constructor, initializes several variables
+        * @deprecated since TYPO3 CMS 6.3, not in use, as everything can be done via the ModuleMenuRepository directly
         */
        public function __construct() {
+               GeneralUtility::logDeprecatedFunction();
                if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX) {
                        $GLOBALS['LANG']->includeLLFile('EXT:lang/locallang_misc.xlf');
                }
diff --git a/typo3/sysext/backend/Resources/Private/Templates/ModuleMenu/Main.html b/typo3/sysext/backend/Resources/Private/Templates/ModuleMenu/Main.html
new file mode 100644 (file)
index 0000000..384686f
--- /dev/null
@@ -0,0 +1,19 @@
+<ul id="typo3-menu">
+<f:for each="{modules}" as="mainModule">
+       <li class="menuSection t3-menuitem-main" id="{mainModule.name}" data-navigationcomponentid="{mainModule.navigationComponentId}" data-navigationframescript="{mainModule.navigationFrameScript}" data-navigationframescriptparameters="{mainModule.navigationFrameScriptParameters}">
+               <div class="modgroup expanded">{mainModule.title}</div>
+               <ul class="t3-menuitem-submodules">
+                       <f:for each="{mainModule.children}" as="subModule">
+                               <li id="{subModule.name}" class="t3-menuitem-submodule submodule mod-{subModule.name}"  data-navigationcomponentid="{subModule.navigationComponentId}" data-navigationframescript="{subModule.navigationFrameScript}" data-navigationframescriptparameters="{subModule.navigationFrameScriptParameters}">
+                                       <a title="{subModule.description}" href="{subModule.link}" class="modlink">
+                                               <span class="submodule-icon">
+                                                       <f:format.raw>{subModule.icon.html}</f:format.raw>
+                                               </span>
+                                               <span>{subModule.title}</span>
+                                       </a>
+                               </li>
+                       </f:for>
+               </ul>
+       </li>
+</f:for>
+</ul>
\ No newline at end of file
index 40ac363..810cc43 100644 (file)
@@ -23,231 +23,98 @@ Ext.ns('TYPO3', 'ModuleMenu');
 
 TYPO3.ModuleMenu = {};
 
-TYPO3.ModuleMenu.Store = new Ext.data.JsonStore({
-       storeId: 'ModuleMenuStore',
-       root: 'root',
-       fields: [
-               {name: 'index', type: 'int', mapping: 'sub.index'},
-               {name: 'key', type: 'string'},
-               {name: 'label', type: 'string'},
-               {name: 'menuState', type: 'int'},
-               {name: 'subitems', type: 'int'},
-               'sub'
-       ],
-       url: TYPO3.settings.ajaxUrls['ModuleMenu::getData'],
-       baseParams: {
-               'action': 'getModules'
-       },
-       listeners: {
-               beforeload: function(store) {
-                       this.loaded = false;
-               },
-               load: function(store) {
-                       this.loaded = true;
-               }
-       },
-       // Custom indicator for loaded store:
-       loaded: false,
-       isLoaded: function() {
-               return this.loaded;
-       }
-
-});
-
-TYPO3.ModuleMenu.Template = new Ext.XTemplate(
-               '<ul id="typo3-menu">',
-               '<tpl for=".">',
-               '       <li class="menuSection" id="{key}">',
-               '               <div class="modgroup {[this.getStateClass(values)]}">{label}</div>',
-               '       <ul {[this.getStateStyle(values)]}>',
-               '       <tpl for="sub">',
-               '       <li id="{name}" class="submodule mod-{name}">',
-               '               <a title="{description}" href="#" class="modlink">',
-               '                       <span class="submodule-icon">',
-               '                               <img width="16" height="16" alt="{label}" title="{label}" src="{icon}" />',
-               '                       </span>',
-               '                       <span>{label}</span>',
-               '               </a>',
-               '       </li>',
-               '       </tpl>',
-               '       </ul>',
-               '       </li>',
-               '</tpl>',
-               '</ul>',
-               {
-                       getStateClass: function(value) {
-                               return value.menuState ? 'collapsed' : 'expanded';
-                       },
-                       getStateStyle: function(value) {
-                               return value.menuState ? 'style="display:none"' : '';
-                       }
-               }
-);
-
 TYPO3.ModuleMenu.App = {
        loadedModule: null,
        loadedNavigationComponentId: '',
        availableNavigationComponents: {},
 
-       init: function() {
-               TYPO3.ModuleMenu.Store.load({
-                       scope: this,
-                       callback: function(records, options) {
-                               this.renderMenu(records);
-                               if (top.startInModule) {
-                                       this.showModule(top.startInModule[0], top.startInModule[1]);
-                               } else {
-                                       this.loadFirstAvailableModule();
-                               }
+       initialize: function() {
+               var me = this;
+
+               // load the start module
+               if (top.startInModule && top.startInModule[0] && top.startInModule[0].length > 0) {
+                       me.showModule(top.startInModule[0]);
+               } else {
+                       // fetch first module
+                       me.showModule(jQuery('.t3-menuitem-submodule:first').attr('id'));
+               }
+
+               // check if there are collapsed items in the local storage
+               var collapsedMainMenuItems = this.getCollapsedMainMenuItems();
+               jQuery.each(collapsedMainMenuItems, function(key, itm) {
+                       var $headerElement = jQuery('#' + key).find('.modgroup:first');
+                       if ($headerElement.length > 0) {
+                               $headerElement.addClass('collapsed').removeClass('expanded').next('.t3-menuitem-submodules').slideUp('fast');
                        }
                });
+
+               me.initializeEvents();
        },
 
-       renderMenu: function(records) {
-               TYPO3.Backend.ModuleMenuContainer.removeAll();
-               TYPO3.Backend.ModuleMenuContainer.add({
-                       xtype: 'dataview',
-                       animCollapse: true,
-                       store: TYPO3.ModuleMenu.Store,
-                       tpl: TYPO3.ModuleMenu.Template,
-                       singleSelect: true,
-                       itemSelector: 'li.submodule',
-                       overClass: 'x-view-over',
-                       selectedClass: 'highlighted',
-                       autoHeight: true,
-                       itemId: 'modDataView',
-                       tbar: [{text: 'test'}],
-                       listeners: {
-                               click: function(view, index, node, event) {
-                                       var el = Ext.fly(node);
-                                       if (el.hasClass('submodule')) {
-                                               TYPO3.ModuleMenu.App.showModule(el.getAttribute('id'));
-                                       }
-                               },
-                               containerclick: function(view, event) {
-                                       var item = event.getTarget('li.menuSection', view.getEl());
-                                       if (item) {
-                                               var el = Ext.fly(item);
-                                               var id = el.getAttribute('id');
-                                               var section = el.first('div'), state;
-                                               if (section.hasClass('expanded')) {
-                                                       state = true;
-                                                       section.removeClass('expanded').addClass('collapsed');
-                                                       el.first('ul').slideOut('t', {
-                                                               easing: 'easeOut',
-                                                               duration: .2,
-                                                               remove: false,
-                                                               useDisplay: true
-                                                       });
-
-                                               } else {
-                                                       state = false;
-                                                       section.removeClass('collapsed').addClass('expanded');
-                                                       el.first('ul').slideIn('t', {
-                                                               easing: 'easeIn',
-                                                               duration: .2,
-                                                               remove: false,
-                                                               useDisplay: true
-                                                       });
-                                               }
-                                               // save menu state
-                                               Ext.Ajax.request({
-                                                       url: TYPO3.settings.ajaxUrls['ModuleMenu::saveMenuState'],
-                                                       params: {
-                                                               'menuid': 'modmenu_' + id,
-                                                               'state': state
-                                                       }
-                                               });
-                                       }
-                                       return false;
-                               },
-                               scope: this
+       initializeEvents: function() {
+               var me = this;
+               jQuery(document).on('click', '.t3-menuitem-main .modgroup', function() {
+                       var $headerElement = jQuery(this);
+                       if ($headerElement.hasClass('expanded')) {
+                               me.addCollapsedMainMenuItem($headerElement.parent().attr('id'));
+                               $headerElement.addClass('collapsed').removeClass('expanded').next('.t3-menuitem-submodules').slideUp();
+                       } else {
+                               me.removeCollapseMainMenuItem($headerElement.parent().attr('id'));
+                               $headerElement.addClass('expanded').removeClass('collapsed').next('.t3-menuitem-submodules').slideDown();
                        }
                });
-               TYPO3.Backend.ModuleMenuContainer.doLayout();
-       },
 
-       getRecordFromIndex: function(index) {
-               var i, record;
-               for (i = 0; i < TYPO3.ModuleMenu.Store.getCount(); i++) {
-                       record = TYPO3.ModuleMenu.Store.getAt(i);
-                       if (index < record.data.subitems) {
-                               return record.data.sub[index];
-                       }
-                       index -= record.data.subitems;
-               }
+               // register clicking on sub modules
+               jQuery(document).on('click', '.t3-menuitem-submodule', function(evt) {
+                       evt.preventDefault();
+                       me.showModule(jQuery(this).attr('id'));
+               });
        },
 
+       /* fetch the data for a submodule */
        getRecordFromName: function(name) {
-               var i, j, record;
-               for (i = 0; i < TYPO3.ModuleMenu.Store.getCount(); i++) {
-                       record = TYPO3.ModuleMenu.Store.getAt(i);
-                       for (j = 0; j < record.data.subitems; j++) {
-                               if (record.data.sub[j].name === name) {
-                                       return record.data.sub[j];
-                               }
-                       }
-               }
+               var $subModuleElement = jQuery('#' + name);
+               return {
+                       name: name,
+                       navigationComponentId: $subModuleElement.data('navigationcomponentid'),
+                       navigationFrameScript: $subModuleElement.data('navigationframescript'),
+                       navigationFrameScriptParam: $subModuleElement.data('navigationframescriptparameters'),
+                       link: $subModuleElement.find('a').attr('href')
+               };
        },
 
        showModule: function(mod, params) {
                params = params || '';
-               this.selectedModule = mod;
-
                params = this.includeId(mod, params);
                var record = this.getRecordFromName(mod);
-
-               if (record) {
-                       this.loadModuleComponents(record, params);
-               } else {
-                               //defined startup module is not present, use the first available instead
-                       this.loadFirstAvailableModule(params);
-               }
-       },
-
-       loadFirstAvailableModule: function(params) {
-               params = params || '';
-               if (TYPO3.ModuleMenu.Store.isLoaded() === false) {
-                       new Ext.util.DelayedTask(
-                               this.loadFirstAvailableModule,
-                               this,
-                               [params]
-                       ).delay(250);
-               } else if (TYPO3.ModuleMenu.Store.getCount() === 0) {
-                               // Store is empty, something went wrong
-                       TYPO3.Flashmessage.display(TYPO3.Severity.error, 'Module loader', 'No module found. If this is a temporary error, please reload the Backend!', 50000);
-               } else {
-                       mod = TYPO3.ModuleMenu.Store.getAt(0).data.sub[0];
-                       this.loadModuleComponents(mod, params);
-               }
+               this.loadModuleComponents(record, params);
        },
 
        loadModuleComponents: function(record, params) {
                var mod = record.name;
                if (record.navigationComponentId) {
-                               this.loadNavigationComponent(record.navigationComponentId);
-                               TYPO3.Backend.NavigationDummy.hide();
-                               TYPO3.Backend.NavigationIframe.getEl().parent().setStyle('overflow', 'auto');
-                       } else if (record.navframe || record.navigationFrameScript) {
-                               TYPO3.Backend.NavigationDummy.hide();
-                               TYPO3.Backend.NavigationContainer.show();
-                               this.loadNavigationComponent('typo3-navigationIframe');
-                               this.openInNavFrame(record.navigationFrameScript || record.navframe, record.navigationFrameScriptParam);
-                               TYPO3.Backend.NavigationIframe.getEl().parent().setStyle('overflow', 'hidden');
-                       } else {
-                               TYPO3.Backend.NavigationContainer.hide();
-                               TYPO3.Backend.NavigationDummy.show();
-                       }
+                       this.loadNavigationComponent(record.navigationComponentId);
+                       TYPO3.Backend.NavigationDummy.hide();
+                       TYPO3.Backend.NavigationIframe.getEl().parent().setStyle('overflow', 'auto');
+               } else if (record.navigationFrameScript) {
+                       TYPO3.Backend.NavigationDummy.hide();
+                       TYPO3.Backend.NavigationContainer.show();
+                       this.loadNavigationComponent('typo3-navigationIframe');
+                       this.openInNavFrame(record.navigationFrameScript, record.navigationFrameScriptParam);
+                       TYPO3.Backend.NavigationIframe.getEl().parent().setStyle('overflow', 'hidden');
+               } else {
+                       TYPO3.Backend.NavigationContainer.hide();
+                       TYPO3.Backend.NavigationDummy.show();
+               }
 
-                       this.highlightModuleMenuItem(mod);
-                       this.loadedModule = mod;
-                       this.openInContentFrame(record.originalLink, params);
+               this.highlightModuleMenuItem(mod);
+               this.openInContentFrame(record.link, params);
 
-                               // compatibility
-                       top.currentSubScript = record.originalLink;
-                       top.currentModuleLoaded = mod;
+               // compatibility
+               top.currentSubScript = record.link;
+               top.currentModuleLoaded = mod;
 
-                       TYPO3.Backend.doLayout();
+               TYPO3.Backend.doLayout();
        },
 
        includeId: function(mod, params) {
@@ -288,7 +155,7 @@ TYPO3.ModuleMenu.App = {
 
                        // backwards compatibility
                top.nav = component;
-               
+
                TYPO3.Backend.NavigationContainer.show();
                this.loadedNavigationComponentId = navigationComponentId;
        },
@@ -306,40 +173,66 @@ TYPO3.ModuleMenu.App = {
        },
 
        openInContentFrame: function(url, params) {
-               var urlToLoad;
                if (top.nextLoadModuleUrl) {
                        TYPO3.Backend.ContentContainer.setUrl(top.nextLoadModuleUrl);
                        top.nextLoadModuleUrl = '';
                } else {
-                       urlToLoad = url + (params ? (url.indexOf('?') !== -1 ? '&' : '?') + params : '')
+                       var urlToLoad = url + (params ? (url.indexOf('?') !== -1 ? '&' : '?') + params : '')
                        TYPO3.Backend.ContentContainer.setUrl(urlToLoad);
-                       return;
                }
        },
 
        highlightModuleMenuItem: function(module, mainModule) {
-               TYPO3.Backend.ModuleMenuContainer.getComponent('modDataView').select(module, false, false);
+               jQuery('#typo3-menu').find('.highlighted').removeClass('highlighted');
+               jQuery('#' + module).addClass('highlighted');
        },
 
        relativeUrl: function(url) {
                return url.replace(TYPO3.configuration.siteUrl + 'typo3/', '');
        },
 
+               // refresh the HTML by fetching the menu again
        refreshMenu: function() {
-               TYPO3.ModuleMenu.Store.load({
-                       scope: this,
-                       callback: function(records, options) {
-                               this.renderMenu(records);
-                               if (this.loadedModule) {
-                                       this.highlightModuleMenuItem(this.loadedModule);
-                               }
-                       }
+               jQuery.ajax(TYPO3.settings.ajaxUrls['ModuleMenu::reload']).done(function(result) {
+                       jQuery('#typo3-menu').replaceWith(result.menu);
                });
        },
 
        reloadFrames: function() {
                TYPO3.Backend.NavigationIframe.refresh();
                TYPO3.Backend.ContentContainer.refresh();
+       },
+
+       /**
+        * fetches all module menu elements in the local storage that should be collapsed
+        * @returns {*}
+        */
+       getCollapsedMainMenuItems: function() {
+               if (typeof localStorage.getItem('t3-modulemenu') !== "undefined" && typeof localStorage.getItem('t3-modulemenu') !== "null" && localStorage.getItem('t3-modulemenu') != 'undefined' && localStorage.getItem('t3-modulemenu')) {
+                       return JSON.parse(localStorage.getItem('t3-modulemenu'));
+               } else {
+                       return {};
+               }
+       },
+
+       /**
+        * adds a module menu item to the local storage
+        * @param item
+        */
+       addCollapsedMainMenuItem: function(item) {
+               var existingItems = this.getCollapsedMainMenuItems();
+               existingItems[item] = true;
+               localStorage.setItem('t3-modulemenu', JSON.stringify(existingItems));
+       },
+
+       /**
+        * removes a module menu item from the local storage
+        * @param item
+        */
+       removeCollapseMainMenuItem: function(item) {
+               var existingItems = this.getCollapsedMainMenuItems();
+               existingItems[item] = null;
+               localStorage.setItem('t3-modulemenu', JSON.stringify(jQuery.existingItems));
        }
 
 };
@@ -347,7 +240,7 @@ TYPO3.ModuleMenu.App = {
 
 
 Ext.onReady(function() {
-       TYPO3.ModuleMenu.App.init();
+       TYPO3.ModuleMenu.App.initialize();
 
                // keep backward compatibility
        top.list = TYPO3.Backend.ContentContainer;
index 575fd22..2bfff31 100644 (file)
@@ -342,7 +342,7 @@ class BackendUserAuthentication extends \TYPO3\CMS\Core\Authentication\AbstractU
                'thumbnailsByDefault' => 1,
                'emailMeAtLogin' => 0,
                'noMenuMode' => 0,
-               'startModule' => 'help_aboutmodules',
+               'startModule' => 'help_AboutmodulesAboutmodules',
                'hideSubmoduleIcons' => 0,
                'helpText' => 1,
                'titleLen' => 50,
index 7099cf1..9e19569 100644 (file)
@@ -655,12 +655,8 @@ return array(
                                'callbackMethod' => 'TYPO3\\CMS\\Backend\\Toolbar\\ShortcutToolbarItem->createAjaxShortcut',
                                'csrfTokenCheck' => TRUE
                        ),
-                       'ModuleMenu::saveMenuState' => array(
-                               'callbackMethod' => 'TYPO3\\CMS\\Backend\\View\\ModuleMenuView->saveMenuState',
-                               'csrfTokenCheck' => TRUE
-                       ),
-                       'ModuleMenu::getData' => array(
-                               'callbackMethod' => 'TYPO3\\CMS\\Backend\\View\\ModuleMenuView->getModuleData',
+                       'ModuleMenu::reload' => array(
+                               'callbackMethod' => 'TYPO3\\CMS\\Backend\\Controller\\BackendController->getModuleMenuForReload',
                                'csrfTokenCheck' => TRUE
                        ),
                        'BackendLogin::login' => array(
index 6fa7a17..164213c 100644 (file)
@@ -2,8 +2,11 @@
 Module menu
 - - - - - - - - - - - - - - - - - - - - - */
 
-#typo3-module-menu .x-panel-body {
-    overflow-x: hidden !important;
+#typo3-module-menu {
+       left: 0;
+       top: 0;
+       position: absolute;
+       overflow: hidden;
 }
 #typo3-menu {
     margin-top: 18px;