[BUGFIX] Add missing translation value for Modals
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Private / TypeScript / Modal.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 {SeverityEnum} from './Enum/Severity';
15 import 'bootstrap';
16 import * as $ from 'jquery';
17 import Icons = require('./Icons');
18 import Severity = require('./Severity');
19
20 enum Identifiers {
21   modal = '.t3js-modal',
22   content = '.t3js-modal-content',
23   title = '.t3js-modal-title',
24   close = '.t3js-modal-close',
25   body = '.t3js-modal-body',
26   footer = '.t3js-modal-footer',
27   iframe = '.t3js-modal-iframe',
28   iconPlaceholder = '.t3js-modal-icon-placeholder'
29 }
30
31 enum Sizes {
32   small = 'small',
33   default = 'default',
34   medium = 'medium',
35   large = 'large',
36   full = 'full'
37 }
38
39 enum Styles {
40   default = 'default',
41   light = 'light',
42   dark = 'dark'
43 }
44
45 enum Types {
46   default = 'default',
47   ajax = 'ajax',
48   iframe = 'iframe'
49 }
50
51 interface Button {
52   text: string;
53   active: boolean;
54   btnClass: string;
55   name: string;
56   trigger: (e: JQueryEventObject) => {};
57   dataAttributes: { [key: string]: string };
58   icon: string;
59 }
60
61 interface Configuration {
62   type: Types;
63   title: string;
64   content: string | JQuery;
65   severity: SeverityEnum;
66   buttons: Array<Button>;
67   style: string;
68   size: string;
69   additionalCssClasses: Array<string>;
70   callback: Function;
71   ajaxCallback: Function;
72   ajaxTarget: string;
73 }
74
75 /**
76  * Module: TYPO3/CMS/Backend/Modal
77  * API for modal windows powered by Twitter Bootstrap.
78  */
79 class Modal {
80   public readonly sizes: any = Sizes;
81   public readonly styles: any = Styles;
82   public readonly types: any = Types;
83   public currentModal: JQuery = null;
84   private instances: Array<JQuery> = [];
85   private readonly $template: JQuery = $(
86     '<div class="t3js-modal modal fade">' +
87     '<div class="modal-dialog">' +
88     '<div class="t3js-modal-content modal-content">' +
89     '<div class="modal-header">' +
90     '<button class="t3js-modal-close close">' +
91     '<span aria-hidden="true">' +
92     '<span class="t3js-modal-icon-placeholder" data-icon="actions-close"></span>' +
93     '</span>' +
94     '<span class="sr-only"></span>' +
95     '</button>' +
96     '<h4 class="t3js-modal-title modal-title"></h4>' +
97     '</div>' +
98     '<div class="t3js-modal-body modal-body"></div>' +
99     '<div class="t3js-modal-footer modal-footer"></div>' +
100     '</div>' +
101     '</div>' +
102     '</div>'
103   );
104
105   private defaultConfiguration: Configuration = {
106     type: Types.default,
107     title: 'Information',
108     content: 'No content provided, please check your <code>Modal</code> configuration.',
109     severity: SeverityEnum.notice,
110     buttons: [],
111     style: Styles.default,
112     size: Sizes.default,
113     additionalCssClasses: [],
114     callback: $.noop(),
115     ajaxCallback: $.noop(),
116     ajaxTarget: null
117   };
118
119   constructor() {
120     $(document).on('modal-dismiss', this.dismiss);
121     this.initializeMarkupTrigger(document);
122   }
123
124   /**
125    * Close the current open modal
126    */
127   public dismiss(): void {
128     if (this.currentModal) {
129       this.currentModal.modal('hide');
130     }
131   }
132
133   /**
134    * Shows a confirmation dialog
135    * Events:
136    * - button.clicked
137    * - confirm.button.cancel
138    * - confirm.button.ok
139    *
140    * @param {string} title The title for the confirm modal
141    * @param {string | JQuery} content The content for the conform modal, e.g. the main question
142    * @param {SeverityEnum} severity Default SeverityEnum.warning
143    * @param {Array<Button>} buttons An array with buttons, default no buttons
144    * @param {Array<string>} additionalCssClasses Additional css classes to add to the modal
145    * @returns {JQuery}
146    */
147   public confirm(title: string,
148                  content: string | JQuery,
149                  severity: SeverityEnum = SeverityEnum.warning,
150                  buttons: Array<Object> = [],
151                  additionalCssClasses?: Array<string>): JQuery {
152     if (buttons.length === 0) {
153       buttons.push(
154         {
155           text: $(this).data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel',
156           active: true,
157           btnClass: 'btn-default',
158           name: 'cancel'
159         },
160         {
161           text: $(this).data('button-ok-text') || TYPO3.lang['button.ok'] || 'OK',
162           btnClass: 'btn-' + Severity.getCssClass(severity),
163           name: 'ok'
164         }
165       );
166     }
167
168     return this.advanced({
169       title,
170       content,
171       severity,
172       buttons,
173       additionalCssClasses,
174       callback: (currentModal: JQuery): void => {
175         currentModal.on('button.clicked', (e: JQueryEventObject): void => {
176           if (e.target.getAttribute('name') === 'cancel') {
177             $(e.currentTarget).trigger('confirm.button.cancel');
178           } else if (e.target.getAttribute('name') === 'ok') {
179             $(e.currentTarget).trigger('confirm.button.ok');
180           }
181         });
182       }
183     });
184   }
185
186   /**
187    * Load URL with AJAX, append the content to the modal-body
188    * and trigger the callback
189    *
190    * @param {string} title
191    * @param {SeverityEnum} severity
192    * @param {Array<Button>} buttons
193    * @param {string} url
194    * @param {Function} callback
195    * @param {string} target
196    * @returns {JQuery}
197    */
198   public loadUrl(title: string,
199                  severity: SeverityEnum = SeverityEnum.info,
200                  buttons: Array<Object>,
201                  url: string,
202                  callback?: Function,
203                  target?: string
204   ): JQuery {
205     return this.advanced({
206       type: Types.ajax,
207       title,
208       severity,
209       buttons,
210       ajaxCallback: callback,
211       ajaxTarget: target
212     });
213   }
214
215   /**
216    * Shows a dialog
217    *
218    * @param {string} title
219    * @param {string | JQuery} content
220    * @param {number} severity
221    * @param {Array<Object>} buttons
222    * @param {Array<string>} additionalCssClasses
223    * @returns {JQuery}
224    */
225   public show(title: string,
226               content: string | JQuery,
227               severity: SeverityEnum = SeverityEnum.info,
228               buttons?: Array<Object>,
229               additionalCssClasses?: Array<string>): JQuery {
230     return this.advanced({
231       type: Types.default,
232       title,
233       content,
234       severity,
235       buttons,
236       additionalCssClasses
237     });
238   }
239
240   /**
241    * Loads modal by configuration
242    *
243    * @param {object} configuration configuration for the modal
244    */
245   public advanced(configuration: { [key: string]: any }): JQuery {
246     // Validation of configuration
247     configuration.type = typeof configuration.type === 'string' && configuration.type in Types
248       ? configuration.type
249       : this.defaultConfiguration.type;
250     configuration.title = typeof configuration.title === 'string'
251       ? configuration.title
252       : this.defaultConfiguration.title;
253     configuration.content = typeof configuration.content === 'string' || typeof configuration.content === 'object'
254       ? configuration.content
255       : this.defaultConfiguration.content;
256     configuration.severity = typeof configuration.severity !== 'undefined'
257       ? configuration.severity
258       : this.defaultConfiguration.severity;
259     configuration.buttons = <Array<Button>>configuration.buttons || this.defaultConfiguration.buttons;
260     configuration.size = typeof configuration.size === 'string' && configuration.size in Sizes
261       ? configuration.size
262       : this.defaultConfiguration.size;
263     configuration.style = typeof configuration.style === 'string' && configuration.style in Styles
264       ? configuration.style
265       : this.defaultConfiguration.style;
266     configuration.additionalCssClasses = configuration.additionalCssClasses || this.defaultConfiguration.additionalCssClasses;
267     configuration.callback = typeof configuration.callback === 'function' ? configuration.callback : this.defaultConfiguration.callback;
268     configuration.ajaxCallback = typeof configuration.ajaxCallback === 'function'
269       ? configuration.ajaxCallback
270       : this.defaultConfiguration.ajaxCallback;
271     configuration.ajaxTarget = typeof configuration.ajaxTarget === 'string'
272       ? configuration.ajaxTarget
273       : this.defaultConfiguration.ajaxTarget;
274
275     return this.generate(<Configuration>configuration);
276   }
277
278   /**
279    * Initialize markup with data attributes
280    *
281    * @param {HTMLDocument} theDocument
282    */
283   private initializeMarkupTrigger(theDocument: HTMLDocument): void {
284     $(theDocument).on('click', '.t3js-modal-trigger', (evt: JQueryEventObject): void => {
285       evt.preventDefault();
286       const $element = $(evt.currentTarget);
287       const content = $element.data('content') || 'Are you sure?';
288       const severity = typeof SeverityEnum[$element.data('severity')] !== 'undefined'
289         ? SeverityEnum[$element.data('severity')]
290         : SeverityEnum.info;
291       let url = $element.data('url') || null;
292       if (url !== null) {
293         const separator = (url.indexOf('?') > -1) ? '&' : '?';
294         const params = $.param({data: $element.data()});
295         url = url + separator + params;
296       }
297       this.advanced({
298         type: url !== null ? Types.ajax : Types.default,
299         title: $element.data('title') || 'Alert',
300         content: url !== null ? url : content,
301         severity,
302         buttons: [
303           {
304             text: $element.data('button-close-text') || TYPO3.lang['button.close'] || 'Close',
305             active: true,
306             btnClass: 'btn-default',
307             trigger: (): void => {
308               this.currentModal.trigger('modal-dismiss');
309             }
310           },
311           {
312             text: $element.data('button-ok-text') || TYPO3.lang['button.ok'] || 'OK',
313             btnClass: 'btn-' + Severity.getCssClass(severity),
314             trigger: (): void => {
315               this.currentModal.trigger('modal-dismiss');
316               evt.target.ownerDocument.location.href = $element.data('href') || $element.attr('href');
317             }
318           }
319         ],
320       });
321     });
322   }
323
324   /**
325    * @param {Configuration} configuration
326    */
327   private generate(configuration: Configuration): JQuery {
328     const currentModal = this.$template.clone();
329     if (configuration.additionalCssClasses.length > 0) {
330       for (let additionalClass of configuration.additionalCssClasses) {
331         currentModal.addClass(additionalClass);
332       }
333     }
334     currentModal.addClass('modal-type-' + configuration.type);
335     currentModal.addClass('modal-severity-' + Severity.getCssClass(configuration.severity));
336     currentModal.addClass('modal-style-' + configuration.style);
337     currentModal.addClass('modal-size-' + configuration.size);
338     currentModal.attr('tabindex', '-1');
339     currentModal.find(Identifiers.title).text(configuration.title);
340     currentModal.find(Identifiers.close).on('click', (): void => {
341       currentModal.modal('hide');
342     });
343
344     if (configuration.type === 'ajax') {
345       const contentTarget = configuration.ajaxTarget ? configuration.ajaxTarget : Identifiers.body;
346       const $loaderTarget = currentModal.find(contentTarget);
347       Icons.getIcon('spinner-circle', Icons.sizes.default, null, null, Icons.markupIdentifiers.inline).done((icon: string): void => {
348         $loaderTarget.html('<div class="modal-loading">' + icon + '</div>');
349         $.get(
350           <string>configuration.content,
351           (response: string): void => {
352             this.currentModal.find(contentTarget)
353               .empty()
354               .append(response);
355             if (configuration.ajaxCallback) {
356               configuration.ajaxCallback();
357             }
358             this.currentModal.trigger('modal-loaded');
359           },
360           'html'
361         );
362       });
363     } else if (configuration.type === 'iframe') {
364       currentModal.find(Identifiers.body).append(
365         $('<iframe />', {
366           src: configuration.content,
367           'name': 'modal_frame',
368           'class': 'modal-iframe t3js-modal-iframe'
369         })
370       );
371       currentModal.find(Identifiers.iframe).on('load', (): void => {
372         currentModal.find(Identifiers.title).text(
373           (<HTMLIFrameElement>currentModal.find(Identifiers.iframe).get(0)).contentDocument.title
374         );
375       });
376     } else {
377       if (typeof configuration.content === 'string') {
378         // we need html, check if we have to wrap content in <p>
379         if (!/^<[a-z][\s\S]*>/i.test(configuration.content)) {
380           configuration.content = $('<p />').html(configuration.content);
381         }
382       }
383       currentModal.find(Identifiers.body).append(configuration.content);
384     }
385
386     // Add buttons
387     if (configuration.buttons.length > 0) {
388       for (let i = 0; i < configuration.buttons.length; i++) {
389         const button = configuration.buttons[i];
390         const $button = $('<button />', {'class': 'btn'});
391         $button.html('<span>' + button.text + '</span>');
392         if (button.active) {
393           $button.addClass('t3js-active');
394         }
395         if (button.btnClass !== '') {
396           $button.addClass(button.btnClass);
397         }
398         if (button.name !== '') {
399           $button.attr('name', button.name);
400         }
401         if (button.trigger) {
402           $button.on('click', button.trigger);
403         }
404         if (button.dataAttributes) {
405           if (Object.keys(button.dataAttributes).length > 0) {
406             Object.keys(button.dataAttributes).map((value: string): any => {
407               $button.attr('data-' + value, button.dataAttributes[value]);
408             });
409           }
410         }
411         if (button.icon) {
412           $button.prepend('<span class="t3js-modal-icon-placeholder" data-icon="' + button.icon + '"></span>');
413         }
414         currentModal.find(Identifiers.footer).append($button);
415       }
416       currentModal
417         .find(Identifiers.footer).find('button')
418         .on('click', (e: JQueryEventObject): void => {
419           $(e.currentTarget).trigger('button.clicked');
420         });
421     } else {
422       currentModal.find(Identifiers.footer).remove();
423     }
424
425     currentModal.on('shown.bs.modal', (e: JQueryEventObject): void => {
426       const $me = $(e.currentTarget);
427       // focus the button which was configured as active button
428       $me.find(Identifiers.footer).find('.t3js-active').first().focus();
429       // Get Icons
430       $me.find(Identifiers.iconPlaceholder).each((index: number, elem: Element): void => {
431         Icons.getIcon($(elem).data('icon'), Icons.sizes.small, null, null, Icons.markupIdentifiers.inline).done((icon: string): void => {
432           this.currentModal.find(Identifiers.iconPlaceholder + '[data-icon=' + $(icon).data('identifier') + ']').replaceWith(icon);
433         });
434       });
435     });
436
437     // Remove modal from Modal.instances when hidden
438     currentModal.on('hidden.bs.modal', (e: JQueryEventObject): void => {
439       if (this.instances.length > 0) {
440         const lastIndex = this.instances.length - 1;
441         this.instances.splice(lastIndex, 1);
442         this.currentModal = this.instances[lastIndex - 1];
443       }
444       currentModal.trigger('modal-destroyed');
445       $(e.currentTarget).remove();
446       // Keep class modal-open on body tag as long as open modals exist
447       if (this.instances.length > 0) {
448         $('body').addClass('modal-open');
449       }
450     });
451
452     // When modal is opened/shown add it to Modal.instances and make it Modal.currentModal
453     currentModal.on('show.bs.modal', (e: JQueryEventObject): void => {
454       this.currentModal = $(e.currentTarget);
455       this.instances.push(this.currentModal);
456     });
457     currentModal.on('modal-dismiss', (e: JQueryEventObject): void => {
458       // Hide modal, the bs.modal events will clean up Modal.instances
459       $(e.currentTarget).modal('hide');
460     });
461
462     if (configuration.callback) {
463       configuration.callback(currentModal);
464     }
465
466     return currentModal.modal();
467   }
468 }
469
470 let modalObject: Modal = null;
471 try {
472   if (parent && parent.window.TYPO3 && parent.window.TYPO3.Modal) {
473     // fetch from parent
474     // we need to trigger the event capturing again, in order to make sure this works inside iframes
475     parent.window.TYPO3.Modal.initializeMarkupTrigger(document);
476     modalObject = parent.window.TYPO3.Modal;
477   } else if (top && top.TYPO3.Modal) {
478     // fetch object from outer frame
479     // we need to trigger the event capturing again, in order to make sure this works inside iframes
480     top.TYPO3.Modal.initializeMarkupTrigger(document);
481     modalObject = top.TYPO3.Modal;
482   }
483 } catch (e) {
484   // This only happens if the opener, parent or top is some other url (eg a local file)
485   // which loaded the current window. Then the browser's cross domain policy jumps in
486   // and raises an exception.
487   // For this case we are safe and we can create our global object below.
488 }
489
490 if (!modalObject) {
491   modalObject = new Modal();
492
493   // expose as global object
494   TYPO3.Modal = modalObject;
495 }
496
497 export = modalObject;