Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/*global H5P*/
2
H5P.ConfirmationDialog = (function (EventDispatcher) {
3
  "use strict";
4
 
5
  /**
6
   * Create a confirmation dialog
7
   *
8
   * @param [options] Options for confirmation dialog
9
   * @param [options.instance] Instance that uses confirmation dialog
10
   * @param [options.headerText] Header text
11
   * @param [options.dialogText] Dialog text
12
   * @param [options.cancelText] Cancel dialog button text
13
   * @param [options.confirmText] Confirm dialog button text
14
   * @param [options.hideCancel] Hide cancel button
15
   * @param [options.hideExit] Hide exit button
16
   * @param [options.skipRestoreFocus] Skip restoring focus when hiding the dialog
17
   * @param [options.classes] Extra classes for popup
18
   * @constructor
19
   */
20
  function ConfirmationDialog(options) {
21
    EventDispatcher.call(this);
22
    var self = this;
23
 
24
    // Make sure confirmation dialogs have unique id
25
    H5P.ConfirmationDialog.uniqueId += 1;
26
    var uniqueId = H5P.ConfirmationDialog.uniqueId;
27
 
28
    // Default options
29
    options = options || {};
30
    options.headerText = options.headerText || H5P.t('confirmDialogHeader');
31
    options.dialogText = options.dialogText || H5P.t('confirmDialogBody');
32
    options.cancelText = options.cancelText || H5P.t('cancelLabel');
33
    options.confirmText = options.confirmText || H5P.t('confirmLabel');
34
 
35
    /**
36
     * Handle confirming event
37
     * @param {Event} e
38
     */
39
    function dialogConfirmed(e) {
40
      self.hide();
41
      self.trigger('confirmed');
42
      e.preventDefault();
43
    }
44
 
45
    /**
46
     * Handle dialog canceled
47
     * @param {Event} e
48
     */
49
    function dialogCanceled(e) {
50
      self.hide();
51
      self.trigger('canceled');
52
      e.preventDefault();
53
    }
54
 
55
    /**
56
     * Flow focus to element
57
     * @param {HTMLElement} element Next element to be focused
58
     * @param {Event} e Original tab event
59
     */
60
    function flowTo(element, e) {
61
      element.focus();
62
      e.preventDefault();
63
    }
64
 
65
    // Offset of exit button
66
    var exitButtonOffset = 2 * 16;
67
    var shadowOffset = 8;
68
 
69
    // Determine if we are too large for our container and must resize
70
    var resizeIFrame = false;
71
 
72
    // Create background
73
    var popupBackground = document.createElement('div');
74
    popupBackground.classList
75
      .add('h5p-confirmation-dialog-background', 'hidden', 'hiding');
76
 
77
    // Create outer popup
78
    var popup = document.createElement('div');
79
    popup.classList.add('h5p-confirmation-dialog-popup', 'hidden');
80
    if (options.classes) {
81
      options.classes.forEach(function (popupClass) {
82
        popup.classList.add(popupClass);
83
      });
84
    }
85
 
86
    popup.setAttribute('role', 'dialog');
87
    popup.setAttribute('aria-labelledby', 'h5p-confirmation-dialog-dialog-text-' + uniqueId);
88
    popupBackground.appendChild(popup);
89
    popup.addEventListener('keydown', function (e) {
90
      if (e.key === 'Escape') {// Esc key
91
        // Exit dialog
92
        dialogCanceled(e);
93
      }
94
    });
95
 
96
    // Popup header
97
    var header = document.createElement('div');
98
    header.classList.add('h5p-confirmation-dialog-header');
99
    popup.appendChild(header);
100
 
101
    // Header text
102
    var headerText = document.createElement('div');
103
    headerText.classList.add('h5p-confirmation-dialog-header-text');
104
    headerText.innerHTML = options.headerText;
105
    header.appendChild(headerText);
106
 
107
    // Popup body
108
    var body = document.createElement('div');
109
    body.classList.add('h5p-confirmation-dialog-body');
110
    popup.appendChild(body);
111
 
112
    // Popup text
113
    var text = document.createElement('div');
114
    text.classList.add('h5p-confirmation-dialog-text');
115
    text.innerHTML = options.dialogText;
116
    text.id = 'h5p-confirmation-dialog-dialog-text-' + uniqueId;
117
    body.appendChild(text);
118
 
119
    // Popup buttons
120
    var buttons = document.createElement('div');
121
    buttons.classList.add('h5p-confirmation-dialog-buttons');
122
    body.appendChild(buttons);
123
 
124
    // Cancel button
125
    var cancelButton = document.createElement('button');
126
    cancelButton.classList.add('h5p-core-cancel-button');
127
    cancelButton.textContent = options.cancelText;
128
 
129
    // Confirm button
130
    var confirmButton = document.createElement('button');
131
    confirmButton.classList.add('h5p-core-button');
132
    confirmButton.classList.add('h5p-confirmation-dialog-confirm-button');
133
    confirmButton.textContent = options.confirmText;
134
 
135
    // Exit button
136
    var exitButton = document.createElement('button');
137
    exitButton.classList.add('h5p-confirmation-dialog-exit');
138
    exitButton.tabIndex = -1;
139
    exitButton.setAttribute('aria-label', options.cancelText);
140
 
141
    // Cancel handler
142
    cancelButton.addEventListener('click', dialogCanceled);
143
    cancelButton.addEventListener('keydown', function (e) {
144
      if (e.key === ' ') { // Space
145
        dialogCanceled(e);
146
      }
147
      else if (e.key === 'Tab' && e.shiftKey) { // Shift-tab
148
        const nextbutton = options.hideExit ? confirmButton : exitButton;
149
        flowTo(nextbutton, e);
150
      }
151
    });
152
 
153
    if (!options.hideCancel) {
154
      buttons.appendChild(cancelButton);
155
    }
156
    else {
157
      // Center buttons
158
      buttons.classList.add('center');
159
    }
160
 
161
    // Confirm handler
162
    confirmButton.addEventListener('click', dialogConfirmed);
163
    confirmButton.addEventListener('keydown', function (e) {
164
      if (e.key === ' ') { // Space
165
        dialogConfirmed(e);
166
      }
167
      else if (e.key === 'Tab' && !e.shiftKey) { // Tab
168
        let nextButton = confirmButton;
169
        if (!options.hideExit) {
170
          nextButton = exitButton;
171
        }
172
        else if (!options.hideCancel) {
173
          nextButton = cancelButton;
174
        }
175
        flowTo(nextButton, e);
176
      }
177
    });
178
    buttons.appendChild(confirmButton);
179
 
180
    // Exit handler
181
    exitButton.addEventListener('click', dialogCanceled);
182
    exitButton.addEventListener('keydown', function (e) {
183
      if (e.key === ' ') { // Space
184
        dialogCanceled(e);
185
      }
186
      else if (e.key === 'Tab' && !e.shiftKey) { // Tab
187
        const nextButton = options.hideCancel ? confirmButton : cancelButton;
188
        flowTo(nextButton, e);
189
      }
190
    });
191
    if (!options.hideExit) {
192
      popup.appendChild(exitButton);
193
    }
194
 
195
    // Wrapper element
196
    var wrapperElement;
197
 
198
    // Focus capturing
199
    var focusPredator;
200
 
201
    // Maintains hidden state of elements
202
    var wrapperSiblingsHidden = [];
203
    var popupSiblingsHidden = [];
204
 
205
    // Element with focus before dialog
206
    var previouslyFocused;
207
 
208
    /**
209
     * Set parent of confirmation dialog
210
     * @param {HTMLElement} wrapper
211
     * @returns {H5P.ConfirmationDialog}
212
     */
213
    this.appendTo = function (wrapper) {
214
      wrapperElement = wrapper;
215
      return this;
216
    };
217
 
218
    /**
219
     * Capture the focus element, send it to confirmation button
220
     * @param {Event} e Original focus event
221
     */
222
    var captureFocus = function (e) {
223
      if (!popupBackground.contains(e.target)) {
224
        e.preventDefault();
225
        confirmButton.focus();
226
      }
227
    };
228
 
229
    /**
230
     * Hide siblings of element from assistive technology
231
     *
232
     * @param {HTMLElement} element
233
     * @returns {Array} The previous hidden state of all siblings
234
     */
235
    var hideSiblings = function (element) {
236
      var hiddenSiblings = [];
237
      var siblings = element.parentNode.children;
238
      var i;
239
      for (i = 0; i < siblings.length; i += 1) {
240
        // Preserve hidden state
241
        hiddenSiblings[i] = siblings[i].getAttribute('aria-hidden') ?
242
          true : false;
243
 
244
        if (siblings[i] !== element) {
245
          siblings[i].setAttribute('aria-hidden', true);
246
        }
247
      }
248
      return hiddenSiblings;
249
    };
250
 
251
    /**
252
     * Restores assistive technology state of element's siblings
253
     *
254
     * @param {HTMLElement} element
255
     * @param {Array} hiddenSiblings Hidden state of all siblings
256
     */
257
    var restoreSiblings = function (element, hiddenSiblings) {
258
      var siblings = element.parentNode.children;
259
      var i;
260
      for (i = 0; i < siblings.length; i += 1) {
261
        if (siblings[i] !== element && !hiddenSiblings[i]) {
262
          siblings[i].removeAttribute('aria-hidden');
263
        }
264
      }
265
    };
266
 
267
    /**
268
     * Start capturing focus of parent and send it to dialog
269
     */
270
    var startCapturingFocus = function () {
271
      focusPredator = wrapperElement.parentNode || wrapperElement;
272
      focusPredator.addEventListener('focus', captureFocus, true);
273
    };
274
 
275
    /**
276
     * Clean up event listener for capturing focus
277
     */
278
    var stopCapturingFocus = function () {
279
      focusPredator.removeAttribute('aria-hidden');
280
      focusPredator.removeEventListener('focus', captureFocus, true);
281
    };
282
 
283
    /**
284
     * Hide siblings in underlay from assistive technologies
285
     */
286
    var disableUnderlay = function () {
287
      wrapperSiblingsHidden = hideSiblings(wrapperElement);
288
      popupSiblingsHidden = hideSiblings(popupBackground);
289
    };
290
 
291
    /**
292
     * Restore state of underlay for assistive technologies
293
     */
294
    var restoreUnderlay = function () {
295
      restoreSiblings(wrapperElement, wrapperSiblingsHidden);
296
      restoreSiblings(popupBackground, popupSiblingsHidden);
297
    };
298
 
299
    /**
300
     * Fit popup to container. Makes sure it doesn't overflow.
301
     * @params {number} [offsetTop] Offset of popup
302
     */
303
    var fitToContainer = function (offsetTop) {
304
      var popupOffsetTop = parseInt(popup.style.top, 10);
305
      if (offsetTop !== undefined) {
306
        popupOffsetTop = offsetTop;
307
      }
308
 
309
      if (!popupOffsetTop) {
310
        popupOffsetTop = 0;
311
      }
312
 
313
      // Overflows height
314
      if (popupOffsetTop + popup.offsetHeight > wrapperElement.offsetHeight) {
315
        popupOffsetTop = wrapperElement.offsetHeight - popup.offsetHeight - shadowOffset;
316
      }
317
 
318
      if (popupOffsetTop - exitButtonOffset <= 0) {
319
        popupOffsetTop = exitButtonOffset + shadowOffset;
320
 
321
        // We are too big and must resize
322
        resizeIFrame = true;
323
      }
324
      popup.style.top = popupOffsetTop + 'px';
325
    };
326
 
327
    /**
328
     * Show confirmation dialog
329
     * @params {number} offsetTop Offset top
330
     * @returns {H5P.ConfirmationDialog}
331
     */
332
    this.show = function (offsetTop) {
333
      // Capture focused item
334
      previouslyFocused = document.activeElement;
335
      wrapperElement.appendChild(popupBackground);
336
      startCapturingFocus();
337
      disableUnderlay();
338
      popupBackground.classList.remove('hidden');
339
      fitToContainer(offsetTop);
340
      setTimeout(function () {
341
        popup.classList.remove('hidden');
342
        popupBackground.classList.remove('hiding');
343
 
344
        setTimeout(function () {
345
          // Focus confirm button
346
          confirmButton.focus();
347
 
348
          // Resize iFrame if necessary
349
          if (resizeIFrame && options.instance) {
350
            var minHeight = parseInt(popup.offsetHeight, 10) +
351
              exitButtonOffset + (2 * shadowOffset);
352
            self.setViewPortMinimumHeight(minHeight);
353
            options.instance.trigger('resize');
354
            resizeIFrame = false;
355
          }
356
        }, 100);
357
      }, 0);
358
 
359
      return this;
360
    };
361
 
362
    /**
363
     * Hide confirmation dialog
364
     * @returns {H5P.ConfirmationDialog}
365
     */
366
    this.hide = function () {
367
      popupBackground.classList.add('hiding');
368
      popup.classList.add('hidden');
369
 
370
      // Restore focus
371
      stopCapturingFocus();
372
      if (!options.skipRestoreFocus) {
373
        previouslyFocused.focus();
374
      }
375
      restoreUnderlay();
376
      setTimeout(function () {
377
        popupBackground.classList.add('hidden');
378
        wrapperElement.removeChild(popupBackground);
379
        self.setViewPortMinimumHeight(null);
380
      }, 100);
381
 
382
      return this;
383
    };
384
 
385
    /**
386
     * Retrieve element
387
     *
388
     * @return {HTMLElement}
389
     */
390
    this.getElement = function () {
391
      return popup;
392
    };
393
 
394
    /**
395
     * Get previously focused element
396
     * @return {HTMLElement}
397
     */
398
    this.getPreviouslyFocused = function () {
399
      return previouslyFocused;
400
    };
401
 
402
    /**
403
     * Sets the minimum height of the view port
404
     *
405
     * @param {number|null} minHeight
406
     */
407
    this.setViewPortMinimumHeight = function (minHeight) {
408
      var container = document.querySelector('.h5p-container') || document.body;
409
      container.style.minHeight = (typeof minHeight === 'number') ? (minHeight + 'px') : minHeight;
410
    };
411
  }
412
 
413
  ConfirmationDialog.prototype = Object.create(EventDispatcher.prototype);
414
  ConfirmationDialog.prototype.constructor = ConfirmationDialog;
415
 
416
  return ConfirmationDialog;
417
 
418
}(H5P.EventDispatcher));
419
 
420
H5P.ConfirmationDialog.uniqueId = -1;