From cb6acfc46aac50153f935bc308fbc8c041d36160 Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Mon, 27 Nov 2023 09:36:07 +1100 Subject: [PATCH 1/2] MDL-79769 login: Add password toggle icon to the login screen --- admin/settings/plugins.php | 12 ++ auth/classes/output/login.php | 11 ++ auth/tests/behat/loginform.feature | 18 +++ lang/en/admin.php | 1 + lang/en/auth.php | 2 + lang/en/form.php | 1 + lib/amd/build/togglesensitive.min.js | 10 ++ lib/amd/build/togglesensitive.min.js.map | 1 + lib/amd/src/togglesensitive.js | 150 ++++++++++++++++++ lib/moodlelib.php | 15 ++ .../form_input_toggle_sensitive.mustache | 45 ++++++ lib/templates/loginform.mustache | 9 +- theme/boost/scss/moodle/forms.scss | 21 +++ theme/boost/style/moodle.css | 15 ++ theme/classic/style/moodle.css | 15 ++ 15 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 lib/amd/build/togglesensitive.min.js create mode 100644 lib/amd/build/togglesensitive.min.js.map create mode 100644 lib/amd/src/togglesensitive.js create mode 100644 lib/templates/form_input_toggle_sensitive.mustache diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index 33ddda70751..43909fba346 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -135,6 +135,18 @@ if ($hassiteconfig) { $temp->add($setting); $ADMIN->add('authsettings', $temp); + // Toggle password visiblity icon. + $temp->add(new admin_setting_configselect('loginpasswordtoggle', + new lang_string('auth_loginpasswordtoggle', 'auth'), + new lang_string('auth_loginpasswordtoggle_desc', 'auth'), + TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY, + [ + TOGGLE_SENSITIVE_DISABLED => get_string('disabled', 'admin'), + TOGGLE_SENSITIVE_ENABLED => get_string('enabled', 'admin'), + TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY => get_string('smallscreensonly', 'admin'), + ], + )); + $temp = new admin_externalpage('authtestsettings', get_string('testsettings', 'core_auth'), new moodle_url("/auth/test_settings.php"), 'moodle/site:config', true); $ADMIN->add('authsettings', $temp); diff --git a/auth/classes/output/login.php b/auth/classes/output/login.php index 3800cb72ecc..6854234c0d4 100644 --- a/auth/classes/output/login.php +++ b/auth/classes/output/login.php @@ -75,6 +75,10 @@ class login implements renderable, templatable { public $maintenance; /** @var string ReCaptcha element HTML. */ public $recaptcha; + /** @var bool Toggle the password visibility icon. */ + public $togglepassword; + /** @var bool Toggle the password visibility icon for small screens only. */ + public $smallscreensonly; /** * Constructor. @@ -130,6 +134,11 @@ class login implements renderable, templatable { require_once($CFG->libdir . '/recaptchalib_v2.php'); $this->recaptcha = recaptcha_get_challenge_html(RECAPTCHA_API_URL, $CFG->recaptchapublickey); } + + // Toggle password visibility icon. + $this->togglepassword = get_config('core', 'loginpasswordtoggle') == TOGGLE_SENSITIVE_ENABLED || + get_config('core', 'loginpasswordtoggle') == TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY; + $this->smallscreensonly = get_config('core', 'loginpasswordtoggle') == TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY; } /** @@ -175,6 +184,8 @@ class login implements renderable, templatable { $data->maintenance = format_text($this->maintenance, FORMAT_MOODLE); $data->languagemenu = $this->languagemenu; $data->recaptcha = $this->recaptcha; + $data->togglepassword = $this->togglepassword; + $data->smallscreensonly = $this->smallscreensonly; return $data; } diff --git a/auth/tests/behat/loginform.feature b/auth/tests/behat/loginform.feature index a87301d421a..cc4c01338e7 100644 --- a/auth/tests/behat/loginform.feature +++ b/auth/tests/behat/loginform.feature @@ -101,3 +101,21 @@ Feature: Test if the login form provides the correct feedback And I press "Log in" And I press the tab key Then the focused element is "Username" "field" + + Scenario: Display the password visibility toggle icon + Given the following config values are set as admin: + | loginpasswordtoggle | 1 | + When I follow "Log in" + Then "Toggle sensitive" "button" should be visible + And the following config values are set as admin: + | loginpasswordtoggle | 0 | + And I reload the page + And "Toggle sensitive" "button" should not be visible + + Scenario: Display the password visibility toggle icon for small screens only + Given the following config values are set as admin: + | loginpasswordtoggle | 2 | + When I follow "Log in" + Then "Toggle sensitive" "button" should not be visible + And I change the viewport size to "mobile" + And "Toggle sensitive" "button" should be visible diff --git a/lang/en/admin.php b/lang/en/admin.php index 71dbb942050..dc10097c83d 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -1312,6 +1312,7 @@ $string['sixtyfourbitsrequired'] = 'It has been detected that your site is not u $string['sixtyfourbitswarning'] = 'It has been detected that your site is not using a 64-bit PHP version. It is recommended that you upgrade your site to ensure future compatibility.'; $string['slasharguments'] = 'Use slash arguments'; $string['slashargumentswarning'] = 'It is recommended that the use of slash arguments is enabled. In future it will be required. For more details, see the documentation Using slash arguments.'; +$string['smallscreensonly'] = 'Small screens only'; $string['smtp'] = 'SMTP'; $string['smtpauthtype'] = 'SMTP Auth Type'; $string['smtpdetail'] = 'Simple Mail Transfer Protocol (SMTP) settings for sending email.'; diff --git a/lang/en/auth.php b/lang/en/auth.php index c73816c063c..2a42ae444a1 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -41,6 +41,8 @@ $string['auth_changepasswordurl_expl'] = 'Specify the url to send users who have $string['auth_changingemailaddress'] = 'You have requested a change of email address, from {$a->oldemail} to {$a->newemail}. For security reasons, we are sending you an email message at the new address to confirm that it belongs to you. Your email address will be updated as soon as you open the URL sent to you in that message.'; $string['authinstructions'] = 'Leave this blank for the default login instructions to be displayed on the login page. If you want to provide custom login instructions, enter them here.'; $string['auth_invalidnewemailkey'] = 'Error: if you are trying to confirm a change of email address, you may have made a mistake in copying the URL we sent you by email. Please copy the address and try again.'; +$string['auth_loginpasswordtoggle'] = 'Password visibility toggle'; +$string['auth_loginpasswordtoggle_desc'] = 'Add an icon to the password field on the login screen that allows users to show or hide their entered password.'; $string['auth_loginrecaptcha'] = 'Enable reCAPTCHA for login'; $string['auth_loginrecaptcha_desc'] = 'Add a visual/audio confirmation form element to the login page. This reduces the risk of unwarranted login attempts. See Google reCAPTCHA for more details. '; $string['auth_multiplehosts'] = 'Multiple hosts OR addresses can be specified (eg host1.com;host2.com;host3.com) or (eg xxx.xxx.xxx.xxx;xxx.xxx.xxx.xxx)'; diff --git a/lang/en/form.php b/lang/en/form.php index 440997cb63e..0ddade386d0 100644 --- a/lang/en/form.php +++ b/lang/en/form.php @@ -87,5 +87,6 @@ $string['suggestions'] = 'Suggestions'; $string['time'] = 'Time'; $string['timeunit'] = 'Time unit'; $string['timing'] = 'Timing'; +$string['togglesensitive'] = 'Toggle sensitive'; $string['unmaskpassword'] = 'Unmask'; $string['year'] = 'Year'; diff --git a/lib/amd/build/togglesensitive.min.js b/lib/amd/build/togglesensitive.min.js new file mode 100644 index 00000000000..60aaba458c1 --- /dev/null +++ b/lib/amd/build/togglesensitive.min.js @@ -0,0 +1,10 @@ +define("core/togglesensitive",["exports","core/pagehelpers","core/templates","core/pending","core/prefetch","core/notification"],(function(_exports,_pagehelpers,_templates,_pending,_prefetch,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * JS module for toggling the sensitive input visibility (e.g. passwords, keys). + * + * @module core/togglesensitive + * @copyright 2023 David Woloszyn + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending),_prefetch=_interopRequireDefault(_prefetch),_notification=_interopRequireDefault(_notification);const SELECTORS_BUTTON=".toggle-sensitive-btn",PIX_EYE="t/hide",PIX_EYE_SLASH="t/show";let sensitiveElementId,smallScreensOnly;_exports.init=function(elementId){let isSmallScreensOnly=arguments.length>1&&void 0!==arguments[1]&&arguments[1];const sensitiveInput=document.getElementById(elementId);null!==sensitiveInput&&(sensitiveElementId=elementId,smallScreensOnly=isSmallScreensOnly,_prefetch.default.prefetchTemplate("core/form_input_toggle_sensitive"),renderSensitiveToggle(sensitiveInput).then((()=>{})).catch(_notification.default.exception),registerListenerEvents())};const renderSensitiveToggle=async sensitiveInput=>{const{html:html}=await _templates.default.renderForPromise("core/form_input_toggle_sensitive",{smallscreensonly:smallScreensOnly,sensitiveinput:sensitiveInput.outerHTML});sensitiveInput.outerHTML=html},registerListenerEvents=()=>{document.addEventListener("click",handleButtonInteraction),smallScreensOnly&&window.addEventListener("resize",handleScreenResizing)},handleButtonInteraction=event=>{const toggleButton=event.target.closest(SELECTORS_BUTTON);if(toggleButton){const sensitiveInput=document.getElementById(sensitiveElementId);toggleSensitiveVisibility(sensitiveInput,toggleButton)}},handleScreenResizing=()=>{if(!(0,_pagehelpers.isExtraSmall)()){const sensitiveInput=document.getElementById(sensitiveElementId),toggleButton=sensitiveInput.parentNode.querySelector(SELECTORS_BUTTON);toggleSensitiveVisibility(sensitiveInput,toggleButton,!0)}},toggleSensitiveVisibility=function(sensitiveInput,toggleButton){let force=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const pendingPromise=new _pending.default("core/togglesensitive:toggle");let type,icon;!0===force?(type="password",icon=PIX_EYE):(type="password"===sensitiveInput.getAttribute("type")?"text":"password",icon="password"===sensitiveInput.getAttribute("type")?PIX_EYE_SLASH:PIX_EYE),sensitiveInput.setAttribute("type",type),_templates.default.renderPix(icon,"core").then((icon=>{toggleButton.innerHTML=icon,pendingPromise.resolve()})).catch(_notification.default.exception)}})); + +//# sourceMappingURL=togglesensitive.min.js.map \ No newline at end of file diff --git a/lib/amd/build/togglesensitive.min.js.map b/lib/amd/build/togglesensitive.min.js.map new file mode 100644 index 00000000000..874832ab0ed --- /dev/null +++ b/lib/amd/build/togglesensitive.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"togglesensitive.min.js","sources":["../src/togglesensitive.js"],"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 * JS module for toggling the sensitive input visibility (e.g. passwords, keys).\n *\n * @module core/togglesensitive\n * @copyright 2023 David Woloszyn \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {isExtraSmall} from 'core/pagehelpers';\nimport Templates from 'core/templates';\nimport Pending from 'core/pending';\nimport Prefetch from 'core/prefetch';\nimport Notification from 'core/notification';\n\nconst SELECTORS = {\n BUTTON: '.toggle-sensitive-btn',\n ICON: '.toggle-sensitive-btn .icon',\n};\n\nconst PIX = {\n EYE: 't/hide',\n EYE_SLASH: 't/show',\n};\n\nlet sensitiveElementId;\nlet smallScreensOnly;\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n * @param {String} elementId Form button element.\n * @param {boolean} isSmallScreensOnly Is this for small screens only?\n */\nexport const init = (elementId, isSmallScreensOnly = false) => {\n const sensitiveInput = document.getElementById(elementId);\n if (sensitiveInput === null) {\n // Exit early if invalid element id passed.\n return;\n }\n sensitiveElementId = elementId;\n smallScreensOnly = isSmallScreensOnly;\n Prefetch.prefetchTemplate('core/form_input_toggle_sensitive');\n // Render the sensitive input with a toggle button.\n renderSensitiveToggle(sensitiveInput).then(() => {\n return;\n }).catch(Notification.exception);\n // Register event listeners.\n registerListenerEvents();\n};\n\n/**\n * Render the new input html with toggle button and update the incoming html.\n *\n * @method renderSensitiveToggle\n * @param {HTMLElement} sensitiveInput HTML element for the sensitive input.\n */\nconst renderSensitiveToggle = async(sensitiveInput) => {\n const {html} = await Templates.renderForPromise(\n 'core/form_input_toggle_sensitive',\n {\n smallscreensonly: smallScreensOnly,\n sensitiveinput: sensitiveInput.outerHTML,\n }\n );\n sensitiveInput.outerHTML = html;\n};\n\n/**\n * Register event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n // Toggle the sensitive input visibility when interacting with the toggle button.\n document.addEventListener('click', handleButtonInteraction);\n // For small screens only, hide all sensitive inputs when the screen is enlarged.\n if (smallScreensOnly) {\n window.addEventListener('resize', handleScreenResizing);\n }\n};\n\n/**\n * Handle events trigger by interacting with the toggle button.\n *\n * @method handleButtonInteraction\n * @param {Event} event The button event.\n */\nconst handleButtonInteraction = (event) => {\n const toggleButton = event.target.closest(SELECTORS.BUTTON);\n if (toggleButton) {\n const sensitiveInput = document.getElementById(sensitiveElementId);\n toggleSensitiveVisibility(sensitiveInput, toggleButton);\n }\n};\n\n/**\n * Handle events trigger by resizing the screen.\n *\n * @method handleScreenResizing\n */\nconst handleScreenResizing = () => {\n if (!isExtraSmall()) {\n const sensitiveInput = document.getElementById(sensitiveElementId);\n const toggleButton = sensitiveInput.parentNode.querySelector(SELECTORS.BUTTON);\n toggleSensitiveVisibility(sensitiveInput, toggleButton, true);\n }\n};\n\n/**\n * Toggle the sensitive input visibility and its associated icon.\n *\n * @method toggleSensitiveVisibility\n * @param {HTMLInputElement} sensitiveInput The sensitive input element.\n * @param {HTMLElement} toggleButton The toggle button.\n * @param {boolean} force Force the input back to password type.\n */\nconst toggleSensitiveVisibility = (sensitiveInput, toggleButton, force = false) => {\n const pendingPromise = new Pending('core/togglesensitive:toggle');\n let type;\n let icon;\n if (force === true) {\n type = 'password';\n icon = PIX.EYE;\n } else {\n type = sensitiveInput.getAttribute('type') === 'password' ? 'text' : 'password';\n icon = sensitiveInput.getAttribute('type') === 'password' ? PIX.EYE_SLASH : PIX.EYE;\n }\n sensitiveInput.setAttribute('type', type);\n Templates.renderPix(icon, 'core').then((icon) => {\n toggleButton.innerHTML = icon;\n pendingPromise.resolve();\n return;\n }).catch(Notification.exception);\n};\n"],"names":["SELECTORS","PIX","sensitiveElementId","smallScreensOnly","elementId","isSmallScreensOnly","sensitiveInput","document","getElementById","prefetchTemplate","renderSensitiveToggle","then","catch","Notification","exception","registerListenerEvents","async","html","Templates","renderForPromise","smallscreensonly","sensitiveinput","outerHTML","addEventListener","handleButtonInteraction","window","handleScreenResizing","event","toggleButton","target","closest","toggleSensitiveVisibility","parentNode","querySelector","force","pendingPromise","Pending","type","icon","getAttribute","setAttribute","renderPix","innerHTML","resolve"],"mappings":";;;;;;;gRA6BMA,iBACM,wBAINC,QACG,SADHA,cAES,aAGXC,mBACAC,+BASgB,SAACC,eAAWC,iFACtBC,eAAiBC,SAASC,eAAeJ,WACxB,OAAnBE,iBAIJJ,mBAAqBE,UACrBD,iBAAmBE,qCACVI,iBAAiB,oCAE1BC,sBAAsBJ,gBAAgBK,MAAK,SAExCC,MAAMC,sBAAaC,WAEtBC,iCASEL,sBAAwBM,MAAAA,uBACpBC,KAACA,YAAcC,mBAAUC,iBAC3B,mCACA,CACIC,iBAAkBjB,iBAClBkB,eAAgBf,eAAegB,YAGvChB,eAAegB,UAAYL,MAQzBF,uBAAyB,KAE3BR,SAASgB,iBAAiB,QAASC,yBAE/BrB,kBACAsB,OAAOF,iBAAiB,SAAUG,uBAUpCF,wBAA2BG,cACvBC,aAAeD,MAAME,OAAOC,QAAQ9B,qBACtC4B,aAAc,OACRtB,eAAiBC,SAASC,eAAeN,oBAC/C6B,0BAA0BzB,eAAgBsB,gBAS5CF,qBAAuB,UACpB,+BAAgB,OACXpB,eAAiBC,SAASC,eAAeN,oBACzC0B,aAAetB,eAAe0B,WAAWC,cAAcjC,kBAC7D+B,0BAA0BzB,eAAgBsB,cAAc,KAY1DG,0BAA4B,SAACzB,eAAgBsB,kBAAcM,oEACvDC,eAAiB,IAAIC,iBAAQ,mCAC/BC,KACAC,MACU,IAAVJ,OACAG,KAAO,WACPC,KAAOrC,UAEPoC,KAA+C,aAAxC/B,eAAeiC,aAAa,QAAyB,OAAS,WACrED,KAA+C,aAAxChC,eAAeiC,aAAa,QAAyBtC,cAAgBA,SAEhFK,eAAekC,aAAa,OAAQH,yBAC1BI,UAAUH,KAAM,QAAQ3B,MAAM2B,OACpCV,aAAac,UAAYJ,KACzBH,eAAeQ,aAEhB/B,MAAMC,sBAAaC"} \ No newline at end of file diff --git a/lib/amd/src/togglesensitive.js b/lib/amd/src/togglesensitive.js new file mode 100644 index 00000000000..1040b248e41 --- /dev/null +++ b/lib/amd/src/togglesensitive.js @@ -0,0 +1,150 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * JS module for toggling the sensitive input visibility (e.g. passwords, keys). + * + * @module core/togglesensitive + * @copyright 2023 David Woloszyn + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {isExtraSmall} from 'core/pagehelpers'; +import Templates from 'core/templates'; +import Pending from 'core/pending'; +import Prefetch from 'core/prefetch'; +import Notification from 'core/notification'; + +const SELECTORS = { + BUTTON: '.toggle-sensitive-btn', + ICON: '.toggle-sensitive-btn .icon', +}; + +const PIX = { + EYE: 't/hide', + EYE_SLASH: 't/show', +}; + +let sensitiveElementId; +let smallScreensOnly; + +/** + * Entrypoint of the js. + * + * @method init + * @param {String} elementId Form button element. + * @param {boolean} isSmallScreensOnly Is this for small screens only? + */ +export const init = (elementId, isSmallScreensOnly = false) => { + const sensitiveInput = document.getElementById(elementId); + if (sensitiveInput === null) { + // Exit early if invalid element id passed. + return; + } + sensitiveElementId = elementId; + smallScreensOnly = isSmallScreensOnly; + Prefetch.prefetchTemplate('core/form_input_toggle_sensitive'); + // Render the sensitive input with a toggle button. + renderSensitiveToggle(sensitiveInput).then(() => { + return; + }).catch(Notification.exception); + // Register event listeners. + registerListenerEvents(); +}; + +/** + * Render the new input html with toggle button and update the incoming html. + * + * @method renderSensitiveToggle + * @param {HTMLElement} sensitiveInput HTML element for the sensitive input. + */ +const renderSensitiveToggle = async(sensitiveInput) => { + const {html} = await Templates.renderForPromise( + 'core/form_input_toggle_sensitive', + { + smallscreensonly: smallScreensOnly, + sensitiveinput: sensitiveInput.outerHTML, + } + ); + sensitiveInput.outerHTML = html; +}; + +/** + * Register event listeners. + * + * @method registerListenerEvents + */ +const registerListenerEvents = () => { + // Toggle the sensitive input visibility when interacting with the toggle button. + document.addEventListener('click', handleButtonInteraction); + // For small screens only, hide all sensitive inputs when the screen is enlarged. + if (smallScreensOnly) { + window.addEventListener('resize', handleScreenResizing); + } +}; + +/** + * Handle events trigger by interacting with the toggle button. + * + * @method handleButtonInteraction + * @param {Event} event The button event. + */ +const handleButtonInteraction = (event) => { + const toggleButton = event.target.closest(SELECTORS.BUTTON); + if (toggleButton) { + const sensitiveInput = document.getElementById(sensitiveElementId); + toggleSensitiveVisibility(sensitiveInput, toggleButton); + } +}; + +/** + * Handle events trigger by resizing the screen. + * + * @method handleScreenResizing + */ +const handleScreenResizing = () => { + if (!isExtraSmall()) { + const sensitiveInput = document.getElementById(sensitiveElementId); + const toggleButton = sensitiveInput.parentNode.querySelector(SELECTORS.BUTTON); + toggleSensitiveVisibility(sensitiveInput, toggleButton, true); + } +}; + +/** + * Toggle the sensitive input visibility and its associated icon. + * + * @method toggleSensitiveVisibility + * @param {HTMLInputElement} sensitiveInput The sensitive input element. + * @param {HTMLElement} toggleButton The toggle button. + * @param {boolean} force Force the input back to password type. + */ +const toggleSensitiveVisibility = (sensitiveInput, toggleButton, force = false) => { + const pendingPromise = new Pending('core/togglesensitive:toggle'); + let type; + let icon; + if (force === true) { + type = 'password'; + icon = PIX.EYE; + } else { + type = sensitiveInput.getAttribute('type') === 'password' ? 'text' : 'password'; + icon = sensitiveInput.getAttribute('type') === 'password' ? PIX.EYE_SLASH : PIX.EYE; + } + sensitiveInput.setAttribute('type', type); + Templates.renderPix(icon, 'core').then((icon) => { + toggleButton.innerHTML = icon; + pendingPromise.resolve(); + return; + }).catch(Notification.exception); +}; diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 1db650c1cf2..d5a9808cf6a 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -601,6 +601,21 @@ define('CONTACT_SUPPORT_ANYONE', 2); */ define('MAX_PASSWORD_CHARACTERS', 128); +/** + * Toggle sensitive feature is disabled. Used for sensitive inputs (passwords, tokens, keys). + */ +define('TOGGLE_SENSITIVE_DISABLED', 0); + +/** + * Toggle sensitive feature is enabled. Used for sensitive inputs (passwords, tokens, keys). + */ +define('TOGGLE_SENSITIVE_ENABLED', 1); + +/** + * Toggle sensitive feature is enabled for small screens only. Used for sensitive inputs (passwords, tokens, keys). + */ +define('TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY', 2); + // PARAMETER HANDLING. /** diff --git a/lib/templates/form_input_toggle_sensitive.mustache b/lib/templates/form_input_toggle_sensitive.mustache new file mode 100644 index 00000000000..ba47b469c0c --- /dev/null +++ b/lib/templates/form_input_toggle_sensitive.mustache @@ -0,0 +1,45 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/form_input_toggle_sensitive + + Moodle template for toggling senstive inputs. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * none + + Example context (json): + { + "smallscreensonly": true, + "sensitiveinput": "" + } +}} +
+ {{{sensitiveinput}}} +
+ +
+
diff --git a/lib/templates/loginform.mustache b/lib/templates/loginform.mustache index 05060f3ff6e..6dc17fa437c 100644 --- a/lib/templates/loginform.mustache +++ b/lib/templates/loginform.mustache @@ -91,7 +91,9 @@ "sitename": "Beer & Chips", "logintoken": "randomstring", "maintenance": "For full access to this site, you need to login in as an admin.", - "languagemenu": "Choose language" + "languagemenu": "Choose language", + "togglepassword": true, + "smallscreensonly": true } }} @@ -214,6 +216,11 @@ {{#js}} {{^error}} + {{#togglepassword}} + require(['core/togglesensitive'], function(ToggleSensitive) { + ToggleSensitive.init("password", {{smallscreensonly}}); + }); + {{/togglepassword}} {{#autofocusform}} var userNameField = document.getElementById('username'); if (userNameField.value.length == 0) { diff --git a/theme/boost/scss/moodle/forms.scss b/theme/boost/scss/moodle/forms.scss index 57a91eb4c82..8e856746f4d 100644 --- a/theme/boost/scss/moodle/forms.scss +++ b/theme/boost/scss/moodle/forms.scss @@ -480,3 +480,24 @@ textarea[data-auto-rows] { padding-left: 0; } } + +// Remove the margins that come with the icon. +.toggle-sensitive-btn .icon { + margin-right: 0; +} + +// Input group elements display border radius depending on the proximity of other elements. +// This corrects border radius of the input when 'small screen only' mode hides the toggle button. +@media (min-width: 576px) { + .toggle-sensitive-wrapper.small-screens-only.input-group:not(.has-validation) > { + .form-control:not(:last-child) { + @include border-radius($input-border-radius); + } + .form-control-lg:not(:last-child) { + @include border-radius($input-border-radius-lg); + } + .input-group-append .toggle-sensitive-btn { + display: none; + } + } +} diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 8ba5690c500..9d7a523074f 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -33262,6 +33262,21 @@ textarea[data-auto-rows] { padding-left: 0; } +.toggle-sensitive-btn .icon { + margin-right: 0; +} + +@media (min-width: 576px) { + .toggle-sensitive-wrapper.small-screens-only.input-group:not(.has-validation) > .form-control:not(:last-child) { + border-radius: 0.5rem; + } + .toggle-sensitive-wrapper.small-screens-only.input-group:not(.has-validation) > .form-control-lg:not(:last-child) { + border-radius: 0.6rem; + } + .toggle-sensitive-wrapper.small-screens-only.input-group:not(.has-validation) > .input-group-append .toggle-sensitive-btn { + display: none; + } +} .pagelayout-login #region-main { border: 0; background-color: inherit; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 5d6a658ebde..d296862a14a 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -33262,6 +33262,21 @@ textarea[data-auto-rows] { padding-left: 0; } +.toggle-sensitive-btn .icon { + margin-right: 0; +} + +@media (min-width: 576px) { + .toggle-sensitive-wrapper.small-screens-only.input-group:not(.has-validation) > .form-control:not(:last-child) { + border-radius: 0.25rem; + } + .toggle-sensitive-wrapper.small-screens-only.input-group:not(.has-validation) > .form-control-lg:not(:last-child) { + border-radius: 0.3rem; + } + .toggle-sensitive-wrapper.small-screens-only.input-group:not(.has-validation) > .input-group-append .toggle-sensitive-btn { + display: none; + } +} .pagelayout-login #region-main { border: 0; background-color: inherit; From cd935eb3e2322e21d793b39fcd0f2e2e560b1a9f Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Fri, 8 Mar 2024 12:26:19 +1100 Subject: [PATCH 2/2] MDL-79769 login: Fix focus loss of loginpageautofocus setting Added new form event to fix login fields focus loss when DOM is manipulated by togglesensitive. --- lib/amd/build/togglesensitive.min.js | 4 ++-- lib/amd/build/togglesensitive.min.js.map | 2 +- lib/amd/src/togglesensitive.js | 3 +++ lib/form/amd/build/events.min.js | 2 +- lib/form/amd/build/events.min.js.map | 2 +- lib/form/amd/src/events.js | 28 ++++++++++++++++++++++++ lib/templates/loginform.mustache | 18 ++++++++++----- 7 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/amd/build/togglesensitive.min.js b/lib/amd/build/togglesensitive.min.js index 60aaba458c1..9a17394595d 100644 --- a/lib/amd/build/togglesensitive.min.js +++ b/lib/amd/build/togglesensitive.min.js @@ -1,10 +1,10 @@ -define("core/togglesensitive",["exports","core/pagehelpers","core/templates","core/pending","core/prefetch","core/notification"],(function(_exports,_pagehelpers,_templates,_pending,_prefetch,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core/togglesensitive",["exports","core/pagehelpers","core/templates","core/pending","core/prefetch","core/notification","core_form/events"],(function(_exports,_pagehelpers,_templates,_pending,_prefetch,_notification,_events){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * JS module for toggling the sensitive input visibility (e.g. passwords, keys). * * @module core/togglesensitive * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending),_prefetch=_interopRequireDefault(_prefetch),_notification=_interopRequireDefault(_notification);const SELECTORS_BUTTON=".toggle-sensitive-btn",PIX_EYE="t/hide",PIX_EYE_SLASH="t/show";let sensitiveElementId,smallScreensOnly;_exports.init=function(elementId){let isSmallScreensOnly=arguments.length>1&&void 0!==arguments[1]&&arguments[1];const sensitiveInput=document.getElementById(elementId);null!==sensitiveInput&&(sensitiveElementId=elementId,smallScreensOnly=isSmallScreensOnly,_prefetch.default.prefetchTemplate("core/form_input_toggle_sensitive"),renderSensitiveToggle(sensitiveInput).then((()=>{})).catch(_notification.default.exception),registerListenerEvents())};const renderSensitiveToggle=async sensitiveInput=>{const{html:html}=await _templates.default.renderForPromise("core/form_input_toggle_sensitive",{smallscreensonly:smallScreensOnly,sensitiveinput:sensitiveInput.outerHTML});sensitiveInput.outerHTML=html},registerListenerEvents=()=>{document.addEventListener("click",handleButtonInteraction),smallScreensOnly&&window.addEventListener("resize",handleScreenResizing)},handleButtonInteraction=event=>{const toggleButton=event.target.closest(SELECTORS_BUTTON);if(toggleButton){const sensitiveInput=document.getElementById(sensitiveElementId);toggleSensitiveVisibility(sensitiveInput,toggleButton)}},handleScreenResizing=()=>{if(!(0,_pagehelpers.isExtraSmall)()){const sensitiveInput=document.getElementById(sensitiveElementId),toggleButton=sensitiveInput.parentNode.querySelector(SELECTORS_BUTTON);toggleSensitiveVisibility(sensitiveInput,toggleButton,!0)}},toggleSensitiveVisibility=function(sensitiveInput,toggleButton){let force=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const pendingPromise=new _pending.default("core/togglesensitive:toggle");let type,icon;!0===force?(type="password",icon=PIX_EYE):(type="password"===sensitiveInput.getAttribute("type")?"text":"password",icon="password"===sensitiveInput.getAttribute("type")?PIX_EYE_SLASH:PIX_EYE),sensitiveInput.setAttribute("type",type),_templates.default.renderPix(icon,"core").then((icon=>{toggleButton.innerHTML=icon,pendingPromise.resolve()})).catch(_notification.default.exception)}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending),_prefetch=_interopRequireDefault(_prefetch),_notification=_interopRequireDefault(_notification);const SELECTORS_BUTTON=".toggle-sensitive-btn",PIX_EYE="t/hide",PIX_EYE_SLASH="t/show";let sensitiveElementId,smallScreensOnly;_exports.init=function(elementId){let isSmallScreensOnly=arguments.length>1&&void 0!==arguments[1]&&arguments[1];const sensitiveInput=document.getElementById(elementId);null!==sensitiveInput&&(sensitiveElementId=elementId,smallScreensOnly=isSmallScreensOnly,_prefetch.default.prefetchTemplate("core/form_input_toggle_sensitive"),renderSensitiveToggle(sensitiveInput).then((()=>{(0,_events.notifyFieldStructureChanged)(sensitiveInput.id)})).catch(_notification.default.exception),registerListenerEvents())};const renderSensitiveToggle=async sensitiveInput=>{const{html:html}=await _templates.default.renderForPromise("core/form_input_toggle_sensitive",{smallscreensonly:smallScreensOnly,sensitiveinput:sensitiveInput.outerHTML});sensitiveInput.outerHTML=html},registerListenerEvents=()=>{document.addEventListener("click",handleButtonInteraction),smallScreensOnly&&window.addEventListener("resize",handleScreenResizing)},handleButtonInteraction=event=>{const toggleButton=event.target.closest(SELECTORS_BUTTON);if(toggleButton){const sensitiveInput=document.getElementById(sensitiveElementId);toggleSensitiveVisibility(sensitiveInput,toggleButton)}},handleScreenResizing=()=>{if(!(0,_pagehelpers.isExtraSmall)()){const sensitiveInput=document.getElementById(sensitiveElementId),toggleButton=sensitiveInput.parentNode.querySelector(SELECTORS_BUTTON);toggleSensitiveVisibility(sensitiveInput,toggleButton,!0)}},toggleSensitiveVisibility=function(sensitiveInput,toggleButton){let force=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const pendingPromise=new _pending.default("core/togglesensitive:toggle");let type,icon;!0===force?(type="password",icon=PIX_EYE):(type="password"===sensitiveInput.getAttribute("type")?"text":"password",icon="password"===sensitiveInput.getAttribute("type")?PIX_EYE_SLASH:PIX_EYE),sensitiveInput.setAttribute("type",type),_templates.default.renderPix(icon,"core").then((icon=>{toggleButton.innerHTML=icon,pendingPromise.resolve()})).catch(_notification.default.exception)}})); //# sourceMappingURL=togglesensitive.min.js.map \ No newline at end of file diff --git a/lib/amd/build/togglesensitive.min.js.map b/lib/amd/build/togglesensitive.min.js.map index 874832ab0ed..4e652ce8585 100644 --- a/lib/amd/build/togglesensitive.min.js.map +++ b/lib/amd/build/togglesensitive.min.js.map @@ -1 +1 @@ -{"version":3,"file":"togglesensitive.min.js","sources":["../src/togglesensitive.js"],"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 * JS module for toggling the sensitive input visibility (e.g. passwords, keys).\n *\n * @module core/togglesensitive\n * @copyright 2023 David Woloszyn \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {isExtraSmall} from 'core/pagehelpers';\nimport Templates from 'core/templates';\nimport Pending from 'core/pending';\nimport Prefetch from 'core/prefetch';\nimport Notification from 'core/notification';\n\nconst SELECTORS = {\n BUTTON: '.toggle-sensitive-btn',\n ICON: '.toggle-sensitive-btn .icon',\n};\n\nconst PIX = {\n EYE: 't/hide',\n EYE_SLASH: 't/show',\n};\n\nlet sensitiveElementId;\nlet smallScreensOnly;\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n * @param {String} elementId Form button element.\n * @param {boolean} isSmallScreensOnly Is this for small screens only?\n */\nexport const init = (elementId, isSmallScreensOnly = false) => {\n const sensitiveInput = document.getElementById(elementId);\n if (sensitiveInput === null) {\n // Exit early if invalid element id passed.\n return;\n }\n sensitiveElementId = elementId;\n smallScreensOnly = isSmallScreensOnly;\n Prefetch.prefetchTemplate('core/form_input_toggle_sensitive');\n // Render the sensitive input with a toggle button.\n renderSensitiveToggle(sensitiveInput).then(() => {\n return;\n }).catch(Notification.exception);\n // Register event listeners.\n registerListenerEvents();\n};\n\n/**\n * Render the new input html with toggle button and update the incoming html.\n *\n * @method renderSensitiveToggle\n * @param {HTMLElement} sensitiveInput HTML element for the sensitive input.\n */\nconst renderSensitiveToggle = async(sensitiveInput) => {\n const {html} = await Templates.renderForPromise(\n 'core/form_input_toggle_sensitive',\n {\n smallscreensonly: smallScreensOnly,\n sensitiveinput: sensitiveInput.outerHTML,\n }\n );\n sensitiveInput.outerHTML = html;\n};\n\n/**\n * Register event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n // Toggle the sensitive input visibility when interacting with the toggle button.\n document.addEventListener('click', handleButtonInteraction);\n // For small screens only, hide all sensitive inputs when the screen is enlarged.\n if (smallScreensOnly) {\n window.addEventListener('resize', handleScreenResizing);\n }\n};\n\n/**\n * Handle events trigger by interacting with the toggle button.\n *\n * @method handleButtonInteraction\n * @param {Event} event The button event.\n */\nconst handleButtonInteraction = (event) => {\n const toggleButton = event.target.closest(SELECTORS.BUTTON);\n if (toggleButton) {\n const sensitiveInput = document.getElementById(sensitiveElementId);\n toggleSensitiveVisibility(sensitiveInput, toggleButton);\n }\n};\n\n/**\n * Handle events trigger by resizing the screen.\n *\n * @method handleScreenResizing\n */\nconst handleScreenResizing = () => {\n if (!isExtraSmall()) {\n const sensitiveInput = document.getElementById(sensitiveElementId);\n const toggleButton = sensitiveInput.parentNode.querySelector(SELECTORS.BUTTON);\n toggleSensitiveVisibility(sensitiveInput, toggleButton, true);\n }\n};\n\n/**\n * Toggle the sensitive input visibility and its associated icon.\n *\n * @method toggleSensitiveVisibility\n * @param {HTMLInputElement} sensitiveInput The sensitive input element.\n * @param {HTMLElement} toggleButton The toggle button.\n * @param {boolean} force Force the input back to password type.\n */\nconst toggleSensitiveVisibility = (sensitiveInput, toggleButton, force = false) => {\n const pendingPromise = new Pending('core/togglesensitive:toggle');\n let type;\n let icon;\n if (force === true) {\n type = 'password';\n icon = PIX.EYE;\n } else {\n type = sensitiveInput.getAttribute('type') === 'password' ? 'text' : 'password';\n icon = sensitiveInput.getAttribute('type') === 'password' ? PIX.EYE_SLASH : PIX.EYE;\n }\n sensitiveInput.setAttribute('type', type);\n Templates.renderPix(icon, 'core').then((icon) => {\n toggleButton.innerHTML = icon;\n pendingPromise.resolve();\n return;\n }).catch(Notification.exception);\n};\n"],"names":["SELECTORS","PIX","sensitiveElementId","smallScreensOnly","elementId","isSmallScreensOnly","sensitiveInput","document","getElementById","prefetchTemplate","renderSensitiveToggle","then","catch","Notification","exception","registerListenerEvents","async","html","Templates","renderForPromise","smallscreensonly","sensitiveinput","outerHTML","addEventListener","handleButtonInteraction","window","handleScreenResizing","event","toggleButton","target","closest","toggleSensitiveVisibility","parentNode","querySelector","force","pendingPromise","Pending","type","icon","getAttribute","setAttribute","renderPix","innerHTML","resolve"],"mappings":";;;;;;;gRA6BMA,iBACM,wBAINC,QACG,SADHA,cAES,aAGXC,mBACAC,+BASgB,SAACC,eAAWC,iFACtBC,eAAiBC,SAASC,eAAeJ,WACxB,OAAnBE,iBAIJJ,mBAAqBE,UACrBD,iBAAmBE,qCACVI,iBAAiB,oCAE1BC,sBAAsBJ,gBAAgBK,MAAK,SAExCC,MAAMC,sBAAaC,WAEtBC,iCASEL,sBAAwBM,MAAAA,uBACpBC,KAACA,YAAcC,mBAAUC,iBAC3B,mCACA,CACIC,iBAAkBjB,iBAClBkB,eAAgBf,eAAegB,YAGvChB,eAAegB,UAAYL,MAQzBF,uBAAyB,KAE3BR,SAASgB,iBAAiB,QAASC,yBAE/BrB,kBACAsB,OAAOF,iBAAiB,SAAUG,uBAUpCF,wBAA2BG,cACvBC,aAAeD,MAAME,OAAOC,QAAQ9B,qBACtC4B,aAAc,OACRtB,eAAiBC,SAASC,eAAeN,oBAC/C6B,0BAA0BzB,eAAgBsB,gBAS5CF,qBAAuB,UACpB,+BAAgB,OACXpB,eAAiBC,SAASC,eAAeN,oBACzC0B,aAAetB,eAAe0B,WAAWC,cAAcjC,kBAC7D+B,0BAA0BzB,eAAgBsB,cAAc,KAY1DG,0BAA4B,SAACzB,eAAgBsB,kBAAcM,oEACvDC,eAAiB,IAAIC,iBAAQ,mCAC/BC,KACAC,MACU,IAAVJ,OACAG,KAAO,WACPC,KAAOrC,UAEPoC,KAA+C,aAAxC/B,eAAeiC,aAAa,QAAyB,OAAS,WACrED,KAA+C,aAAxChC,eAAeiC,aAAa,QAAyBtC,cAAgBA,SAEhFK,eAAekC,aAAa,OAAQH,yBAC1BI,UAAUH,KAAM,QAAQ3B,MAAM2B,OACpCV,aAAac,UAAYJ,KACzBH,eAAeQ,aAEhB/B,MAAMC,sBAAaC"} \ No newline at end of file +{"version":3,"file":"togglesensitive.min.js","sources":["../src/togglesensitive.js"],"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 * JS module for toggling the sensitive input visibility (e.g. passwords, keys).\n *\n * @module core/togglesensitive\n * @copyright 2023 David Woloszyn \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {isExtraSmall} from 'core/pagehelpers';\nimport Templates from 'core/templates';\nimport Pending from 'core/pending';\nimport Prefetch from 'core/prefetch';\nimport Notification from 'core/notification';\nimport {notifyFieldStructureChanged} from 'core_form/events';\n\nconst SELECTORS = {\n BUTTON: '.toggle-sensitive-btn',\n ICON: '.toggle-sensitive-btn .icon',\n};\n\nconst PIX = {\n EYE: 't/hide',\n EYE_SLASH: 't/show',\n};\n\nlet sensitiveElementId;\nlet smallScreensOnly;\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n * @param {String} elementId Form button element.\n * @param {boolean} isSmallScreensOnly Is this for small screens only?\n */\nexport const init = (elementId, isSmallScreensOnly = false) => {\n const sensitiveInput = document.getElementById(elementId);\n if (sensitiveInput === null) {\n // Exit early if invalid element id passed.\n return;\n }\n sensitiveElementId = elementId;\n smallScreensOnly = isSmallScreensOnly;\n Prefetch.prefetchTemplate('core/form_input_toggle_sensitive');\n // Render the sensitive input with a toggle button.\n renderSensitiveToggle(sensitiveInput).then(() => {\n // Dispatch the event indicating the sensitive input has changed.\n notifyFieldStructureChanged(sensitiveInput.id);\n return;\n }).catch(Notification.exception);\n // Register event listeners.\n registerListenerEvents();\n};\n\n/**\n * Render the new input html with toggle button and update the incoming html.\n *\n * @method renderSensitiveToggle\n * @param {HTMLElement} sensitiveInput HTML element for the sensitive input.\n */\nconst renderSensitiveToggle = async(sensitiveInput) => {\n const {html} = await Templates.renderForPromise(\n 'core/form_input_toggle_sensitive',\n {\n smallscreensonly: smallScreensOnly,\n sensitiveinput: sensitiveInput.outerHTML,\n }\n );\n sensitiveInput.outerHTML = html;\n};\n\n/**\n * Register event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n // Toggle the sensitive input visibility when interacting with the toggle button.\n document.addEventListener('click', handleButtonInteraction);\n // For small screens only, hide all sensitive inputs when the screen is enlarged.\n if (smallScreensOnly) {\n window.addEventListener('resize', handleScreenResizing);\n }\n};\n\n/**\n * Handle events trigger by interacting with the toggle button.\n *\n * @method handleButtonInteraction\n * @param {Event} event The button event.\n */\nconst handleButtonInteraction = (event) => {\n const toggleButton = event.target.closest(SELECTORS.BUTTON);\n if (toggleButton) {\n const sensitiveInput = document.getElementById(sensitiveElementId);\n toggleSensitiveVisibility(sensitiveInput, toggleButton);\n }\n};\n\n/**\n * Handle events trigger by resizing the screen.\n *\n * @method handleScreenResizing\n */\nconst handleScreenResizing = () => {\n if (!isExtraSmall()) {\n const sensitiveInput = document.getElementById(sensitiveElementId);\n const toggleButton = sensitiveInput.parentNode.querySelector(SELECTORS.BUTTON);\n toggleSensitiveVisibility(sensitiveInput, toggleButton, true);\n }\n};\n\n/**\n * Toggle the sensitive input visibility and its associated icon.\n *\n * @method toggleSensitiveVisibility\n * @param {HTMLInputElement} sensitiveInput The sensitive input element.\n * @param {HTMLElement} toggleButton The toggle button.\n * @param {boolean} force Force the input back to password type.\n */\nconst toggleSensitiveVisibility = (sensitiveInput, toggleButton, force = false) => {\n const pendingPromise = new Pending('core/togglesensitive:toggle');\n let type;\n let icon;\n if (force === true) {\n type = 'password';\n icon = PIX.EYE;\n } else {\n type = sensitiveInput.getAttribute('type') === 'password' ? 'text' : 'password';\n icon = sensitiveInput.getAttribute('type') === 'password' ? PIX.EYE_SLASH : PIX.EYE;\n }\n sensitiveInput.setAttribute('type', type);\n Templates.renderPix(icon, 'core').then((icon) => {\n toggleButton.innerHTML = icon;\n pendingPromise.resolve();\n return;\n }).catch(Notification.exception);\n};\n"],"names":["SELECTORS","PIX","sensitiveElementId","smallScreensOnly","elementId","isSmallScreensOnly","sensitiveInput","document","getElementById","prefetchTemplate","renderSensitiveToggle","then","id","catch","Notification","exception","registerListenerEvents","async","html","Templates","renderForPromise","smallscreensonly","sensitiveinput","outerHTML","addEventListener","handleButtonInteraction","window","handleScreenResizing","event","toggleButton","target","closest","toggleSensitiveVisibility","parentNode","querySelector","force","pendingPromise","Pending","type","icon","getAttribute","setAttribute","renderPix","innerHTML","resolve"],"mappings":";;;;;;;gRA8BMA,iBACM,wBAINC,QACG,SADHA,cAES,aAGXC,mBACAC,+BASgB,SAACC,eAAWC,iFACtBC,eAAiBC,SAASC,eAAeJ,WACxB,OAAnBE,iBAIJJ,mBAAqBE,UACrBD,iBAAmBE,qCACVI,iBAAiB,oCAE1BC,sBAAsBJ,gBAAgBK,MAAK,6CAEXL,eAAeM,OAE5CC,MAAMC,sBAAaC,WAEtBC,iCASEN,sBAAwBO,MAAAA,uBACpBC,KAACA,YAAcC,mBAAUC,iBAC3B,mCACA,CACIC,iBAAkBlB,iBAClBmB,eAAgBhB,eAAeiB,YAGvCjB,eAAeiB,UAAYL,MAQzBF,uBAAyB,KAE3BT,SAASiB,iBAAiB,QAASC,yBAE/BtB,kBACAuB,OAAOF,iBAAiB,SAAUG,uBAUpCF,wBAA2BG,cACvBC,aAAeD,MAAME,OAAOC,QAAQ/B,qBACtC6B,aAAc,OACRvB,eAAiBC,SAASC,eAAeN,oBAC/C8B,0BAA0B1B,eAAgBuB,gBAS5CF,qBAAuB,UACpB,+BAAgB,OACXrB,eAAiBC,SAASC,eAAeN,oBACzC2B,aAAevB,eAAe2B,WAAWC,cAAclC,kBAC7DgC,0BAA0B1B,eAAgBuB,cAAc,KAY1DG,0BAA4B,SAAC1B,eAAgBuB,kBAAcM,oEACvDC,eAAiB,IAAIC,iBAAQ,mCAC/BC,KACAC,MACU,IAAVJ,OACAG,KAAO,WACPC,KAAOtC,UAEPqC,KAA+C,aAAxChC,eAAekC,aAAa,QAAyB,OAAS,WACrED,KAA+C,aAAxCjC,eAAekC,aAAa,QAAyBvC,cAAgBA,SAEhFK,eAAemC,aAAa,OAAQH,yBAC1BI,UAAUH,KAAM,QAAQ5B,MAAM4B,OACpCV,aAAac,UAAYJ,KACzBH,eAAeQ,aAEhB/B,MAAMC,sBAAaC"} \ No newline at end of file diff --git a/lib/amd/src/togglesensitive.js b/lib/amd/src/togglesensitive.js index 1040b248e41..394778f5f41 100644 --- a/lib/amd/src/togglesensitive.js +++ b/lib/amd/src/togglesensitive.js @@ -26,6 +26,7 @@ import Templates from 'core/templates'; import Pending from 'core/pending'; import Prefetch from 'core/prefetch'; import Notification from 'core/notification'; +import {notifyFieldStructureChanged} from 'core_form/events'; const SELECTORS = { BUTTON: '.toggle-sensitive-btn', @@ -58,6 +59,8 @@ export const init = (elementId, isSmallScreensOnly = false) => { Prefetch.prefetchTemplate('core/form_input_toggle_sensitive'); // Render the sensitive input with a toggle button. renderSensitiveToggle(sensitiveInput).then(() => { + // Dispatch the event indicating the sensitive input has changed. + notifyFieldStructureChanged(sensitiveInput.id); return; }).catch(Notification.exception); // Register event listeners. diff --git a/lib/form/amd/build/events.min.js b/lib/form/amd/build/events.min.js index 1769f39faba..2dcca565d70 100644 --- a/lib/form/amd/build/events.min.js +++ b/lib/form/amd/build/events.min.js @@ -14,6 +14,6 @@ define("core_form/events",["exports","core/str","core/event_dispatcher","jquery" * window.console.log(e.target); // The form that was submitted. * window.console.log(e.detail.skipValidation); // Whether form validation was skipped. * }); - */let changesMadeString;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.types=_exports.triggerUploadStarted=_exports.triggerUploadCompleted=_exports.notifyUploadStarted=_exports.notifyUploadCompleted=_exports.notifyUploadChanged=_exports.notifyFormSubmittedByJavascript=_exports.notifyFormError=_exports.notifyFieldValidationFailure=_exports.eventTypes=void 0,_jquery=_interopRequireDefault(_jquery),_yui=_interopRequireDefault(_yui);const changesMadeCheck=e=>{e&&(e.returnValue=changesMadeString)},eventTypes={formError:"core_form/error",formSubmittedByJavascript:"core_form/submittedByJavascript",formFieldValidationFailed:"core_form/fieldValidationFailed",uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted",uploadChanged:"core_form/uploadChanged"};_exports.eventTypes=eventTypes;_exports.notifyFormError=field=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formError,{},field);_exports.notifyFormSubmittedByJavascript=function(form){let skipValidation=arguments.length>1&&void 0!==arguments[1]&&arguments[1],fallbackHandled=arguments.length>2&&void 0!==arguments[2]&&arguments[2];skipValidation&&(window.skipClientValidation=!0);const customEvent=(0,_event_dispatcher.dispatchEvent)(eventTypes.formSubmittedByJavascript,{skipValidation:skipValidation,fallbackHandled:fallbackHandled},form);return skipValidation&&(window.skipClientValidation=!1),customEvent};_exports.notifyFieldValidationFailure=(field,message)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formFieldValidationFailed,{message:message},field,{cancelable:!0});const notifyUploadStarted=async elementId=>(changesMadeString=await(0,_str.getString)("changesmadereallygoaway","moodle"),window.addEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadStarted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadStarted=notifyUploadStarted;const notifyUploadCompleted=elementId=>(window.removeEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadCompleted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadCompleted=notifyUploadCompleted;const triggerUploadStarted=notifyUploadStarted;_exports.triggerUploadStarted=triggerUploadStarted;const triggerUploadCompleted=notifyUploadCompleted;_exports.triggerUploadCompleted=triggerUploadCompleted;_exports.types={uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted"};let legacyEventsRegistered=!1;legacyEventsRegistered||(_yui.default.use("event","moodle-core-event",(()=>{document.addEventListener(eventTypes.formError,(e=>{const element=_yui.default.one(e.target),formElement=_yui.default.one(e.target.closest("form"));_yui.default.Global.fire(M.core.globalEvents.FORM_ERROR,{formid:formElement.generateID(),elementid:element.generateID()})})),document.addEventListener(eventTypes.formSubmittedByJavascript,(e=>{if(e.detail.fallbackHandled)return;e.skipValidation&&(window.skipClientValidation=!0);const form=_yui.default.one(e.target);form.fire(M.core.event.FORM_SUBMIT_AJAX,{currentTarget:form,fallbackHandled:!0}),e.skipValidation&&(window.skipClientValidation=!1)}))})),document.addEventListener(eventTypes.formFieldValidationFailed,(e=>{const legacyEvent=_jquery.default.Event("core_form-field-validation");(0,_jquery.default)(e.target).trigger(legacyEvent,e.detail.message)})),legacyEventsRegistered=!0);_exports.notifyUploadChanged=elementId=>(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadChanged,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1})})); + */let changesMadeString;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.types=_exports.triggerUploadStarted=_exports.triggerUploadCompleted=_exports.notifyUploadStarted=_exports.notifyUploadCompleted=_exports.notifyUploadChanged=_exports.notifyFormSubmittedByJavascript=_exports.notifyFormError=_exports.notifyFieldValidationFailure=_exports.notifyFieldStructureChanged=_exports.eventTypes=void 0,_jquery=_interopRequireDefault(_jquery),_yui=_interopRequireDefault(_yui);const changesMadeCheck=e=>{e&&(e.returnValue=changesMadeString)},eventTypes={formError:"core_form/error",formSubmittedByJavascript:"core_form/submittedByJavascript",formFieldValidationFailed:"core_form/fieldValidationFailed",uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted",uploadChanged:"core_form/uploadChanged",fieldStructureChanged:"core_form/fieldStructureChanged"};_exports.eventTypes=eventTypes;_exports.notifyFormError=field=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formError,{},field);_exports.notifyFormSubmittedByJavascript=function(form){let skipValidation=arguments.length>1&&void 0!==arguments[1]&&arguments[1],fallbackHandled=arguments.length>2&&void 0!==arguments[2]&&arguments[2];skipValidation&&(window.skipClientValidation=!0);const customEvent=(0,_event_dispatcher.dispatchEvent)(eventTypes.formSubmittedByJavascript,{skipValidation:skipValidation,fallbackHandled:fallbackHandled},form);return skipValidation&&(window.skipClientValidation=!1),customEvent};_exports.notifyFieldValidationFailure=(field,message)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.formFieldValidationFailed,{message:message},field,{cancelable:!0});const notifyUploadStarted=async elementId=>(changesMadeString=await(0,_str.getString)("changesmadereallygoaway","moodle"),window.addEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadStarted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadStarted=notifyUploadStarted;const notifyUploadCompleted=elementId=>(window.removeEventListener("beforeunload",changesMadeCheck),(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadCompleted,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1}));_exports.notifyUploadCompleted=notifyUploadCompleted;const triggerUploadStarted=notifyUploadStarted;_exports.triggerUploadStarted=triggerUploadStarted;const triggerUploadCompleted=notifyUploadCompleted;_exports.triggerUploadCompleted=triggerUploadCompleted;_exports.types={uploadStarted:"core_form/uploadStarted",uploadCompleted:"core_form/uploadCompleted"};let legacyEventsRegistered=!1;legacyEventsRegistered||(_yui.default.use("event","moodle-core-event",(()=>{document.addEventListener(eventTypes.formError,(e=>{const element=_yui.default.one(e.target),formElement=_yui.default.one(e.target.closest("form"));_yui.default.Global.fire(M.core.globalEvents.FORM_ERROR,{formid:formElement.generateID(),elementid:element.generateID()})})),document.addEventListener(eventTypes.formSubmittedByJavascript,(e=>{if(e.detail.fallbackHandled)return;e.skipValidation&&(window.skipClientValidation=!0);const form=_yui.default.one(e.target);form.fire(M.core.event.FORM_SUBMIT_AJAX,{currentTarget:form,fallbackHandled:!0}),e.skipValidation&&(window.skipClientValidation=!1)}))})),document.addEventListener(eventTypes.formFieldValidationFailed,(e=>{const legacyEvent=_jquery.default.Event("core_form-field-validation");(0,_jquery.default)(e.target).trigger(legacyEvent,e.detail.message)})),legacyEventsRegistered=!0);_exports.notifyUploadChanged=elementId=>(0,_event_dispatcher.dispatchEvent)(eventTypes.uploadChanged,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1});_exports.notifyFieldStructureChanged=elementId=>(0,_event_dispatcher.dispatchEvent)(eventTypes.fieldStructureChanged,{},document.getElementById(elementId),{bubbles:!0,cancellable:!1})})); //# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/lib/form/amd/build/events.min.js.map b/lib/form/amd/build/events.min.js.map index f928286812d..5757b431f7d 100644 --- a/lib/form/amd/build/events.min.js.map +++ b/lib/form/amd/build/events.min.js.map @@ -1 +1 @@ -{"version":3,"file":"events.min.js","sources":["../src/events.js"],"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 * Javascript events for the `core_form` subsystem.\n *\n * @module core_form/events\n * @copyright 2021 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.10\n *\n * @example Example of listening to a form event.\n * import {eventTypes as formEventTypes} from 'core_form/events';\n *\n * document.addEventListener(formEventTypes.formSubmittedByJavascript, e => {\n * window.console.log(e.target); // The form that was submitted.\n * window.console.log(e.detail.skipValidation); // Whether form validation was skipped.\n * });\n */\n\nimport {getString} from 'core/str';\nimport {dispatchEvent} from 'core/event_dispatcher';\n\nlet changesMadeString;\n\n/**\n * Prevent user navigate away when upload progress still running.\n * @param {Event} e The event\n */\nconst changesMadeCheck = e => {\n if (e) {\n e.returnValue = changesMadeString;\n }\n};\n\n/**\n * Events for `core_form`.\n *\n * @constant\n * @property {String} formError See {@link event:core_form/error}\n * @property {String} formFieldValidationFailed See {@link event:core_form/fieldValidationFailed}\n * @property {String} formSubmittedByJavascript See {@link event:core_form/submittedByJavascript}\n * @property {String} uploadChanged See {@link event:core_form/uploadChanged}\n */\nexport const eventTypes = {\n /**\n * An event triggered when a form contains an error\n *\n * @event formError\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field which errored\n */\n formError: 'core_form/error',\n\n /**\n * An event triggered when an mform is about to be submitted via javascript.\n *\n * @event core_form/submittedByJavascript\n * @type {CustomEvent}\n * @property {HTMLElement} target The form that was submitted\n * @property {object} detail\n * @property {boolean} detail.skipValidation Whether the form was submitted without validation (i.e. via a Cancel button)\n * @property {boolean} detail.fallbackHandled Whether the legacy YUI event has been handled\n */\n formSubmittedByJavascript: 'core_form/submittedByJavascript',\n\n /**\n * An event triggered upon form field validation failure.\n *\n * @event core_form/fieldValidationFailed\n * @type {CustomEvent}\n * @property {HTMLElement} target The field that failed validation\n * @property {object} detail\n * @property {String} detail.message The message displayed upon failure\n */\n formFieldValidationFailed: 'core_form/fieldValidationFailed',\n\n /**\n * An event triggered when an upload is started\n *\n * @event core_form/uploadStarted\n * @type {CustomEvent}\n * @property {HTMLElement} target The location where the upload began\n */\n uploadStarted: 'core_form/uploadStarted',\n\n /**\n * An event triggered when an upload completes\n *\n * @event core_form/uploadCompleted\n * @type {CustomEvent}\n * @property {HTMLElement} target The location where the upload completed\n */\n uploadCompleted: 'core_form/uploadCompleted',\n\n /**\n * An event triggered when a file upload field has been changed.\n *\n * @event core_form/uploadChanged\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field which was changed\n */\n uploadChanged: 'core_form/uploadChanged',\n};\n\n// These are only imported for legacy.\nimport jQuery from 'jquery';\nimport Y from 'core/yui';\n\n/**\n * Trigger an event to indicate that a form field contained an error.\n *\n * @method notifyFormError\n * @param {HTMLElement} field The form field causing the error\n * @returns {CustomEvent}\n * @fires formError\n */\nexport const notifyFormError = field => dispatchEvent(eventTypes.formError, {}, field);\n\n/**\n * Trigger an event to indiciate that a form was submitted by Javascript.\n *\n * @method\n * @param {HTMLElement} form The form that was submitted\n * @param {Boolean} skipValidation Submit the form without validation. E.g. \"Cancel\".\n * @param {Boolean} fallbackHandled The legacy YUI event has been handled\n * @returns {CustomEvent}\n * @fires formSubmittedByJavascript\n */\nexport const notifyFormSubmittedByJavascript = (form, skipValidation = false, fallbackHandled = false) => {\n if (skipValidation) {\n window.skipClientValidation = true;\n }\n\n const customEvent = dispatchEvent(\n eventTypes.formSubmittedByJavascript,\n {\n skipValidation,\n fallbackHandled,\n },\n form\n );\n\n if (skipValidation) {\n window.skipClientValidation = false;\n }\n\n return customEvent;\n};\n\n/**\n * Trigger an event to indicate that a form field contained an error.\n *\n * @method notifyFieldValidationFailure\n * @param {HTMLElement} field The field which failed validation\n * @param {String} message The message displayed\n * @returns {CustomEvent}\n * @fires formFieldValidationFailed\n */\nexport const notifyFieldValidationFailure = (field, message) => dispatchEvent(\n eventTypes.formFieldValidationFailed,\n {\n message,\n },\n field,\n {\n cancelable: true\n }\n);\n\n/**\n * Trigger an event to indicate that an upload was started.\n *\n * @method\n * @param {String} elementId The element which was uploaded to\n * @returns {CustomEvent}\n * @fires uploadStarted\n */\nexport const notifyUploadStarted = async elementId => {\n // Add an additional check for changes made.\n changesMadeString = await getString('changesmadereallygoaway', 'moodle');\n window.addEventListener('beforeunload', changesMadeCheck);\n\n return dispatchEvent(\n eventTypes.uploadStarted,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n );\n};\n\n/**\n * Trigger an event to indicate that an upload was completed.\n *\n * @method\n * @param {String} elementId The element which was uploaded to\n * @returns {CustomEvent}\n * @fires uploadCompleted\n */\nexport const notifyUploadCompleted = elementId => {\n // Remove the additional check for changes made.\n window.removeEventListener('beforeunload', changesMadeCheck);\n\n return dispatchEvent(\n eventTypes.uploadCompleted,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n );\n};\n\n/**\n * Trigger upload start event.\n *\n * @method\n * @param {String} elementId\n * @returns {CustomEvent}\n * @fires uploadStarted\n * @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadStarted notifyUploadStarted}\n */\nexport const triggerUploadStarted = notifyUploadStarted;\n\n/**\n * Trigger upload complete event.\n *\n * @method\n * @param {String} elementId\n * @returns {CustomEvent}\n * @fires uploadCompleted\n * @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadCompleted notifyUploadCompleted}\n */\nexport const triggerUploadCompleted = notifyUploadCompleted;\n\n/**\n * List of the events.\n *\n * @deprecated since Moodle 4.0. See {@link module:core_form/events.eventTypes eventTypes} instead.\n **/\nexport const types = {\n uploadStarted: 'core_form/uploadStarted',\n uploadCompleted: 'core_form/uploadCompleted',\n};\n\nlet legacyEventsRegistered = false;\nif (!legacyEventsRegistered) {\n // The following event triggers are legacy and will be removed in the future.\n // The following approach provides a backwards-compatability layer for the new events.\n // Code should be updated to make use of native events.\n Y.use('event', 'moodle-core-event', () => {\n\n // Watch for the new native formError event, and trigger the legacy YUI event.\n document.addEventListener(eventTypes.formError, e => {\n const element = Y.one(e.target);\n const formElement = Y.one(e.target.closest('form'));\n\n Y.Global.fire(\n M.core.globalEvents.FORM_ERROR,\n {\n formid: formElement.generateID(),\n elementid: element.generateID(),\n }\n );\n });\n\n // Watch for the new native formSubmittedByJavascript event, and trigger the legacy YUI event.\n document.addEventListener(eventTypes.formSubmittedByJavascript, e => {\n if (e.detail.fallbackHandled) {\n // This event was originally generated by a YUI event.\n // Do not generate another as this will recurse.\n return;\n }\n\n if (e.skipValidation) {\n window.skipClientValidation = true;\n }\n\n // Trigger the legacy YUI event.\n const form = Y.one(e.target);\n form.fire(\n M.core.event.FORM_SUBMIT_AJAX,\n {\n currentTarget: form,\n fallbackHandled: true,\n }\n );\n\n if (e.skipValidation) {\n window.skipClientValidation = false;\n }\n });\n });\n\n // Watch for the new native formFieldValidationFailed event, and trigger the legacy jQuery event.\n document.addEventListener(eventTypes.formFieldValidationFailed, e => {\n // Note: The \"core_form-field-validation\" event is hard-coded in core/event.\n // This is not included to prevent cyclic module dependencies.\n const legacyEvent = jQuery.Event(\"core_form-field-validation\");\n\n jQuery(e.target).trigger(legacyEvent, e.detail.message);\n });\n\n legacyEventsRegistered = true;\n}\n\n/**\n * Trigger an event to notify the file upload field has been changed.\n *\n * @method\n * @param {string} elementId The element which was changed\n * @returns {CustomEvent}\n * @fires uploadChanged\n */\nexport const notifyUploadChanged = elementId => dispatchEvent(\n eventTypes.uploadChanged,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n);\n"],"names":["changesMadeString","changesMadeCheck","e","returnValue","eventTypes","formError","formSubmittedByJavascript","formFieldValidationFailed","uploadStarted","uploadCompleted","uploadChanged","field","form","skipValidation","fallbackHandled","window","skipClientValidation","customEvent","message","cancelable","notifyUploadStarted","async","addEventListener","document","getElementById","elementId","bubbles","cancellable","notifyUploadCompleted","removeEventListener","triggerUploadStarted","triggerUploadCompleted","legacyEventsRegistered","use","element","Y","one","target","formElement","closest","Global","fire","M","core","globalEvents","FORM_ERROR","formid","generateID","elementid","detail","event","FORM_SUBMIT_AJAX","currentTarget","legacyEvent","jQuery","Event","trigger"],"mappings":";;;;;;;;;;;;;;;;SAmCIA,mcAMEC,iBAAmBC,IACjBA,IACAA,EAAEC,YAAcH,oBAaXI,WAAa,CAQtBC,UAAW,kBAYXC,0BAA2B,kCAW3BC,0BAA2B,kCAS3BC,cAAe,0BASfC,gBAAiB,4BASjBC,cAAe,mFAeYC,QAAS,mCAAcP,WAAWC,UAAW,GAAIM,gDAYjC,SAACC,UAAMC,uEAAwBC,wEACtED,iBACAE,OAAOC,sBAAuB,SAG5BC,aAAc,mCAChBb,WAAWE,0BACX,CACIO,eAAAA,eACAC,gBAAAA,iBAEJF,aAGAC,iBACAE,OAAOC,sBAAuB,GAG3BC,mDAYiC,CAACN,MAAOO,WAAY,mCAC5Dd,WAAWG,0BACX,CACIW,QAAAA,SAEJP,MACA,CACIQ,YAAY,UAYPC,oBAAsBC,MAAAA,YAE/BrB,wBAA0B,kBAAU,0BAA2B,UAC/De,OAAOO,iBAAiB,eAAgBrB,mBAEjC,mCACHG,WAAWI,cACX,GACAe,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,4DAaZC,sBAAwBH,YAEjCV,OAAOc,oBAAoB,eAAgB5B,mBAEpC,mCACHG,WAAWK,gBACX,GACAc,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,gEAcZG,qBAAuBV,6EAWvBW,uBAAyBH,4FAOjB,CACjBpB,cAAe,0BACfC,gBAAiB,iCAGjBuB,wBAAyB,EACxBA,sCAICC,IAAI,QAAS,qBAAqB,KAGhCV,SAASD,iBAAiBlB,WAAWC,WAAWH,UACtCgC,QAAUC,aAAEC,IAAIlC,EAAEmC,QAClBC,YAAcH,aAAEC,IAAIlC,EAAEmC,OAAOE,QAAQ,sBAEzCC,OAAOC,KACLC,EAAEC,KAAKC,aAAaC,WACpB,CACIC,OAAQR,YAAYS,aACpBC,UAAWd,QAAQa,kBAM/BxB,SAASD,iBAAiBlB,WAAWE,2BAA2BJ,OACxDA,EAAE+C,OAAOnC,uBAMTZ,EAAEW,iBACFE,OAAOC,sBAAuB,SAI5BJ,KAAOuB,aAAEC,IAAIlC,EAAEmC,QACrBzB,KAAK6B,KACDC,EAAEC,KAAKO,MAAMC,iBACb,CACIC,cAAexC,KACfE,iBAAiB,IAIrBZ,EAAEW,iBACFE,OAAOC,sBAAuB,SAM1CO,SAASD,iBAAiBlB,WAAWG,2BAA2BL,UAGtDmD,YAAcC,gBAAOC,MAAM,kDAE1BrD,EAAEmC,QAAQmB,QAAQH,YAAanD,EAAE+C,OAAO/B,YAGnDc,wBAAyB,gCAWMP,YAAa,mCAC5CrB,WAAWM,cACX,GACAa,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa"} \ No newline at end of file +{"version":3,"file":"events.min.js","sources":["../src/events.js"],"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 * Javascript events for the `core_form` subsystem.\n *\n * @module core_form/events\n * @copyright 2021 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.10\n *\n * @example Example of listening to a form event.\n * import {eventTypes as formEventTypes} from 'core_form/events';\n *\n * document.addEventListener(formEventTypes.formSubmittedByJavascript, e => {\n * window.console.log(e.target); // The form that was submitted.\n * window.console.log(e.detail.skipValidation); // Whether form validation was skipped.\n * });\n */\n\nimport {getString} from 'core/str';\nimport {dispatchEvent} from 'core/event_dispatcher';\n\nlet changesMadeString;\n\n/**\n * Prevent user navigate away when upload progress still running.\n * @param {Event} e The event\n */\nconst changesMadeCheck = e => {\n if (e) {\n e.returnValue = changesMadeString;\n }\n};\n\n/**\n * Events for `core_form`.\n *\n * @constant\n * @property {String} formError See {@link event:core_form/error}\n * @property {String} formFieldValidationFailed See {@link event:core_form/fieldValidationFailed}\n * @property {String} formSubmittedByJavascript See {@link event:core_form/submittedByJavascript}\n * @property {String} uploadChanged See {@link event:core_form/uploadChanged}\n * @property {String} fieldStructureChanged See {@link event:core_form/fieldStructureChanged}\n */\nexport const eventTypes = {\n /**\n * An event triggered when a form contains an error\n *\n * @event formError\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field which errored\n */\n formError: 'core_form/error',\n\n /**\n * An event triggered when an mform is about to be submitted via javascript.\n *\n * @event core_form/submittedByJavascript\n * @type {CustomEvent}\n * @property {HTMLElement} target The form that was submitted\n * @property {object} detail\n * @property {boolean} detail.skipValidation Whether the form was submitted without validation (i.e. via a Cancel button)\n * @property {boolean} detail.fallbackHandled Whether the legacy YUI event has been handled\n */\n formSubmittedByJavascript: 'core_form/submittedByJavascript',\n\n /**\n * An event triggered upon form field validation failure.\n *\n * @event core_form/fieldValidationFailed\n * @type {CustomEvent}\n * @property {HTMLElement} target The field that failed validation\n * @property {object} detail\n * @property {String} detail.message The message displayed upon failure\n */\n formFieldValidationFailed: 'core_form/fieldValidationFailed',\n\n /**\n * An event triggered when an upload is started\n *\n * @event core_form/uploadStarted\n * @type {CustomEvent}\n * @property {HTMLElement} target The location where the upload began\n */\n uploadStarted: 'core_form/uploadStarted',\n\n /**\n * An event triggered when an upload completes\n *\n * @event core_form/uploadCompleted\n * @type {CustomEvent}\n * @property {HTMLElement} target The location where the upload completed\n */\n uploadCompleted: 'core_form/uploadCompleted',\n\n /**\n * An event triggered when a file upload field has been changed.\n *\n * @event core_form/uploadChanged\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field which was changed\n */\n uploadChanged: 'core_form/uploadChanged',\n\n /**\n * An event triggered when a form field structure has changed.\n *\n * @event core_form/fieldStructureChanged\n * @type {CustomEvent}\n * @property {HTMLElement} target The form field that has changed\n */\n fieldStructureChanged: 'core_form/fieldStructureChanged',\n};\n\n// These are only imported for legacy.\nimport jQuery from 'jquery';\nimport Y from 'core/yui';\n\n/**\n * Trigger an event to indicate that a form field contained an error.\n *\n * @method notifyFormError\n * @param {HTMLElement} field The form field causing the error\n * @returns {CustomEvent}\n * @fires formError\n */\nexport const notifyFormError = field => dispatchEvent(eventTypes.formError, {}, field);\n\n/**\n * Trigger an event to indiciate that a form was submitted by Javascript.\n *\n * @method\n * @param {HTMLElement} form The form that was submitted\n * @param {Boolean} skipValidation Submit the form without validation. E.g. \"Cancel\".\n * @param {Boolean} fallbackHandled The legacy YUI event has been handled\n * @returns {CustomEvent}\n * @fires formSubmittedByJavascript\n */\nexport const notifyFormSubmittedByJavascript = (form, skipValidation = false, fallbackHandled = false) => {\n if (skipValidation) {\n window.skipClientValidation = true;\n }\n\n const customEvent = dispatchEvent(\n eventTypes.formSubmittedByJavascript,\n {\n skipValidation,\n fallbackHandled,\n },\n form\n );\n\n if (skipValidation) {\n window.skipClientValidation = false;\n }\n\n return customEvent;\n};\n\n/**\n * Trigger an event to indicate that a form field contained an error.\n *\n * @method notifyFieldValidationFailure\n * @param {HTMLElement} field The field which failed validation\n * @param {String} message The message displayed\n * @returns {CustomEvent}\n * @fires formFieldValidationFailed\n */\nexport const notifyFieldValidationFailure = (field, message) => dispatchEvent(\n eventTypes.formFieldValidationFailed,\n {\n message,\n },\n field,\n {\n cancelable: true\n }\n);\n\n/**\n * Trigger an event to indicate that an upload was started.\n *\n * @method\n * @param {String} elementId The element which was uploaded to\n * @returns {CustomEvent}\n * @fires uploadStarted\n */\nexport const notifyUploadStarted = async elementId => {\n // Add an additional check for changes made.\n changesMadeString = await getString('changesmadereallygoaway', 'moodle');\n window.addEventListener('beforeunload', changesMadeCheck);\n\n return dispatchEvent(\n eventTypes.uploadStarted,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n );\n};\n\n/**\n * Trigger an event to indicate that an upload was completed.\n *\n * @method\n * @param {String} elementId The element which was uploaded to\n * @returns {CustomEvent}\n * @fires uploadCompleted\n */\nexport const notifyUploadCompleted = elementId => {\n // Remove the additional check for changes made.\n window.removeEventListener('beforeunload', changesMadeCheck);\n\n return dispatchEvent(\n eventTypes.uploadCompleted,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n );\n};\n\n/**\n * Trigger upload start event.\n *\n * @method\n * @param {String} elementId\n * @returns {CustomEvent}\n * @fires uploadStarted\n * @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadStarted notifyUploadStarted}\n */\nexport const triggerUploadStarted = notifyUploadStarted;\n\n/**\n * Trigger upload complete event.\n *\n * @method\n * @param {String} elementId\n * @returns {CustomEvent}\n * @fires uploadCompleted\n * @deprecated Since Moodle 4.0 See {@link module:core_form/events.notifyUploadCompleted notifyUploadCompleted}\n */\nexport const triggerUploadCompleted = notifyUploadCompleted;\n\n/**\n * List of the events.\n *\n * @deprecated since Moodle 4.0. See {@link module:core_form/events.eventTypes eventTypes} instead.\n **/\nexport const types = {\n uploadStarted: 'core_form/uploadStarted',\n uploadCompleted: 'core_form/uploadCompleted',\n};\n\nlet legacyEventsRegistered = false;\nif (!legacyEventsRegistered) {\n // The following event triggers are legacy and will be removed in the future.\n // The following approach provides a backwards-compatability layer for the new events.\n // Code should be updated to make use of native events.\n Y.use('event', 'moodle-core-event', () => {\n\n // Watch for the new native formError event, and trigger the legacy YUI event.\n document.addEventListener(eventTypes.formError, e => {\n const element = Y.one(e.target);\n const formElement = Y.one(e.target.closest('form'));\n\n Y.Global.fire(\n M.core.globalEvents.FORM_ERROR,\n {\n formid: formElement.generateID(),\n elementid: element.generateID(),\n }\n );\n });\n\n // Watch for the new native formSubmittedByJavascript event, and trigger the legacy YUI event.\n document.addEventListener(eventTypes.formSubmittedByJavascript, e => {\n if (e.detail.fallbackHandled) {\n // This event was originally generated by a YUI event.\n // Do not generate another as this will recurse.\n return;\n }\n\n if (e.skipValidation) {\n window.skipClientValidation = true;\n }\n\n // Trigger the legacy YUI event.\n const form = Y.one(e.target);\n form.fire(\n M.core.event.FORM_SUBMIT_AJAX,\n {\n currentTarget: form,\n fallbackHandled: true,\n }\n );\n\n if (e.skipValidation) {\n window.skipClientValidation = false;\n }\n });\n });\n\n // Watch for the new native formFieldValidationFailed event, and trigger the legacy jQuery event.\n document.addEventListener(eventTypes.formFieldValidationFailed, e => {\n // Note: The \"core_form-field-validation\" event is hard-coded in core/event.\n // This is not included to prevent cyclic module dependencies.\n const legacyEvent = jQuery.Event(\"core_form-field-validation\");\n\n jQuery(e.target).trigger(legacyEvent, e.detail.message);\n });\n\n legacyEventsRegistered = true;\n}\n\n/**\n * Trigger an event to notify the file upload field has been changed.\n *\n * @method\n * @param {string} elementId The element which was changed\n * @returns {CustomEvent}\n * @fires uploadChanged\n */\nexport const notifyUploadChanged = elementId => dispatchEvent(\n eventTypes.uploadChanged,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n);\n\n/**\n * Trigger an event to notify the field structure has changed.\n *\n * @method\n * @param {string} elementId The element which was changed\n * @returns {CustomEvent}\n * @fires fieldStructureChanged\n */\nexport const notifyFieldStructureChanged = elementId => dispatchEvent(\n eventTypes.fieldStructureChanged,\n {},\n document.getElementById(elementId),\n {\n bubbles: true,\n cancellable: false,\n }\n);\n"],"names":["changesMadeString","changesMadeCheck","e","returnValue","eventTypes","formError","formSubmittedByJavascript","formFieldValidationFailed","uploadStarted","uploadCompleted","uploadChanged","fieldStructureChanged","field","form","skipValidation","fallbackHandled","window","skipClientValidation","customEvent","message","cancelable","notifyUploadStarted","async","addEventListener","document","getElementById","elementId","bubbles","cancellable","notifyUploadCompleted","removeEventListener","triggerUploadStarted","triggerUploadCompleted","legacyEventsRegistered","use","element","Y","one","target","formElement","closest","Global","fire","M","core","globalEvents","FORM_ERROR","formid","generateID","elementid","detail","event","FORM_SUBMIT_AJAX","currentTarget","legacyEvent","jQuery","Event","trigger"],"mappings":";;;;;;;;;;;;;;;;SAmCIA,weAMEC,iBAAmBC,IACjBA,IACAA,EAAEC,YAAcH,oBAcXI,WAAa,CAQtBC,UAAW,kBAYXC,0BAA2B,kCAW3BC,0BAA2B,kCAS3BC,cAAe,0BASfC,gBAAiB,4BASjBC,cAAe,0BASfC,sBAAuB,2FAeIC,QAAS,mCAAcR,WAAWC,UAAW,GAAIO,gDAYjC,SAACC,UAAMC,uEAAwBC,wEACtED,iBACAE,OAAOC,sBAAuB,SAG5BC,aAAc,mCAChBd,WAAWE,0BACX,CACIQ,eAAAA,eACAC,gBAAAA,iBAEJF,aAGAC,iBACAE,OAAOC,sBAAuB,GAG3BC,mDAYiC,CAACN,MAAOO,WAAY,mCAC5Df,WAAWG,0BACX,CACIY,QAAAA,SAEJP,MACA,CACIQ,YAAY,UAYPC,oBAAsBC,MAAAA,YAE/BtB,wBAA0B,kBAAU,0BAA2B,UAC/DgB,OAAOO,iBAAiB,eAAgBtB,mBAEjC,mCACHG,WAAWI,cACX,GACAgB,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,4DAaZC,sBAAwBH,YAEjCV,OAAOc,oBAAoB,eAAgB7B,mBAEpC,mCACHG,WAAWK,gBACX,GACAe,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,gEAcZG,qBAAuBV,6EAWvBW,uBAAyBH,4FAOjB,CACjBrB,cAAe,0BACfC,gBAAiB,iCAGjBwB,wBAAyB,EACxBA,sCAICC,IAAI,QAAS,qBAAqB,KAGhCV,SAASD,iBAAiBnB,WAAWC,WAAWH,UACtCiC,QAAUC,aAAEC,IAAInC,EAAEoC,QAClBC,YAAcH,aAAEC,IAAInC,EAAEoC,OAAOE,QAAQ,sBAEzCC,OAAOC,KACLC,EAAEC,KAAKC,aAAaC,WACpB,CACIC,OAAQR,YAAYS,aACpBC,UAAWd,QAAQa,kBAM/BxB,SAASD,iBAAiBnB,WAAWE,2BAA2BJ,OACxDA,EAAEgD,OAAOnC,uBAMTb,EAAEY,iBACFE,OAAOC,sBAAuB,SAI5BJ,KAAOuB,aAAEC,IAAInC,EAAEoC,QACrBzB,KAAK6B,KACDC,EAAEC,KAAKO,MAAMC,iBACb,CACIC,cAAexC,KACfE,iBAAiB,IAIrBb,EAAEY,iBACFE,OAAOC,sBAAuB,SAM1CO,SAASD,iBAAiBnB,WAAWG,2BAA2BL,UAGtDoD,YAAcC,gBAAOC,MAAM,kDAE1BtD,EAAEoC,QAAQmB,QAAQH,YAAapD,EAAEgD,OAAO/B,YAGnDc,wBAAyB,gCAWMP,YAAa,mCAC5CtB,WAAWM,cACX,GACAc,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa,yCAYsBF,YAAa,mCACpDtB,WAAWO,sBACX,GACAa,SAASC,eAAeC,WACxB,CACIC,SAAS,EACTC,aAAa"} \ No newline at end of file diff --git a/lib/form/amd/src/events.js b/lib/form/amd/src/events.js index 2cbedb241e0..b5c3b4df82d 100644 --- a/lib/form/amd/src/events.js +++ b/lib/form/amd/src/events.js @@ -53,6 +53,7 @@ const changesMadeCheck = e => { * @property {String} formFieldValidationFailed See {@link event:core_form/fieldValidationFailed} * @property {String} formSubmittedByJavascript See {@link event:core_form/submittedByJavascript} * @property {String} uploadChanged See {@link event:core_form/uploadChanged} + * @property {String} fieldStructureChanged See {@link event:core_form/fieldStructureChanged} */ export const eventTypes = { /** @@ -113,6 +114,15 @@ export const eventTypes = { * @property {HTMLElement} target The form field which was changed */ uploadChanged: 'core_form/uploadChanged', + + /** + * An event triggered when a form field structure has changed. + * + * @event core_form/fieldStructureChanged + * @type {CustomEvent} + * @property {HTMLElement} target The form field that has changed + */ + fieldStructureChanged: 'core_form/fieldStructureChanged', }; // These are only imported for legacy. @@ -337,3 +347,21 @@ export const notifyUploadChanged = elementId => dispatchEvent( cancellable: false, } ); + +/** + * Trigger an event to notify the field structure has changed. + * + * @method + * @param {string} elementId The element which was changed + * @returns {CustomEvent} + * @fires fieldStructureChanged + */ +export const notifyFieldStructureChanged = elementId => dispatchEvent( + eventTypes.fieldStructureChanged, + {}, + document.getElementById(elementId), + { + bubbles: true, + cancellable: false, + } +); diff --git a/lib/templates/loginform.mustache b/lib/templates/loginform.mustache index 6dc17fa437c..9e5fd7aa72f 100644 --- a/lib/templates/loginform.mustache +++ b/lib/templates/loginform.mustache @@ -222,12 +222,18 @@ }); {{/togglepassword}} {{#autofocusform}} - var userNameField = document.getElementById('username'); - if (userNameField.value.length == 0) { - userNameField.focus(); - } else { - document.getElementById('password').focus(); - } + require(['core_form/events'], function(FormEvent) { + function autoFocus() { + const userNameField = document.getElementById('username'); + if (userNameField.value.length == 0) { + userNameField.focus(); + } else { + document.getElementById('password').focus(); + } + } + autoFocus(); + window.addEventListener(FormEvent.eventTypes.fieldStructureChanged, autoFocus); + }); {{/autofocusform}} {{/error}} {{#error}}