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