Modal.generate   F
last analyzed

Complexity

Conditions 12

Size

Total Lines 121
Code Lines 88

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 88
dl 0
loc 121
rs 3.7527
c 0
b 0
f 0
cc 12

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Modal.generate often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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