[BUGFIX] Prevent loading jsfunc.inline.js twice
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Private / TypeScript / ModuleMenu.ts
1 /*
2  * This file is part of the TYPO3 CMS project.
3  *
4  * It is free software; you can redistribute it and/or modify it under
5  * the terms of the GNU General Public License, either version 2
6  * of the License, or any later version.
7  *
8  * For the full copyright and license information, please read the
9  * LICENSE.txt file that was distributed with this source code.
10  *
11  * The TYPO3 project - inspiring people to share!
12  */
13
14 import {NavigationComponentInterface} from './Viewport/NavigationComponentInterface';
15 import {ScaffoldIdentifierEnum} from './Enum/Viewport/ScaffoldIdentifier';
16 import * as $ from 'jquery';
17 import PersistentStorage = require('./Storage/Persistent');
18 import Viewport = require('./Viewport');
19 import ClientRequest = require('./Event/ClientRequest');
20 import TriggerRequest = require('./Event/TriggerRequest');
21 import InteractionRequest = require('./Event/InteractionRequest');
22
23 interface Module {
24   name: string;
25   navigationComponentId: string;
26   navigationFrameScript: string;
27   navigationFrameScriptParam: string;
28   link: string;
29 }
30
31 /**
32  * Class to render the module menu and handle the BE navigation
33  */
34 class ModuleMenu {
35   private loadedModule: string = null;
36   private loadedNavigationComponentId: string = '';
37
38   /**
39    * Fetches all module menu elements in the local storage that should be collapsed
40    *
41    * @returns {Object}
42    */
43   private static getCollapsedMainMenuItems(): { [key: string]: boolean } {
44     if (PersistentStorage.isset('modulemenu')) {
45       return JSON.parse(PersistentStorage.get('modulemenu'));
46     } else {
47       return {};
48     }
49   }
50
51   /**
52    * Adds a module menu item to the local storage
53    *
54    * @param {string} item
55    */
56   private static addCollapsedMainMenuItem(item: string): void {
57     const existingItems = ModuleMenu.getCollapsedMainMenuItems();
58     existingItems[item] = true;
59     PersistentStorage.set('modulemenu', JSON.stringify(existingItems));
60   }
61
62   /**
63    * Removes a module menu item from the local storage
64    *
65    * @param {string} item
66    */
67   private static removeCollapseMainMenuItem(item: string): void {
68     const existingItems = this.getCollapsedMainMenuItems();
69     delete existingItems[item];
70     PersistentStorage.set('modulemenu', JSON.stringify(existingItems));
71   }
72
73   /**
74    * Prepends previously saved record id to the url params
75    *
76    * @param {Object} moduleData
77    * @param {string} params query string parameters for module url
78    * @return {string}
79    */
80   private static includeId(moduleData: Module, params: string): string {
81     if (!moduleData.navigationComponentId && !moduleData.navigationFrameScript) {
82       return params;
83     }
84     // get id
85     let section = '';
86     if (moduleData.navigationComponentId === 'TYPO3/CMS/Backend/PageTree/PageTreeElement') {
87       section = 'web';
88     } else {
89       section = moduleData.name.split('_')[0];
90     }
91     if (top.fsMod.recentIds[section]) {
92       params = 'id=' + top.fsMod.recentIds[section] + '&' + params;
93     }
94
95     return params;
96   }
97
98   /**
99    * @param {boolean} collapse
100    */
101   private static toggleMenu(collapse?: boolean): void {
102     Viewport.NavigationContainer.cleanup();
103
104     const $mainContainer = $(ScaffoldIdentifierEnum.scaffold);
105     const expandedClass = 'scaffold-modulemenu-expanded';
106
107     if (typeof collapse === 'undefined') {
108       collapse = $mainContainer.hasClass(expandedClass);
109     }
110     $mainContainer.toggleClass(expandedClass, !collapse);
111     if (!collapse) {
112       $('.scaffold')
113         .removeClass('scaffold-search-expanded')
114         .removeClass('scaffold-toolbar-expanded');
115     }
116
117     // Persist collapsed state in the UC of the current user
118     PersistentStorage.set(
119       'BackendComponents.States.typo3-module-menu',
120       {
121         collapsed: collapse
122       }
123     );
124
125     Viewport.doLayout();
126   }
127
128   /**
129    * Gets the module properties from module menu markup (data attributes)
130    *
131    * @param {string} name
132    * @returns {Module}
133    */
134   private static getRecordFromName(name: string): Module {
135     const $subModuleElement = $('#' + name);
136     return {
137       name: name,
138       navigationComponentId: $subModuleElement.data('navigationcomponentid'),
139       navigationFrameScript: $subModuleElement.data('navigationframescript'),
140       navigationFrameScriptParam: $subModuleElement.data('navigationframescriptparameters'),
141       link: $subModuleElement.find('a').data('link')
142     };
143   }
144
145   /**
146    * @param {string} module
147    */
148   private static highlightModuleMenuItem(module: string): void {
149     $('.modulemenu-item.active').removeClass('active');
150     $('#' + module).addClass('active');
151   }
152
153   constructor() {
154     this.initialize();
155   }
156
157   /**
158    * Refresh the HTML by fetching the menu again
159    */
160   public refreshMenu(): void {
161     $.ajax(TYPO3.settings.ajaxUrls.modulemenu).done((result: { [key: string]: string }): void => {
162       $('#menu').replaceWith(result.menu);
163       if (top.currentModuleLoaded) {
164         ModuleMenu.highlightModuleMenuItem(top.currentModuleLoaded);
165       }
166       Viewport.doLayout();
167     });
168   }
169
170   /**
171    * Reloads the frames
172    *
173    * Hint: This method can't be static (yet), as this must be bound to the ModuleMenu instance.
174    */
175   public reloadFrames(): void {
176     Viewport.NavigationContainer.refresh();
177     Viewport.ContentContainer.refresh();
178   }
179
180   /**
181    * Event handler called after clicking on the module menu item
182    *
183    * @param {string} name
184    * @param {string} params
185    * @param {JQueryEventObject} event
186    * @returns {JQueryDeferred<TriggerRequest>}
187    */
188   public showModule(name: string, params?: string, event?: JQueryEventObject): JQueryDeferred<TriggerRequest> {
189     params = params || '';
190     const moduleData = ModuleMenu.getRecordFromName(name);
191     return this.loadModuleComponents(
192       moduleData,
193       params,
194       new ClientRequest('typo3.showModule', event)
195     );
196   }
197
198   private initialize(): void {
199     const me = this;
200     let deferred = $.Deferred();
201     deferred.resolve();
202
203     // load the start module
204     if (top.startInModule && top.startInModule[0] && $('#' + top.startInModule[0]).length > 0) {
205       deferred = this.showModule(
206         top.startInModule[0],
207         top.startInModule[1]
208       );
209     } else {
210       // fetch first module
211       const $firstModule = $('.t3js-mainmodule:first');
212       if ($firstModule.attr('id')) {
213         deferred = this.showModule(
214           $firstModule.attr('id')
215         );
216       }
217       // else case: the main module has no entries, this is probably a backend
218       // user with very little access rights, maybe only the logout button and
219       // a user settings module in topbar.
220     }
221
222     deferred.then((): void => {
223       // check if module menu should be collapsed or not
224       const state = PersistentStorage.get('BackendComponents.States.typo3-module-menu');
225       if (state && state.collapsed) {
226         ModuleMenu.toggleMenu(state.collapsed === 'true');
227       }
228
229       // check if there are collapsed items in the users' configuration
230       const collapsedMainMenuItems = ModuleMenu.getCollapsedMainMenuItems();
231       $.each(collapsedMainMenuItems, (key: string, itm: boolean): void => {
232         if (itm !== true) {
233           return;
234         }
235
236         const $group = $('#' + key);
237         if ($group.length > 0) {
238           const $groupContainer = $group.find('.modulemenu-group-container');
239           $group.addClass('collapsed').removeClass('expanded');
240           Viewport.NavigationContainer.cleanup();
241           $groupContainer.hide().promise().done((): void => {
242             Viewport.doLayout();
243           });
244         }
245       });
246       me.initializeEvents();
247     });
248   }
249
250   private initializeEvents(): void {
251     $(document).on('click', '.modulemenu-group .modulemenu-group-header', (e: JQueryEventObject): void => {
252       const $group = $(e.currentTarget).parent('.modulemenu-group');
253       const $groupContainer = $group.find('.modulemenu-group-container');
254
255       Viewport.NavigationContainer.cleanup();
256       if ($group.hasClass('expanded')) {
257         ModuleMenu.addCollapsedMainMenuItem($group.attr('id'));
258         $group.addClass('collapsed').removeClass('expanded');
259         $groupContainer.stop().slideUp().promise().done((): void => {
260           Viewport.doLayout();
261         });
262       } else {
263         ModuleMenu.removeCollapseMainMenuItem($group.attr('id'));
264         $group.addClass('expanded').removeClass('collapsed');
265         $groupContainer.stop().slideDown().promise().done((): void => {
266           Viewport.doLayout();
267         });
268       }
269     });
270
271     // register clicking on sub modules
272     $(document).on('click', '.modulemenu-item,.t3-menuitem-submodule', (evt: JQueryEventObject): void => {
273       evt.preventDefault();
274       this.showModule($(evt.currentTarget).attr('id'), '', evt);
275     });
276     $(document).on('click', '.t3js-topbar-button-modulemenu', (evt: JQueryEventObject): void => {
277         evt.preventDefault();
278         ModuleMenu.toggleMenu();
279       }
280     );
281     $(document).on('click', '.t3js-scaffold-content-overlay', (evt: JQueryEventObject): void => {
282         evt.preventDefault();
283         ModuleMenu.toggleMenu(true);
284       }
285     );
286     $(document).on('click', '.t3js-topbar-button-navigationcomponent', (evt: JQueryEventObject): void => {
287       evt.preventDefault();
288       Viewport.NavigationContainer.toggle();
289     });
290   }
291
292   /**
293    * Shows requested module (e.g. list/page)
294    *
295    * @param {Object} moduleData
296    * @param {string} params
297    * @param {InteractionRequest} [interactionRequest]
298    * @return {jQuery.Deferred}
299    */
300   private loadModuleComponents(
301     moduleData: Module,
302     params: string,
303     interactionRequest: InteractionRequest
304   ): JQueryDeferred<TriggerRequest> {
305     const moduleName = moduleData.name;
306
307     // Allow other components e.g. Formengine to cancel switching between modules
308     // (e.g. you have unsaved changes in the form)
309     const deferred = Viewport.ContentContainer.beforeSetUrl(interactionRequest);
310     deferred.then(
311       $.proxy(
312         (): void => {
313           if (moduleData.navigationComponentId) {
314             this.loadNavigationComponent(moduleData.navigationComponentId);
315           } else if (moduleData.navigationFrameScript) {
316             Viewport.NavigationContainer.show('typo3-navigationIframe');
317             this.openInNavFrame(
318               moduleData.navigationFrameScript,
319               moduleData.navigationFrameScriptParam,
320               new TriggerRequest(
321                 'typo3.loadModuleComponents',
322                 interactionRequest
323               )
324             );
325           } else {
326             Viewport.NavigationContainer.hide();
327           }
328
329           ModuleMenu.highlightModuleMenuItem(moduleName);
330           this.loadedModule = moduleName;
331           params = ModuleMenu.includeId(moduleData, params);
332           this.openInContentFrame(
333             moduleData.link,
334             params,
335             new TriggerRequest(
336               'typo3.loadModuleComponents',
337               interactionRequest
338             )
339           );
340
341           // compatibility
342           top.currentSubScript = moduleData.link;
343           top.currentModuleLoaded = moduleName;
344
345           Viewport.doLayout();
346         },
347         this
348       )
349     );
350
351     return deferred;
352   }
353
354   /**
355    * Renders registered (non-iframe) navigation component e.g. a page tree
356    *
357    * @param {string} navigationComponentId
358    */
359   private loadNavigationComponent(navigationComponentId: string): void {
360     const me = this;
361
362     Viewport.NavigationContainer.show(navigationComponentId);
363     if (navigationComponentId === this.loadedNavigationComponentId) {
364       return;
365     }
366     const componentCssName = navigationComponentId.replace(/[/]/g, '_');
367     if (this.loadedNavigationComponentId !== '') {
368       $('#navigationComponent-' + this.loadedNavigationComponentId.replace(/[/]/g, '_')).hide();
369     }
370     if ($('.t3js-scaffold-content-navigation [data-component="' + navigationComponentId + '"]').length < 1) {
371       $('.t3js-scaffold-content-navigation')
372         .append($('<div />', {
373           'class': 'scaffold-content-navigation-component',
374           'data-component': navigationComponentId,
375           id: 'navigationComponent-' + componentCssName
376         }));
377     }
378
379     require([navigationComponentId], (NavigationComponent: NavigationComponentInterface): void => {
380       NavigationComponent.initialize('#navigationComponent-' + componentCssName);
381       Viewport.NavigationContainer.show(navigationComponentId);
382       me.loadedNavigationComponentId = navigationComponentId;
383     });
384   }
385
386   /**
387    * @param {string} url
388    * @param {string} params
389    * @param {InteractionRequest} interactionRequest
390    * @returns {JQueryDeferred<TriggerRequest>}
391    */
392   private openInNavFrame(url: string, params: string, interactionRequest: InteractionRequest): JQueryDeferred<TriggerRequest> {
393     const navUrl = url + (params ? (url.indexOf('?') !== -1 ? '&' : '?') + params : '');
394     const currentUrl = Viewport.NavigationContainer.getUrl();
395     const deferred = Viewport.NavigationContainer.setUrl(
396       url,
397       new TriggerRequest('typo3.openInNavFrame', interactionRequest)
398     );
399     if (currentUrl !== navUrl) {
400       // if deferred is already resolved, execute directly
401       if (deferred.state() === 'resolved') {
402         Viewport.NavigationContainer.refresh();
403         // otherwise hand in future callback
404       } else {
405         deferred.then(Viewport.NavigationContainer.refresh);
406       }
407     }
408     return deferred;
409   }
410
411   /**
412    * @param {string} url
413    * @param {string} params
414    * @param {InteractionRequest} interactionRequest
415    * @returns {JQueryDeferred<TriggerRequest>}
416    */
417   private openInContentFrame(url: string, params: string, interactionRequest: InteractionRequest):  JQueryDeferred<TriggerRequest> {
418     let deferred;
419
420     if (top.nextLoadModuleUrl) {
421       deferred = Viewport.ContentContainer.setUrl(
422         top.nextLoadModuleUrl,
423         new TriggerRequest('typo3.openInContentFrame', interactionRequest)
424       );
425       top.nextLoadModuleUrl = '';
426     } else {
427       const urlToLoad = url + (params ? (url.indexOf('?') !== -1 ? '&' : '?') + params : '');
428       deferred = Viewport.ContentContainer.setUrl(
429         urlToLoad,
430         new TriggerRequest('typo3.openInContentFrame', interactionRequest)
431       );
432     }
433
434     return deferred;
435   }
436 }
437
438 if (!top.TYPO3.ModuleMenu) {
439   top.TYPO3.ModuleMenu = {
440     App: new ModuleMenu()
441   };
442 }
443 const moduleMenuApp = top.TYPO3.ModuleMenu;
444
445 export = moduleMenuApp;