411 lines
12 KiB
JavaScript
411 lines
12 KiB
JavaScript
/*global H5P*/
|
|
H5P.ConfirmationDialog = (function (EventDispatcher) {
|
|
"use strict";
|
|
|
|
/**
|
|
* Create a confirmation dialog
|
|
*
|
|
* @param [options] Options for confirmation dialog
|
|
* @param [options.instance] Instance that uses confirmation dialog
|
|
* @param [options.headerText] Header text
|
|
* @param [options.dialogText] Dialog text
|
|
* @param [options.cancelText] Cancel dialog button text
|
|
* @param [options.confirmText] Confirm dialog button text
|
|
* @param [options.hideCancel] Hide cancel button
|
|
* @param [options.hideExit] Hide exit button
|
|
* @param [options.skipRestoreFocus] Skip restoring focus when hiding the dialog
|
|
* @param [options.classes] Extra classes for popup
|
|
* @constructor
|
|
*/
|
|
function ConfirmationDialog(options) {
|
|
EventDispatcher.call(this);
|
|
var self = this;
|
|
|
|
// Make sure confirmation dialogs have unique id
|
|
H5P.ConfirmationDialog.uniqueId += 1;
|
|
var uniqueId = H5P.ConfirmationDialog.uniqueId;
|
|
|
|
// Default options
|
|
options = options || {};
|
|
options.headerText = options.headerText || H5P.t('confirmDialogHeader');
|
|
options.dialogText = options.dialogText || H5P.t('confirmDialogBody');
|
|
options.cancelText = options.cancelText || H5P.t('cancelLabel');
|
|
options.confirmText = options.confirmText || H5P.t('confirmLabel');
|
|
|
|
/**
|
|
* Handle confirming event
|
|
* @param {Event} e
|
|
*/
|
|
function dialogConfirmed(e) {
|
|
self.hide();
|
|
self.trigger('confirmed');
|
|
e.preventDefault();
|
|
}
|
|
|
|
/**
|
|
* Handle dialog canceled
|
|
* @param {Event} e
|
|
*/
|
|
function dialogCanceled(e) {
|
|
self.hide();
|
|
self.trigger('canceled');
|
|
e.preventDefault();
|
|
}
|
|
|
|
/**
|
|
* Flow focus to element
|
|
* @param {HTMLElement} element Next element to be focused
|
|
* @param {Event} e Original tab event
|
|
*/
|
|
function flowTo(element, e) {
|
|
element.focus();
|
|
e.preventDefault();
|
|
}
|
|
|
|
// Offset of exit button
|
|
var exitButtonOffset = 2 * 16;
|
|
var shadowOffset = 8;
|
|
|
|
// Determine if we are too large for our container and must resize
|
|
var resizeIFrame = false;
|
|
|
|
// Create background
|
|
var popupBackground = document.createElement('div');
|
|
popupBackground.classList
|
|
.add('h5p-confirmation-dialog-background', 'hidden', 'hiding');
|
|
|
|
// Create outer popup
|
|
var popup = document.createElement('div');
|
|
popup.classList.add('h5p-confirmation-dialog-popup', 'hidden');
|
|
if (options.classes) {
|
|
options.classes.forEach(function (popupClass) {
|
|
popup.classList.add(popupClass);
|
|
});
|
|
}
|
|
|
|
popup.setAttribute('role', 'dialog');
|
|
popup.setAttribute('aria-labelledby', 'h5p-confirmation-dialog-dialog-text-' + uniqueId);
|
|
popupBackground.appendChild(popup);
|
|
popup.addEventListener('keydown', function (e) {
|
|
if (e.which === 27) {// Esc key
|
|
// Exit dialog
|
|
dialogCanceled(e);
|
|
}
|
|
});
|
|
|
|
// Popup header
|
|
var header = document.createElement('div');
|
|
header.classList.add('h5p-confirmation-dialog-header');
|
|
popup.appendChild(header);
|
|
|
|
// Header text
|
|
var headerText = document.createElement('div');
|
|
headerText.classList.add('h5p-confirmation-dialog-header-text');
|
|
headerText.innerHTML = options.headerText;
|
|
header.appendChild(headerText);
|
|
|
|
// Popup body
|
|
var body = document.createElement('div');
|
|
body.classList.add('h5p-confirmation-dialog-body');
|
|
popup.appendChild(body);
|
|
|
|
// Popup text
|
|
var text = document.createElement('div');
|
|
text.classList.add('h5p-confirmation-dialog-text');
|
|
text.innerHTML = options.dialogText;
|
|
text.id = 'h5p-confirmation-dialog-dialog-text-' + uniqueId;
|
|
body.appendChild(text);
|
|
|
|
// Popup buttons
|
|
var buttons = document.createElement('div');
|
|
buttons.classList.add('h5p-confirmation-dialog-buttons');
|
|
body.appendChild(buttons);
|
|
|
|
// Cancel button
|
|
var cancelButton = document.createElement('button');
|
|
cancelButton.classList.add('h5p-core-cancel-button');
|
|
cancelButton.textContent = options.cancelText;
|
|
|
|
// Confirm button
|
|
var confirmButton = document.createElement('button');
|
|
confirmButton.classList.add('h5p-core-button');
|
|
confirmButton.classList.add('h5p-confirmation-dialog-confirm-button');
|
|
confirmButton.textContent = options.confirmText;
|
|
|
|
// Exit button
|
|
var exitButton = document.createElement('button');
|
|
exitButton.classList.add('h5p-confirmation-dialog-exit');
|
|
exitButton.setAttribute('aria-hidden', 'true');
|
|
exitButton.tabIndex = -1;
|
|
exitButton.title = options.cancelText;
|
|
|
|
// Cancel handler
|
|
cancelButton.addEventListener('click', dialogCanceled);
|
|
cancelButton.addEventListener('keydown', function (e) {
|
|
if (e.which === 32) { // Space
|
|
dialogCanceled(e);
|
|
}
|
|
else if (e.which === 9 && e.shiftKey) { // Shift-tab
|
|
flowTo(confirmButton, e);
|
|
}
|
|
});
|
|
|
|
if (!options.hideCancel) {
|
|
buttons.appendChild(cancelButton);
|
|
}
|
|
else {
|
|
// Center buttons
|
|
buttons.classList.add('center');
|
|
}
|
|
|
|
// Confirm handler
|
|
confirmButton.addEventListener('click', dialogConfirmed);
|
|
confirmButton.addEventListener('keydown', function (e) {
|
|
if (e.which === 32) { // Space
|
|
dialogConfirmed(e);
|
|
}
|
|
else if (e.which === 9 && !e.shiftKey) { // Tab
|
|
const nextButton = !options.hideCancel ? cancelButton : confirmButton;
|
|
flowTo(nextButton, e);
|
|
}
|
|
});
|
|
buttons.appendChild(confirmButton);
|
|
|
|
// Exit handler
|
|
exitButton.addEventListener('click', dialogCanceled);
|
|
exitButton.addEventListener('keydown', function (e) {
|
|
if (e.which === 32) { // Space
|
|
dialogCanceled(e);
|
|
}
|
|
});
|
|
if (!options.hideExit) {
|
|
popup.appendChild(exitButton);
|
|
}
|
|
|
|
// Wrapper element
|
|
var wrapperElement;
|
|
|
|
// Focus capturing
|
|
var focusPredator;
|
|
|
|
// Maintains hidden state of elements
|
|
var wrapperSiblingsHidden = [];
|
|
var popupSiblingsHidden = [];
|
|
|
|
// Element with focus before dialog
|
|
var previouslyFocused;
|
|
|
|
/**
|
|
* Set parent of confirmation dialog
|
|
* @param {HTMLElement} wrapper
|
|
* @returns {H5P.ConfirmationDialog}
|
|
*/
|
|
this.appendTo = function (wrapper) {
|
|
wrapperElement = wrapper;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Capture the focus element, send it to confirmation button
|
|
* @param {Event} e Original focus event
|
|
*/
|
|
var captureFocus = function (e) {
|
|
if (!popupBackground.contains(e.target)) {
|
|
e.preventDefault();
|
|
confirmButton.focus();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Hide siblings of element from assistive technology
|
|
*
|
|
* @param {HTMLElement} element
|
|
* @returns {Array} The previous hidden state of all siblings
|
|
*/
|
|
var hideSiblings = function (element) {
|
|
var hiddenSiblings = [];
|
|
var siblings = element.parentNode.children;
|
|
var i;
|
|
for (i = 0; i < siblings.length; i += 1) {
|
|
// Preserve hidden state
|
|
hiddenSiblings[i] = siblings[i].getAttribute('aria-hidden') ?
|
|
true : false;
|
|
|
|
if (siblings[i] !== element) {
|
|
siblings[i].setAttribute('aria-hidden', true);
|
|
}
|
|
}
|
|
return hiddenSiblings;
|
|
};
|
|
|
|
/**
|
|
* Restores assistive technology state of element's siblings
|
|
*
|
|
* @param {HTMLElement} element
|
|
* @param {Array} hiddenSiblings Hidden state of all siblings
|
|
*/
|
|
var restoreSiblings = function (element, hiddenSiblings) {
|
|
var siblings = element.parentNode.children;
|
|
var i;
|
|
for (i = 0; i < siblings.length; i += 1) {
|
|
if (siblings[i] !== element && !hiddenSiblings[i]) {
|
|
siblings[i].removeAttribute('aria-hidden');
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Start capturing focus of parent and send it to dialog
|
|
*/
|
|
var startCapturingFocus = function () {
|
|
focusPredator = wrapperElement.parentNode || wrapperElement;
|
|
focusPredator.addEventListener('focus', captureFocus, true);
|
|
};
|
|
|
|
/**
|
|
* Clean up event listener for capturing focus
|
|
*/
|
|
var stopCapturingFocus = function () {
|
|
focusPredator.removeAttribute('aria-hidden');
|
|
focusPredator.removeEventListener('focus', captureFocus, true);
|
|
};
|
|
|
|
/**
|
|
* Hide siblings in underlay from assistive technologies
|
|
*/
|
|
var disableUnderlay = function () {
|
|
wrapperSiblingsHidden = hideSiblings(wrapperElement);
|
|
popupSiblingsHidden = hideSiblings(popupBackground);
|
|
};
|
|
|
|
/**
|
|
* Restore state of underlay for assistive technologies
|
|
*/
|
|
var restoreUnderlay = function () {
|
|
restoreSiblings(wrapperElement, wrapperSiblingsHidden);
|
|
restoreSiblings(popupBackground, popupSiblingsHidden);
|
|
};
|
|
|
|
/**
|
|
* Fit popup to container. Makes sure it doesn't overflow.
|
|
* @params {number} [offsetTop] Offset of popup
|
|
*/
|
|
var fitToContainer = function (offsetTop) {
|
|
var popupOffsetTop = parseInt(popup.style.top, 10);
|
|
if (offsetTop !== undefined) {
|
|
popupOffsetTop = offsetTop;
|
|
}
|
|
|
|
if (!popupOffsetTop) {
|
|
popupOffsetTop = 0;
|
|
}
|
|
|
|
// Overflows height
|
|
if (popupOffsetTop + popup.offsetHeight > wrapperElement.offsetHeight) {
|
|
popupOffsetTop = wrapperElement.offsetHeight - popup.offsetHeight - shadowOffset;
|
|
}
|
|
|
|
if (popupOffsetTop - exitButtonOffset <= 0) {
|
|
popupOffsetTop = exitButtonOffset + shadowOffset;
|
|
|
|
// We are too big and must resize
|
|
resizeIFrame = true;
|
|
}
|
|
popup.style.top = popupOffsetTop + 'px';
|
|
};
|
|
|
|
/**
|
|
* Show confirmation dialog
|
|
* @params {number} offsetTop Offset top
|
|
* @returns {H5P.ConfirmationDialog}
|
|
*/
|
|
this.show = function (offsetTop) {
|
|
// Capture focused item
|
|
previouslyFocused = document.activeElement;
|
|
wrapperElement.appendChild(popupBackground);
|
|
startCapturingFocus();
|
|
disableUnderlay();
|
|
popupBackground.classList.remove('hidden');
|
|
fitToContainer(offsetTop);
|
|
setTimeout(function () {
|
|
popup.classList.remove('hidden');
|
|
popupBackground.classList.remove('hiding');
|
|
|
|
setTimeout(function () {
|
|
// Focus confirm button
|
|
confirmButton.focus();
|
|
|
|
// Resize iFrame if necessary
|
|
if (resizeIFrame && options.instance) {
|
|
var minHeight = parseInt(popup.offsetHeight, 10) +
|
|
exitButtonOffset + (2 * shadowOffset);
|
|
self.setViewPortMinimumHeight(minHeight);
|
|
options.instance.trigger('resize');
|
|
resizeIFrame = false;
|
|
}
|
|
}, 100);
|
|
}, 0);
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Hide confirmation dialog
|
|
* @returns {H5P.ConfirmationDialog}
|
|
*/
|
|
this.hide = function () {
|
|
popupBackground.classList.add('hiding');
|
|
popup.classList.add('hidden');
|
|
|
|
// Restore focus
|
|
stopCapturingFocus();
|
|
if (!options.skipRestoreFocus) {
|
|
previouslyFocused.focus();
|
|
}
|
|
restoreUnderlay();
|
|
setTimeout(function () {
|
|
popupBackground.classList.add('hidden');
|
|
wrapperElement.removeChild(popupBackground);
|
|
self.setViewPortMinimumHeight(null);
|
|
}, 100);
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Retrieve element
|
|
*
|
|
* @return {HTMLElement}
|
|
*/
|
|
this.getElement = function () {
|
|
return popup;
|
|
};
|
|
|
|
/**
|
|
* Get previously focused element
|
|
* @return {HTMLElement}
|
|
*/
|
|
this.getPreviouslyFocused = function () {
|
|
return previouslyFocused;
|
|
};
|
|
|
|
/**
|
|
* Sets the minimum height of the view port
|
|
*
|
|
* @param {number|null} minHeight
|
|
*/
|
|
this.setViewPortMinimumHeight = function (minHeight) {
|
|
var container = document.querySelector('.h5p-container') || document.body;
|
|
container.style.minHeight = (typeof minHeight === 'number') ? (minHeight + 'px') : minHeight;
|
|
};
|
|
}
|
|
|
|
ConfirmationDialog.prototype = Object.create(EventDispatcher.prototype);
|
|
ConfirmationDialog.prototype.constructor = ConfirmationDialog;
|
|
|
|
return ConfirmationDialog;
|
|
|
|
}(H5P.EventDispatcher));
|
|
|
|
H5P.ConfirmationDialog.uniqueId = -1;
|