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