[BUGFIX] Use passed severity for modal construction
[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') || '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') || '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;