diff --git a/lib/amd/build/local/aria/focuslock.min.js b/lib/amd/build/local/aria/focuslock.min.js index c1836621d45..07916322519 100644 --- a/lib/amd/build/local/aria/focuslock.min.js +++ b/lib/amd/build/local/aria/focuslock.min.js @@ -1,2 +1,2 @@ -define ("core/local/aria/focuslock",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.untrapFocus=a.trapFocus=void 0;var b={focusable:"input:not([type=\"hidden\"]), a[href], button, textarea, select, [tabindex]"},c=[],d=[],e=[],f=null,g=!1,h=!1,i=function(a){if(g){return}var b=n();if(!b.parentNode){s()}if(b.contains(a.target)){f=a.target}else{j();if(f==document.activeElement){k()}f=document.activeElement}},j=function(){var a=n(),c=Array.from(a.querySelectorAll(b.focusable));return c.some(function(a){return m(a)})},k=function(){var a=n(),c=Array.from(a.querySelectorAll(b.focusable)).reverse();return c.some(function(a){return m(a)})},l=function(a){if(0.\n\n/**\n * Tab locking system.\n *\n * This is based on code and examples provided in the ARIA specification.\n * https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html\n *\n * @module core/tablock\n * @class tablock\n * @package core\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nconst selectors = {\n focusable: 'input:not([type=\"hidden\"]), a[href], button, textarea, select, [tabindex]',\n};\n\nconst lockRegionStack = [];\nconst initialFocusElementStack = [];\nconst finalFocusElementStack = [];\n\nlet lastFocus = null;\nlet ignoreFocusChanges = false;\nlet isLocked = false;\n\n/**\n * The lock handler.\n *\n * This is the item that does a majority of the work.\n * The overall logic from this comes from the examles in the WCAG guidelines.\n *\n * The general idea is that if the focus is not held within by an Element within the lock region, then we replace focus\n * on the first element in the lock region. If the first element is the element previously selected prior to the\n * user-initiated focus change, then instead jump to the last element in the lock region.\n *\n * This gives us a solution which supports focus locking of any kind, which loops in both directions, and which\n * prevents the lock from escaping the modal entirely.\n *\n * @param {Event} event The event from the focus change\n */\nconst lockHandler = event => {\n if (ignoreFocusChanges) {\n // The focus change was made by an internal call to set focus.\n return;\n }\n\n const lockRegion = getCurrentLockRegion();\n\n if (!lockRegion.parentNode) {\n // The lock region does not exist.\n // Perhaps it was removed without being untrapped.\n untrapFocus();\n }\n\n if (lockRegion.contains(event.target)) {\n lastFocus = event.target;\n } else {\n focusFirstDescendant();\n if (lastFocus == document.activeElement) {\n focusLastDescendant();\n }\n lastFocus = document.activeElement;\n }\n};\n\n/**\n * Focus the first descendant of the current lock region.\n *\n * @returns {Bool} Whether a node was focused\n */\nconst focusFirstDescendant = () => {\n const lockRegion = getCurrentLockRegion();\n\n // Grab all elements in the lock region and attempt to focus each element until one is focused.\n // We can capture most of this in the query selector, but some cases may still reject focus.\n // For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector\n // to capture this.\n // The use of Array.some just ensures that we stop as soon as we have a successful focus.\n const focusableElements = Array.from(lockRegion.querySelectorAll(selectors.focusable));\n return focusableElements.some(focusableElement => attemptFocus(focusableElement));\n};\n\n/**\n * Focus the last descendant of the current lock region.\n *\n * @returns {Bool} Whether a node was focused\n */\nconst focusLastDescendant = () => {\n const lockRegion = getCurrentLockRegion();\n\n // Grab all elements in the lock region, reverse them, and attempt to focus each element until one is focused.\n // We can capture most of this in the query selector, but some cases may still reject focus.\n // For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector\n // to capture this.\n // The use of Array.some just ensures that we stop as soon as we have a successful focus.\n const focusableElements = Array.from(lockRegion.querySelectorAll(selectors.focusable)).reverse();\n return focusableElements.some(focusableElement => attemptFocus(focusableElement));\n};\n\n/**\n * Check whether the supplied focusTarget is actually focusable.\n * There are cases where a normally focusable element can reject focus.\n *\n * Note: This example is a wholesale copy of the WCAG example.\n *\n * @param {HTMLElement} focusTarget\n * @returns {Bool}\n */\nconst isFocusable = focusTarget => {\n if (focusTarget.tabIndex > 0 || (focusTarget.tabIndex === 0 && focusTarget.getAttribute('tabIndex') !== null)) {\n return true;\n }\n\n if (focusTarget.disabled) {\n return false;\n }\n\n switch (focusTarget.nodeName) {\n case 'A':\n return !!focusTarget.href && focusTarget.rel != 'ignore';\n case 'INPUT':\n return focusTarget.type != 'hidden' && focusTarget.type != 'file';\n case 'BUTTON':\n case 'SELECT':\n case 'TEXTAREA':\n return true;\n default:\n return false;\n }\n};\n\n/**\n * Attempt to focus the supplied focusTarget.\n *\n * Note: This example is a heavily inspired by the WCAG example.\n *\n * @param {HTMLElement} focusTarget\n * @returns {Bool} Whether focus was successful o rnot.\n */\nconst attemptFocus = focusTarget => {\n if (!isFocusable(focusTarget)) {\n return false;\n }\n\n // The ignoreFocusChanges variable prevents the focus event handler from interfering and entering a fight with itself.\n ignoreFocusChanges = true;\n\n try {\n focusTarget.focus();\n } catch (e) {\n // Ignore failures. We will just try to focus the next element in the list.\n // eslint-disable-line\n }\n\n ignoreFocusChanges = false;\n\n // If focus was successful the activeElement will be the one we focused.\n return (document.activeElement === focusTarget);\n};\n\n/**\n * Get the current lock region from the top of the stack.\n *\n * @returns {HTMLElement}\n */\nconst getCurrentLockRegion = () => {\n return lockRegionStack[lockRegionStack.length - 1];\n};\n\n/**\n * Add a new lock region to the stack.\n *\n * @param {HTMLElement} newLockRegion\n */\nconst addLockRegionToStack = newLockRegion => {\n if (newLockRegion === getCurrentLockRegion()) {\n return;\n }\n\n lockRegionStack.push(newLockRegion);\n const currentLockRegion = getCurrentLockRegion();\n\n // Append an empty div which can be focused just outside of the item locked.\n // This locks tab focus to within the tab region, and does not allow it to extend back into the window by\n // guaranteeing the existence of a tabable item after the lock region which can be focused but which will be caught\n // by the handler.\n const element = document.createElement('div');\n element.tabIndex = 0;\n element.style.position = 'fixed';\n element.style.top = 0;\n element.style.left = 0;\n\n const initialNode = element.cloneNode();\n currentLockRegion.parentNode.insertBefore(initialNode, currentLockRegion);\n initialFocusElementStack.push(initialNode);\n\n const finalNode = element.cloneNode();\n currentLockRegion.parentNode.insertBefore(finalNode, currentLockRegion.nextSibling);\n finalFocusElementStack.push(finalNode);\n};\n\n/**\n * Remove the top lock region from the stack.\n */\nconst removeLastLockRegionFromStack = () => {\n // Take the top element off the stack, and replce the current lockRegion value.\n lockRegionStack.pop();\n\n const finalNode = finalFocusElementStack.pop();\n if (finalNode) {\n // The final focus element may have been removed if it was part of a parent item.\n finalNode.remove();\n }\n\n const initialNode = initialFocusElementStack.pop();\n if (initialNode) {\n // The initial focus element may have been removed if it was part of a parent item.\n initialNode.remove();\n }\n};\n\n/**\n * Whether any region is left in the stack.\n *\n * @return {Bool}\n */\nconst hasTrappedRegionsInStack = () => {\n return !!lockRegionStack.length;\n};\n\n/**\n * Start trapping the focus and lock it to the specified newLockRegion.\n *\n * @param {HTMLElement} newLockRegion The container to lock focus to\n */\nexport const trapFocus = newLockRegion => {\n // Update the lock region stack.\n // This allows us to support nesting.\n addLockRegionToStack(newLockRegion);\n\n if (!isLocked) {\n // Add the focus handler.\n document.addEventListener('focus', lockHandler, true);\n }\n\n // Attempt to focus on the first item in the lock region.\n if (!focusFirstDescendant()) {\n const currentLockRegion = getCurrentLockRegion();\n\n // No focusable descendants found in the region yet.\n // This can happen when the region is locked before content is generated.\n // Focus on the region itself for now.\n const originalRegionTabIndex = currentLockRegion.tabIndex;\n currentLockRegion.tabIndex = 0;\n attemptFocus(currentLockRegion);\n currentLockRegion.tabIndex = originalRegionTabIndex;\n }\n\n // Keep track of the last item focused.\n lastFocus = document.activeElement;\n\n isLocked = true;\n};\n\n/**\n * Stop trapping the focus.\n */\nexport const untrapFocus = () => {\n // Remove the top region from the stack.\n removeLastLockRegionFromStack();\n\n if (hasTrappedRegionsInStack()) {\n // The focus manager still has items in the stack.\n return;\n }\n\n document.removeEventListener('focus', lockHandler, true);\n\n lastFocus = null;\n ignoreFocusChanges = false;\n isLocked = false;\n};\n"],"file":"focuslock.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/aria/focuslock.js"],"names":["selectors","focusable","lockRegionStack","initialFocusElementStack","finalFocusElementStack","lastFocus","ignoreFocusChanges","isLocked","lockHandler","event","lockRegion","getCurrentLockRegion","parentNode","untrapFocus","contains","target","focusFirstDescendant","document","activeElement","focusLastDescendant","focusableElements","Array","from","querySelectorAll","unshift","some","focusableElement","attemptFocus","reverse","push","isFocusable","focusTarget","tabIndex","getAttribute","disabled","nodeName","href","rel","type","focus","e","length","addLockRegionToStack","newLockRegion","currentLockRegion","element","createElement","style","position","top","left","initialNode","cloneNode","insertBefore","finalNode","nextSibling","removeLastLockRegionFromStack","pop","remove","hasTrappedRegionsInStack","trapFocus","addEventListener","originalRegionTabIndex","removeEventListener"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tab locking system.\n *\n * This is based on code and examples provided in the ARIA specification.\n * https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html\n *\n * @module core/tablock\n * @class tablock\n * @package core\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nconst selectors = {\n focusable: 'input:not([type=\"hidden\"]), a[href], button, textarea, select, [tabindex]',\n};\n\nconst lockRegionStack = [];\nconst initialFocusElementStack = [];\nconst finalFocusElementStack = [];\n\nlet lastFocus = null;\nlet ignoreFocusChanges = false;\nlet isLocked = false;\n\n/**\n * The lock handler.\n *\n * This is the item that does a majority of the work.\n * The overall logic from this comes from the examles in the WCAG guidelines.\n *\n * The general idea is that if the focus is not held within by an Element within the lock region, then we replace focus\n * on the first element in the lock region. If the first element is the element previously selected prior to the\n * user-initiated focus change, then instead jump to the last element in the lock region.\n *\n * This gives us a solution which supports focus locking of any kind, which loops in both directions, and which\n * prevents the lock from escaping the modal entirely.\n *\n * @param {Event} event The event from the focus change\n */\nconst lockHandler = event => {\n if (ignoreFocusChanges) {\n // The focus change was made by an internal call to set focus.\n return;\n }\n\n const lockRegion = getCurrentLockRegion();\n\n if (!lockRegion.parentNode) {\n // The lock region does not exist.\n // Perhaps it was removed without being untrapped.\n untrapFocus();\n }\n\n if (lockRegion.contains(event.target)) {\n lastFocus = event.target;\n } else {\n focusFirstDescendant();\n if (lastFocus == document.activeElement) {\n focusLastDescendant();\n }\n lastFocus = document.activeElement;\n }\n};\n\n/**\n * Focus the first descendant of the current lock region.\n *\n * @returns {Bool} Whether a node was focused\n */\nconst focusFirstDescendant = () => {\n const lockRegion = getCurrentLockRegion();\n\n // Grab all elements in the lock region and attempt to focus each element until one is focused.\n // We can capture most of this in the query selector, but some cases may still reject focus.\n // For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector\n // to capture this.\n // The use of Array.some just ensures that we stop as soon as we have a successful focus.\n const focusableElements = Array.from(lockRegion.querySelectorAll(selectors.focusable));\n\n // The lock region itself may be focusable. This is particularly true on Moodle's older dialogues.\n // We must include it in the calculation of descendants to ensure that looping works correctly.\n focusableElements.unshift(lockRegion);\n return focusableElements.some(focusableElement => attemptFocus(focusableElement));\n};\n\n/**\n * Focus the last descendant of the current lock region.\n *\n * @returns {Bool} Whether a node was focused\n */\nconst focusLastDescendant = () => {\n const lockRegion = getCurrentLockRegion();\n\n // Grab all elements in the lock region, reverse them, and attempt to focus each element until one is focused.\n // We can capture most of this in the query selector, but some cases may still reject focus.\n // For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector\n // to capture this.\n // The use of Array.some just ensures that we stop as soon as we have a successful focus.\n const focusableElements = Array.from(lockRegion.querySelectorAll(selectors.focusable)).reverse();\n\n // The lock region itself may be focusable. This is particularly true on Moodle's older dialogues.\n // We must include it in the calculation of descendants to ensure that looping works correctly.\n focusableElements.push(lockRegion);\n return focusableElements.some(focusableElement => attemptFocus(focusableElement));\n};\n\n/**\n * Check whether the supplied focusTarget is actually focusable.\n * There are cases where a normally focusable element can reject focus.\n *\n * Note: This example is a wholesale copy of the WCAG example.\n *\n * @param {HTMLElement} focusTarget\n * @returns {Bool}\n */\nconst isFocusable = focusTarget => {\n if (focusTarget.tabIndex > 0 || (focusTarget.tabIndex === 0 && focusTarget.getAttribute('tabIndex') !== null)) {\n return true;\n }\n\n if (focusTarget.disabled) {\n return false;\n }\n\n switch (focusTarget.nodeName) {\n case 'A':\n return !!focusTarget.href && focusTarget.rel != 'ignore';\n case 'INPUT':\n return focusTarget.type != 'hidden' && focusTarget.type != 'file';\n case 'BUTTON':\n case 'SELECT':\n case 'TEXTAREA':\n return true;\n default:\n return false;\n }\n};\n\n/**\n * Attempt to focus the supplied focusTarget.\n *\n * Note: This example is a heavily inspired by the WCAG example.\n *\n * @param {HTMLElement} focusTarget\n * @returns {Bool} Whether focus was successful o rnot.\n */\nconst attemptFocus = focusTarget => {\n if (!isFocusable(focusTarget)) {\n return false;\n }\n\n // The ignoreFocusChanges variable prevents the focus event handler from interfering and entering a fight with itself.\n ignoreFocusChanges = true;\n\n try {\n focusTarget.focus();\n } catch (e) {\n // Ignore failures. We will just try to focus the next element in the list.\n // eslint-disable-line\n }\n\n ignoreFocusChanges = false;\n\n // If focus was successful the activeElement will be the one we focused.\n return (document.activeElement === focusTarget);\n};\n\n/**\n * Get the current lock region from the top of the stack.\n *\n * @returns {HTMLElement}\n */\nconst getCurrentLockRegion = () => {\n return lockRegionStack[lockRegionStack.length - 1];\n};\n\n/**\n * Add a new lock region to the stack.\n *\n * @param {HTMLElement} newLockRegion\n */\nconst addLockRegionToStack = newLockRegion => {\n if (newLockRegion === getCurrentLockRegion()) {\n return;\n }\n\n lockRegionStack.push(newLockRegion);\n const currentLockRegion = getCurrentLockRegion();\n\n // Append an empty div which can be focused just outside of the item locked.\n // This locks tab focus to within the tab region, and does not allow it to extend back into the window by\n // guaranteeing the existence of a tabable item after the lock region which can be focused but which will be caught\n // by the handler.\n const element = document.createElement('div');\n element.tabIndex = 0;\n element.style.position = 'fixed';\n element.style.top = 0;\n element.style.left = 0;\n\n const initialNode = element.cloneNode();\n currentLockRegion.parentNode.insertBefore(initialNode, currentLockRegion);\n initialFocusElementStack.push(initialNode);\n\n const finalNode = element.cloneNode();\n currentLockRegion.parentNode.insertBefore(finalNode, currentLockRegion.nextSibling);\n finalFocusElementStack.push(finalNode);\n};\n\n/**\n * Remove the top lock region from the stack.\n */\nconst removeLastLockRegionFromStack = () => {\n // Take the top element off the stack, and replce the current lockRegion value.\n lockRegionStack.pop();\n\n const finalNode = finalFocusElementStack.pop();\n if (finalNode) {\n // The final focus element may have been removed if it was part of a parent item.\n finalNode.remove();\n }\n\n const initialNode = initialFocusElementStack.pop();\n if (initialNode) {\n // The initial focus element may have been removed if it was part of a parent item.\n initialNode.remove();\n }\n};\n\n/**\n * Whether any region is left in the stack.\n *\n * @return {Bool}\n */\nconst hasTrappedRegionsInStack = () => {\n return !!lockRegionStack.length;\n};\n\n/**\n * Start trapping the focus and lock it to the specified newLockRegion.\n *\n * @param {HTMLElement} newLockRegion The container to lock focus to\n */\nexport const trapFocus = newLockRegion => {\n // Update the lock region stack.\n // This allows us to support nesting.\n addLockRegionToStack(newLockRegion);\n\n if (!isLocked) {\n // Add the focus handler.\n document.addEventListener('focus', lockHandler, true);\n }\n\n // Attempt to focus on the first item in the lock region.\n if (!focusFirstDescendant()) {\n const currentLockRegion = getCurrentLockRegion();\n\n // No focusable descendants found in the region yet.\n // This can happen when the region is locked before content is generated.\n // Focus on the region itself for now.\n const originalRegionTabIndex = currentLockRegion.tabIndex;\n currentLockRegion.tabIndex = 0;\n attemptFocus(currentLockRegion);\n currentLockRegion.tabIndex = originalRegionTabIndex;\n }\n\n // Keep track of the last item focused.\n lastFocus = document.activeElement;\n\n isLocked = true;\n};\n\n/**\n * Stop trapping the focus.\n */\nexport const untrapFocus = () => {\n // Remove the top region from the stack.\n removeLastLockRegionFromStack();\n\n if (hasTrappedRegionsInStack()) {\n // The focus manager still has items in the stack.\n return;\n }\n\n document.removeEventListener('focus', lockHandler, true);\n\n lastFocus = null;\n ignoreFocusChanges = false;\n isLocked = false;\n};\n"],"file":"focuslock.min.js"} \ No newline at end of file diff --git a/lib/amd/src/local/aria/focuslock.js b/lib/amd/src/local/aria/focuslock.js index ccae7668d30..c8c11918e5f 100644 --- a/lib/amd/src/local/aria/focuslock.js +++ b/lib/amd/src/local/aria/focuslock.js @@ -91,6 +91,10 @@ const focusFirstDescendant = () => { // to capture this. // The use of Array.some just ensures that we stop as soon as we have a successful focus. const focusableElements = Array.from(lockRegion.querySelectorAll(selectors.focusable)); + + // The lock region itself may be focusable. This is particularly true on Moodle's older dialogues. + // We must include it in the calculation of descendants to ensure that looping works correctly. + focusableElements.unshift(lockRegion); return focusableElements.some(focusableElement => attemptFocus(focusableElement)); }; @@ -108,6 +112,10 @@ const focusLastDescendant = () => { // to capture this. // The use of Array.some just ensures that we stop as soon as we have a successful focus. const focusableElements = Array.from(lockRegion.querySelectorAll(selectors.focusable)).reverse(); + + // The lock region itself may be focusable. This is particularly true on Moodle's older dialogues. + // We must include it in the calculation of descendants to ensure that looping works correctly. + focusableElements.push(lockRegion); return focusableElements.some(focusableElement => attemptFocus(focusableElement)); }; diff --git a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js index ab05d133ade..0ed3f4cbf58 100644 --- a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js +++ b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js @@ -149,6 +149,12 @@ Y.extend(DIALOGUE, Y.Panel, { this.plug(Y.M.core.LockScroll); } + // Remove the `focusoutside` listener. + // It conflicts with the ARIA focuslock manager which supports both YUI and non-YUI dialogues. + this.set('focusOn', Y.Array(this.get('focusOn')).filter(function(node) { + return node.eventName !== 'focusoutside'; + })); + // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507 // and allow setting of z-index in theme. bb = this.get('boundingBox'); @@ -285,7 +291,10 @@ Y.extend(DIALOGUE, Y.Panel, { this._orientationevent.detach(); this._orientationevent = null; } - bb.detach('key', this.keyDelegation); + require(['core/local/aria/focuslock'], function(FocusLockManager) { + // Untrap focus when the dialogue is hidden. + FocusLockManager.untrapFocus(); + }); if (this.get('modal')) { // Hide this dialogue from screen readers. @@ -305,7 +314,10 @@ Y.extend(DIALOGUE, Y.Panel, { Y.one(titlebar).setStyle('cursor', 'move'); } } - this.keyDelegation(); + require(['core/local/aria/focuslock'], function(FocusLockManager) { + // Trap focus to the current bounding box. + FocusLockManager.trapFocus(this.get('boundingBox').getDOMNode()); + }.bind(this)); // Only do accessibility hiding for modals because the ARIA spec // says that all ARIA dialogues should be modal. @@ -450,6 +462,7 @@ Y.extend(DIALOGUE, Y.Panel, { * @method keyDelegation */ keyDelegation: function() { + Y.log('The keyDelegation function has been deprecated in favour of the AMD core/local/aria/focuslock module'); var bb = this.get('boundingBox'); bb.delegate('key', function(e) { var target = e.target; diff --git a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js index 72089fb2fdf..135ea41baa9 100644 --- a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js +++ b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js @@ -1,2 +1,2 @@ -YUI.add("moodle-core-notification-dialogue",function(e,t){var n,r,i,s,o,u,a;n="moodle-dialogue",r="notificationBase",i="yesLabel",s="noLabel",o="title",u="question",a={BASE:"moodle-dialogue-base",WRAP:"moodle-dialogue-wrap",HEADER:"moodle-dialogue-hd",BODY:"moodle-dialogue-bd",CONTENT:"moodle-dialogue-content",FOOTER:"moodle-dialogue-ft",HIDDEN:"hidden",LIGHTBOX:"moodle-dialogue-lightbox"},M.core=M.core||{};var f="Moodle dialogue",l,c=n+"-fullscreen",h=n+"-hidden",p=" [role=dialog]",d="[role=menubar]",v=".",m="moodle-has-zindex",g='input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',y="form";l=function(t){var n="moodle-dialogue-"+e.stamp(this);t.notificationBase=e.Node.create('
').append(e.Node.create('').append(e.Node.create('
')).append(e.Node.create('
')).append(e.Node.create('
'))),e.one(document.body).append(t.notificationBase),t.srcNode="#"+n,delete t.buttons,l.superclass.constructor.apply(this,[t])},e.extend(l,e.Panel,{_resizeevent:null,_orientationevent:null,_calculatedzindex:!1,_currentMaskNodeId:null,_originalPosition:null,_hiddenSiblings:null,hideIfNotForm:function(){var e=this.get("boundingBox"),t=e.one(y);t===null&&this.hide()},initializer:function(){var t;this.get("closeButton")!==!1&&this.get("buttons").header[0].setAttribute("title",this.get("closeButtonTitle")),this._hiddenSiblings=[],this.get("render")&&this.render(),this.after("visibleChange",this.visibilityChanged,this),this.get("center")&&this.centerDialogue(),this.get("modal")&&(this.get(r).set("aria-hidden","true"),this.plug(e.M.core.LockScroll)),t=this.get("boundingBox"),t.addClass(m),e.Array.each(this.get("extraClasses"),t.addClass,t),this.get("visible")&&this.applyZIndex(),this.on("maskShow",this.applyZIndex),this.on("maskShow",function(){var t=e.one(e.config.win),n=this.get("boundingBox");this.get("center")||(this._originalPosition=n.getXY());var r=this.get("maskNode");this._currentMaskNodeId!==r.get("_yuid")&&(this._currentMaskNodeId=r.get("_yuid"),r.on("click",this.hideIfNotForm,this)),n.getStyle("position")!=="fixed"&&n.setStyles({top:t.get("scrollTop"),left:t.get("scrollLeft")})},this);var n=this.get("notificationBase"),i=this.get("additionalBaseClass");i!==""&&n.addClass(i),this.after("destroyedChange",function(){this.get(r).remove(!0)},this)},applyZIndex:function(){var t=1040,n=1,r=this.get("boundingBox"),i=this.get("maskNode"),s=this.get("zIndex");s!==0&&!this._calculatedzindex?r.setStyle("zIndex",s):(e.all(p+", "+d+", "+v+m).each(function(e){var n=this.findZIndex(e);n>t&&(t=n)},this),n=(t+1).toString(),r.setStyle("zIndex",n),this.set("zIndex",n),this.get("modal")&&(i.setStyle("zIndex",n),e.UA.ie&&e.UA.compareVersions(e.UA.ie,9)<0&&setTimeout(function(){i.setStyle("position","static"),setTimeout(function(){i.setStyle("position","fixed")},0)},0)),this._calculatedzindex=!0)},findZIndex:function(e){var t=e.getStyle("zIndex")||e.ancestor().getStyle("zIndex");return t?parseInt(t,10):0},visibilityChanged:function(t){var n,r;t.attrName==="visible"&&(this.get("maskNode").addClass(a.LIGHTBOX),t.prevVal&&!t.newVal&&(r=this.get("boundingBox"),this._resizeevent&&(this._resizeevent.detach(),this._resizeevent=null),this._orientationevent&&(this._orientationevent.detach(),this._orientationevent=null),r.detach("key",this.keyDelegation),this.get("modal")&&this.setAccessibilityHidden()),!t.prevVal&&t.newVal&&(this.applyZIndex(),this.makeResponsive(),this.shouldResizeFullscreen()||this.get("draggable")&&(n="#"+this.get("id")+" ."+a.HEADER,this.plug(e.Plugin.Drag,{handles:[n]}),e.one(n).setStyle("cursor","move")),this.keyDelegation(),this.get("modal")&&this.setAccessibilityVisible()),this.get("center")&&!t.prevVal&&t.newVal&&this.centerDialogue())},makeResponsive:function(){var e=this.get("boundingBox");this.shouldResizeFullscreen()?(e.addClass(c),e.setStyles({left:null,top:null,width:null,height:null,right:null,bottom:null})):this.get("responsive")&&e.removeClass(c).setStyles({width:this.get("width"),height:this.get("height")}),this.lockScroll&&this.lockScroll.updateScrollLock(this.shouldResizeFullscreen())},centerDialogue:function(){var t=this.get("boundingBox"),n=t.hasClass(h),r,i;if(this.shouldResizeFullscreen())return;n&&t.setStyle("top","-1000px").removeClass(h),r=Math.max(Math.round((t.get("winWidth")-t.get("offsetWidth"))/2),15),i=Math.max(Math.round((t.get("winHeight")-t.get("offsetHeight"))/2),15)+e.one(window).get("scrollTop"),t.setStyles({left:r,top:i}),n&&t.addClass(h),this.makeResponsive()},shouldResizeFullscreen:function(){return window===window.parent&&this.get("responsive")&&Math.floor(e.one(document.body).get("winWidth"))').append(e.Node.create('').append(e.Node.create('
')).append(e.Node.create('
')).append(e.Node.create('
'))),e.one(document.body).append(t.notificationBase),t.srcNode="#"+n,delete t.buttons,l.superclass.constructor.apply(this,[t])},e.extend(l,e.Panel,{_resizeevent:null,_orientationevent:null,_calculatedzindex:!1,_currentMaskNodeId:null,_originalPosition:null,_hiddenSiblings:null,hideIfNotForm:function(){var e=this.get("boundingBox"),t=e.one(y);t===null&&this.hide()},initializer:function(){var t;this.get("closeButton")!==!1&&this.get("buttons").header[0].setAttribute("title",this.get("closeButtonTitle")),this._hiddenSiblings=[],this.get("render")&&this.render(),this.after("visibleChange",this.visibilityChanged,this),this.get("center")&&this.centerDialogue(),this.get("modal")&&(this.get(r).set("aria-hidden","true"),this.plug(e.M.core.LockScroll)),this.set("focusOn",e.Array(this.get("focusOn")).filter(function(e){return e.eventName!=="focusoutside"})),t=this.get("boundingBox"),t.addClass(m),e.Array.each(this.get("extraClasses"),t.addClass,t),this.get("visible")&&this.applyZIndex(),this.on("maskShow",this.applyZIndex),this.on("maskShow",function(){var t=e.one(e.config.win),n=this.get("boundingBox");this.get("center")||(this._originalPosition=n.getXY());var r=this.get("maskNode");this._currentMaskNodeId!==r.get("_yuid")&&(this._currentMaskNodeId=r.get("_yuid"),r.on("click",this.hideIfNotForm,this)),n.getStyle("position")!=="fixed"&&n.setStyles({top:t.get("scrollTop"),left:t.get("scrollLeft")})},this);var n=this.get("notificationBase"),i=this.get("additionalBaseClass");i!==""&&n.addClass(i),this.after("destroyedChange",function(){this.get(r).remove(!0)},this)},applyZIndex:function(){var t=1040,n=1,r=this.get("boundingBox"),i=this.get("maskNode"),s=this.get("zIndex");s!==0&&!this._calculatedzindex?r.setStyle("zIndex",s):(e.all(p+", "+d+", "+v+m).each(function(e){var n=this.findZIndex(e);n>t&&(t=n)},this),n=(t+1).toString(),r.setStyle("zIndex",n),this.set("zIndex",n),this.get("modal")&&(i.setStyle("zIndex",n),e.UA.ie&&e.UA.compareVersions(e.UA.ie,9)<0&&setTimeout(function(){i.setStyle("position","static"),setTimeout(function(){i.setStyle("position","fixed")},0)},0)),this._calculatedzindex=!0)},findZIndex:function(e){var t=e.getStyle("zIndex")||e.ancestor().getStyle("zIndex");return t?parseInt(t,10):0},visibilityChanged:function(t){var n,r;t.attrName==="visible"&&(this.get("maskNode").addClass(a.LIGHTBOX),t.prevVal&&!t.newVal&&(r=this.get("boundingBox"),this._resizeevent&&(this._resizeevent.detach(),this._resizeevent=null),this._orientationevent&&(this._orientationevent.detach(),this._orientationevent=null),require(["core/local/aria/focuslock"],function(e){e.untrapFocus()}),this.get("modal")&&this.setAccessibilityHidden()),!t.prevVal&&t.newVal&&(this.applyZIndex(),this.makeResponsive(),this.shouldResizeFullscreen()||this.get("draggable")&&(n="#"+this.get("id")+" ."+a.HEADER,this.plug(e.Plugin.Drag,{handles:[n]}),e.one(n).setStyle("cursor","move")),require(["core/local/aria/focuslock"],function(e){e.trapFocus(this.get("boundingBox").getDOMNode())}.bind(this)),this.get("modal")&&this.setAccessibilityVisible()),this.get("center")&&!t.prevVal&&t.newVal&&this.centerDialogue())},makeResponsive:function(){var e=this.get("boundingBox");this.shouldResizeFullscreen()?(e.addClass(c),e.setStyles({left:null,top:null,width:null,height:null,right:null,bottom:null})):this.get("responsive")&&e.removeClass(c).setStyles({width:this.get("width"),height:this.get("height")}),this.lockScroll&&this.lockScroll.updateScrollLock(this.shouldResizeFullscreen())},centerDialogue:function(){var t=this.get("boundingBox"),n=t.hasClass(h),r,i;if(this.shouldResizeFullscreen())return;n&&t.setStyle("top","-1000px").removeClass(h),r=Math.max(Math.round((t.get("winWidth")-t.get("offsetWidth"))/2),15),i=Math.max(Math.round((t.get("winHeight")-t.get("offsetHeight"))/2),15)+e.one(window).get("scrollTop"),t.setStyles({left:r,top:i}),n&&t.addClass(h),this.makeResponsive()},shouldResizeFullscreen:function(){return window===window.parent&&this.get("responsive")&&Math.floor(e.one(document.body).get("winWidth"))