8a6636e696f069d39390b73dc33f2f07aa4967f6
[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 $((): void => 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;