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