diff --git a/h5p.classes.php b/h5p.classes.php index 71e95c4..851515f 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -2898,7 +2898,12 @@ class H5PContentValidator { // Check if string is within allowed length if (isset($semantics->maxLength)) { - $text = mb_substr($text, 0, $semantics->maxLength); + if (!extension_loaded('mbstring')) { + $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'error'); + } + else { + $text = mb_substr($text, 0, $semantics->maxLength); + } } // Check if string is according to optional regexp in semantics @@ -2948,7 +2953,11 @@ class H5PContentValidator { // file name, 2. testing against a returned error array that could // never be more than 1 element long anyway, 3. recreating the regex // for every file. - if (!preg_match($wl_regex, mb_strtolower($file))) { + if (!extension_loaded('mbstring')) { + $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'error'); + $valid = FALSE; + } + else if (!preg_match($wl_regex, mb_strtolower($file))) { $this->h5pF->setErrorMessage($this->h5pF->t('File "%filename" not allowed. Only files with the following extensions are allowed: %files-allowed.', array('%filename' => $file, '%files-allowed' => $whitelist)), 'error'); $valid = FALSE; } diff --git a/js/h5p-confirmation-dialog.js b/js/h5p-confirmation-dialog.js index c948192..a06166b 100644 --- a/js/h5p-confirmation-dialog.js +++ b/js/h5p-confirmation-dialog.js @@ -17,6 +17,10 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { 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'); @@ -44,6 +48,16 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { 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; @@ -59,7 +73,15 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { // Create outer popup var popup = document.createElement('div'); popup.classList.add('h5p-confirmation-dialog-popup', 'hidden'); + 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'); @@ -81,6 +103,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { 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 @@ -92,41 +115,66 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { var cancelButton = document.createElement('button'); cancelButton.classList.add('h5p-core-cancel-button'); cancelButton.textContent = options.cancelText; - cancelButton.onclick = dialogCanceled; - cancelButton.onkeydown = function (e) { - if (e.which === 32) { // Space - dialogCanceled(e); - } - }; - buttons.appendChild(cancelButton); // Confirm button var confirmButton = document.createElement('button'); confirmButton.classList.add('h5p-core-button', 'h5p-confirmation-dialog-confirm-button'); confirmButton.textContent = options.confirmText; - confirmButton.onclick = dialogConfirmed; - confirmButton.onkeydown = function (e) { - if (e.which === 32) { // Space - dialogConfirmed(e); - } - }; - buttons.appendChild(confirmButton); // Exit button var exitButton = document.createElement('button'); exitButton.classList.add('h5p-confirmation-dialog-exit'); - exitButton.onclick = dialogCanceled; - exitButton.onkeydown = function (e) { + 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); + } + }); + buttons.appendChild(cancelButton); + + // 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 + flowTo(cancelButton, e); + } + }); + buttons.appendChild(confirmButton); + + // Exit handler + exitButton.addEventListener('click', dialogCanceled); + exitButton.addEventListener('keydown', function (e) { + if (e.which === 32) { // Space + dialogCanceled(e); + } + }); 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 @@ -137,6 +185,87 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { 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 @@ -167,7 +296,11 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { * @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 () { @@ -199,6 +332,11 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { this.hide = function () { popupBackground.classList.add('hiding'); popup.classList.add('hidden'); + + // Restore focus + stopCapturingFocus(); + previouslyFocused.focus(); + restoreUnderlay(); setTimeout(function () { popupBackground.classList.add('hidden'); wrapperElement.removeChild(popupBackground); @@ -214,3 +352,5 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { return ConfirmationDialog; }(H5P.EventDispatcher)); + +H5P.ConfirmationDialog.uniqueId = -1; diff --git a/js/h5p-data-view.js b/js/h5p-data-view.js index 0edfc56..89080e0 100644 --- a/js/h5p-data-view.js +++ b/js/h5p-data-view.js @@ -288,6 +288,11 @@ var H5PDataView = (function ($) { var self = this; if (self.pagination === undefined) { + if (self.table === undefined) { + // No table, no pagination + return; + } + // Create new widget var $pagerContainer = $('
', {'class': 'h5p-pagination'}); self.pagination = new H5PUtils.Pagination(num, self.limit, function (offset) { diff --git a/js/h5p.js b/js/h5p.js index 5a1c9e1..5de66e2 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -122,13 +122,15 @@ H5P.init = function (target) { delete contentData.contentUserData; var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '

' + H5P.t('contentChanged') + '

' + H5P.t('startingOver') + '

OK
', $container); H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { - $dialog.find('.h5p-dialog-ok-button').click(function () { - dialog.close(); - }).keypress(function (event) { - if (event.which === 32) { + + var closeDialog = function (event) { + if (event.type === 'click' || event.which === 32) { dialog.close(); + H5P.deleteUserData(contentId, 'state', 0); } - }); + }; + + $dialog.find('.h5p-dialog-ok-button').click(closeDialog).keypress(closeDialog); }); dialog.open(); }