From ca25005c69a7fd5b947350eb62278eae07b339cb Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 22 Aug 2016 07:03:51 +0800 Subject: [PATCH] MDL-53048 form: Rewrite passwordunmask This version: * should work with the Behat Goutte driver * should not suffer from password autofill anxiety * should allow unmasking (and masking) of a password * should allow editing of passwords in either masked, or unmasked form AMOS BEGIN MOV [revealpassword,core_form],[passwordunmaskrevealhint,core_form] AMOS END --- .../setting_configpasswordunmask.mustache | 63 ++-- lang/en/deprecated.txt | 1 + lang/en/form.php | 9 +- lib/adminlib.php | 10 +- lib/behat/behat_field_manager.php | 4 + lib/behat/classes/partial_named_selector.php | 5 + .../form_field/behat_form_passwordunmask.php | 64 ++++ lib/form/amd/build/passwordunmask.min.js | 1 + lib/form/amd/src/passwordunmask.js | 286 ++++++++++++++++++ lib/form/passwordunmask.php | 26 +- lib/form/templatable_form_element.php | 1 + .../element-passwordunmask-fill.mustache | 56 ++++ .../templates/element-passwordunmask.mustache | 93 ++++++ lib/form/templates/element-template.mustache | 64 ++++ .../moodle-form-passwordunmask-debug.js | 44 +-- .../moodle-form-passwordunmask-min.js | 2 +- .../moodle-form-passwordunmask.js | 42 +-- .../src/passwordunmask/js/passwordunmask.js | 42 +-- .../passwordunmask/meta/passwordunmask.json | 5 +- pix/t/passwordunmask-edit.png | Bin 0 -> 172 bytes pix/t/passwordunmask-edit.svg | 3 + pix/t/passwordunmask-reveal.png | Bin 0 -> 225 bytes pix/t/passwordunmask-reveal.svg | 3 + .../setting_configpasswordunmask.mustache | 82 ++--- .../core_form/element-passwordunmask.mustache | 102 ++++++- theme/bootstrapbase/less/moodle/forms.less | 6 + theme/bootstrapbase/style/moodle.css | 5 + 27 files changed, 791 insertions(+), 228 deletions(-) create mode 100644 lib/behat/form_field/behat_form_passwordunmask.php create mode 100644 lib/form/amd/build/passwordunmask.min.js create mode 100644 lib/form/amd/src/passwordunmask.js create mode 100644 lib/form/templates/element-passwordunmask-fill.mustache create mode 100644 lib/form/templates/element-passwordunmask.mustache create mode 100644 lib/form/templates/element-template.mustache create mode 100644 pix/t/passwordunmask-edit.png create mode 100644 pix/t/passwordunmask-edit.svg create mode 100644 pix/t/passwordunmask-reveal.png create mode 100644 pix/t/passwordunmask-reveal.svg diff --git a/admin/templates/setting_configpasswordunmask.mustache b/admin/templates/setting_configpasswordunmask.mustache index b9da812422514..bf00ea7bcc208 100644 --- a/admin/templates/setting_configpasswordunmask.mustache +++ b/admin/templates/setting_configpasswordunmask.mustache @@ -34,40 +34,35 @@ } }}
- -
+ + + + + + + + {{> core_form/element-passwordunmask-fill }} + {{# pix }} t/passwordunmask-edit, core, {{# str }} passwordunmaskedithint, form {{/ str }}{{/ pix }} + + + {{# pix }} t/passwordunmask-reveal, core, {{# str }} passwordunmaskrevealhint, form {{/ str }}{{/ pix }} + + + +
{{#js}} -(function() { - var id = '{{id}}'; - var unmaskid = id + 'unmask'; - var unmaskdivid = id + 'unmaskdiv'; - var unmaskstr = {{#quote}}{{#str}}unmaskpassword, form{{/str}}{{/quote}}; - var is_ie = (navigator.userAgent.toLowerCase().indexOf("msie") != -1); - - document.getElementById(id).setAttribute("autocomplete", "off"); - - var unmaskdiv = document.getElementById(unmaskdivid); - - var unmaskchb = document.createElement("input"); - unmaskchb.setAttribute("type", "checkbox"); - unmaskchb.setAttribute("id", unmaskid); - unmaskchb.onchange = function() {unmaskPassword(id);}; - unmaskdiv.appendChild(unmaskchb); - - var unmasklbl = document.createElement("label"); - unmasklbl.innerHTML = unmaskstr; - if (is_ie) { - unmasklbl.setAttribute("htmlFor", unmaskid); - } else { - unmasklbl.setAttribute("for", unmaskid); - } - unmaskdiv.appendChild(unmasklbl); - - if (is_ie) { - // Ugly hack to work around the famous onchange IE bug. - unmaskchb.onclick = function() {this.blur();}; - unmaskdiv.onclick = function() {this.blur();}; - } -})() +require(['core_form/passwordunmask'], function(PasswordUnmask) { + new PasswordUnmask("{{ id }}"); +}); {{/js}} diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index b5986aa41bb99..3aa78e6ed66a3 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -34,3 +34,4 @@ downloadoptions,core_table downloadtsv,core_table downloadxhtml,core_table invalidpersistent,core_competency +revealpassword,core_form diff --git a/lang/en/form.php b/lang/en/form.php index 1f70f67404468..39c5c80af5103 100644 --- a/lang/en/form.php +++ b/lang/en/form.php @@ -51,10 +51,14 @@ $string['nonexistentformelements'] = 'Trying to add help buttons to non-existent form elements : {$a}'; $string['noselection'] = 'No selection'; $string['nosuggestions'] = 'No suggestions'; +$string['novalue'] = 'Nothing entered'; +$string['novalueclicktoset'] = 'Click to enter text'; $string['optional'] = 'Optional'; $string['othersettings'] = 'Other settings'; +$string['passwordunmaskedithint'] = 'Edit password'; +$string['passwordunmaskrevealhint'] = 'Reveal'; +$string['passwordunmaskinstructions'] = 'Press enter to save changes'; $string['requiredelement'] = 'Required field'; -$string['revealpassword'] = 'Reveal'; $string['security'] = 'Security'; $string['selectallornone'] = 'Select all/none'; $string['selected'] = 'Selected'; @@ -68,3 +72,6 @@ $string['timing'] = 'Timing'; $string['unmaskpassword'] = 'Unmask'; $string['year'] = 'Year'; + +// Deprecated since 3.2. +$string['revealpassword'] = 'Reveal'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 68e1b4821e48d..515a6c2511eb0 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -2406,13 +2406,11 @@ protected function add_to_config_log($name, $oldvalue, $value) { } /** - * Returns XHTML for the field - * Writes Javascript into the HTML below right before the last div + * Returns HTML for the field. * - * @todo Make javascript available through newer methods if possible - * @param string $data Value for the field - * @param string $query Passed as final argument for format_admin_setting - * @return string XHTML field + * @param string $data Value for the field + * @param string $query Passed as final argument for format_admin_setting + * @return string Rendered HTML */ public function output_html($data, $query='') { global $OUTPUT; diff --git a/lib/behat/behat_field_manager.php b/lib/behat/behat_field_manager.php index 27fbafab4ae69..3c770bbe37966 100644 --- a/lib/behat/behat_field_manager.php +++ b/lib/behat/behat_field_manager.php @@ -228,6 +228,10 @@ protected static function get_field_node_type(NodeElement $fieldnode, Session $s return $type; } + if (!empty($fieldnode->find('xpath', '/ancestor::*[@data-passwordunmaskid]'))) { + return 'passwordunmask'; + } + // We look for a parent node with 'felement' class. if ($class = $fieldnode->getParent()->getAttribute('class')) { diff --git a/lib/behat/classes/partial_named_selector.php b/lib/behat/classes/partial_named_selector.php index 538e3fc75ef6d..44338c17c381c 100644 --- a/lib/behat/classes/partial_named_selector.php +++ b/lib/behat/classes/partial_named_selector.php @@ -186,6 +186,11 @@ public function __construct() { 'filemanager' => << <<. + +/** + * Silly behat_form_select extension. + * + * @package core_form + * @category test + * @copyright 2013 David MonllaĆ³ + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/behat_form_text.php'); + +/** + * Allows interaction with passwordunmask form fields. + * + * Plain behat_form_select extension as it is the same + * kind of field. + * + * @package core_form + * @category test + * @copyright 2013 David MonllaĆ³ + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_form_passwordunmask extends behat_form_text { + /** + * Sets the value to a field. + * + * @param string $value + * @return void + */ + public function set_value($value) { + if ($this->running_javascript()) { + $id = $this->field->getAttribute('id'); + $js = <<session->executeScript($js); + } + + $this->field->setValue($value); + } +} diff --git a/lib/form/amd/build/passwordunmask.min.js b/lib/form/amd/build/passwordunmask.min.js new file mode 100644 index 0000000000000..5687ff9e76dfb --- /dev/null +++ b/lib/form/amd/build/passwordunmask.min.js @@ -0,0 +1 @@ +define(["jquery","core/templates"],function(a,b){var c=function(b){this.wrapperSelector='[data-passwordunmask="wrapper"][data-passwordunmaskid="'+b+'"]',this.wrapper=a(this.wrapperSelector),this.editorSpace=this.wrapper.find('[data-passwordunmask="editor"]'),this.editLink=this.wrapper.find('a[data-passwordunmask="edit"]'),this.editInstructions=this.wrapper.find('[data-passwordunmask="instructions"]'),this.displayValue=this.wrapper.find('[data-passwordunmask="displayvalue"]');var c=a(this.wrapper.find("noscript").text());this.inputField=c.filter("input"),this.inputField.attr("type","hidden"),this.editorSpace.append(this.inputField),this.wrapper.find("noscript").remove(),this.editInstructions.attr("id")||this.editInstructions.attr("id",b+"_instructions"),this.editInstructions.hide(),this.setDisplayValue(),this.addListeners()};return c.prototype.addListeners=function(){return this.wrapper.on("click keypress",'[data-passwordunmask="edit"]',a.proxy(function(b){"keypress"===b.type&&13!==b.keyCode||(b.stopImmediatePropagation(),b.preventDefault(),"hidden"!==this.inputField.attr("type")?"click"===b.type||a(b.relatedTarget).is(":input")?this.turnEditingOff(!1):this.turnEditingOff(!0):this.turnEditingOn())},this)),this.wrapper.on("click keypress",'[data-passwordunmask="unmask"]',a.proxy(function(a){"keypress"===a.type&&13!==a.keyCode||(a.stopImmediatePropagation(),a.preventDefault(),this.wrapper.data("unmasked",!this.wrapper.data("unmasked")),this.setDisplayValue())},this)),this.wrapper.on("keydown","input",a.proxy(function(a){"keydown"===a.type&&13!==a.keyCode||(a.stopImmediatePropagation(),a.preventDefault(),this.turnEditingOff(!0))},this)),this},c.prototype.checkFocusOut=function(b){this.isEditing()&&window.setTimeout(a.proxy(function(){var c=b.relatedTarget||document.activeElement;this.wrapper.has(a(c)).length||this.turnEditingOff(!a(c).is(":input,a"))},this),100)},c.prototype.passwordVisible=function(){return!!this.wrapper.data("unmasked")},c.prototype.isEditing=function(){return"hidden"!==this.inputField.attr("type")},c.prototype.turnEditingOn=function(){return this.passwordVisible()?this.inputField.attr("type","text"):this.inputField.attr("type","password"),this.editInstructions.length&&(this.inputField.attr("aria-describedby",this.editInstructions.attr("id")),this.editInstructions.show()),this.wrapper.attr("data-passwordunmask-visible",1),this.editLink.hide(),this.inputField.focus().select(),a("body").on("focusout",this.wrapperSelector,a.proxy(this.checkFocusOut,this)),this},c.prototype.turnEditingOff=function(b){return a("body").off("focusout",this.wrapperSelector,this.checkFocusOut),this.inputField.attr("type","hidden").attr("aria-describedby",null),this.editInstructions.hide(),this.wrapper.removeAttr("data-passwordunmask-visible"),this.editLink.show(),this.setDisplayValue(),b&&this.editLink.focus(),this},c.prototype.getDisplayValue=function(){return this.inputField.val()},c.prototype.setDisplayValue=function(){this.isEditing()&&(this.wrapper.data("unmasked")?this.inputField.attr("type","text"):this.inputField.attr("type","password"));var c=this.getDisplayValue();return c&&this.wrapper.data("unmasked")?this.displayValue.text(c):(c||(c=""),b.render("core_form/element-passwordunmask-fill",{element:{frozen:this.inputField.is("[readonly]"),value:c,valuechars:c.split("")}}).done(a.proxy(function(a,c){this.displayValue.html(a),b.runTemplateJS(c)},this))),this},c}); \ No newline at end of file diff --git a/lib/form/amd/src/passwordunmask.js b/lib/form/amd/src/passwordunmask.js new file mode 100644 index 0000000000000..8354f47cb74a5 --- /dev/null +++ b/lib/form/amd/src/passwordunmask.js @@ -0,0 +1,286 @@ +// 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 . + +/** + * Password Unmask functionality. + * + * @module core_form/passwordunmask + * @package core_form + * @class passwordunmask + * @copyright 2016 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.2 + */ +define(['jquery', 'core/templates'], function($, Template) { + + /** + * Constructor for PasswordUnmask. + * + * @param {String} elementid The element to apply the PasswordUnmask to + */ + var PasswordUnmask = function(elementid) { + // Setup variables. + this.wrapperSelector = '[data-passwordunmask="wrapper"][data-passwordunmaskid="' + elementid + '"]'; + this.wrapper = $(this.wrapperSelector); + this.editorSpace = this.wrapper.find('[data-passwordunmask="editor"]'); + this.editLink = this.wrapper.find('a[data-passwordunmask="edit"]'); + this.editInstructions = this.wrapper.find('[data-passwordunmask="instructions"]'); + this.displayValue = this.wrapper.find('[data-passwordunmask="displayvalue"]'); + + // Move and convert the input field to the editor, then remove the noscript. + // We only want a single input field. + var noscriptContent = $(this.wrapper.find('noscript').text()); + this.inputField = noscriptContent.filter('input'); + this.inputField.attr('type', 'hidden'); + this.editorSpace.append(this.inputField); + this.wrapper.find('noscript').remove(); + + if (!this.editInstructions.attr('id')) { + this.editInstructions.attr('id', elementid + '_instructions'); + } + this.editInstructions.hide(); + + this.setDisplayValue(); + + // Add the listeners. + this.addListeners(); + }; + + /** + * Add the event listeners required for PasswordUnmask. + * + * @method addListeners + * @return {PasswordUnmask} + * @chainable + */ + PasswordUnmask.prototype.addListeners = function() { + this.wrapper.on('click keypress', '[data-passwordunmask="edit"]', $.proxy(function(e) { + if (e.type === 'keypress' && e.keyCode !== 13) { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + + if (this.inputField.attr('type') !== 'hidden') { + // Only focus on the edit link if the event was not a click, and the new target is not an input field. + if (e.type !== 'click' && !$(e.relatedTarget).is(':input')) { + this.turnEditingOff(true); + } else { + this.turnEditingOff(false); + } + } else { + this.turnEditingOn(); + } + }, this)); + + this.wrapper.on('click keypress', '[data-passwordunmask="unmask"]', $.proxy(function(e) { + if (e.type === 'keypress' && e.keyCode !== 13) { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + + // Toggle the data attribute. + this.wrapper.data('unmasked', !this.wrapper.data('unmasked')); + + this.setDisplayValue(); + }, this)); + + this.wrapper.on('keydown', 'input', $.proxy(function(e) { + if (e.type === 'keydown' && e.keyCode !== 13) { + return; + } + + e.stopImmediatePropagation(); + e.preventDefault(); + + this.turnEditingOff(true); + }, this)); + + return this; + }; + + /** + * Check whether focus was lost from the PasswordUnmask and turn editing off if required. + * + * @method checkFocusOut + * @param {EventFacade} e The EventFacade generating the suspsected Focus Out + */ + PasswordUnmask.prototype.checkFocusOut = function(e) { + if (!this.isEditing()) { + // Ignore - not editing. + return; + } + + window.setTimeout($.proxy(function() { + // Firefox does not have the focusout event. Instead jQuery falls back to the 'blur' event. + // The blur event does not have a relatedTarget, so instead we use a timeout and the new activeElement. + var relatedTarget = e.relatedTarget || document.activeElement; + if (this.wrapper.has($(relatedTarget)).length) { + // Ignore, some part of the element is still active. + return; + } + + // Only focus on the edit link if the new related target is not an input field or anchor. + this.turnEditingOff(!$(relatedTarget).is(':input,a')); + }, this), 100); + }; + + /** + * Whether the password is currently visible (unmasked). + * + * @method passwordVisible + * @return {Boolean} True if the password is unmasked + */ + PasswordUnmask.prototype.passwordVisible = function() { + return !!this.wrapper.data('unmasked'); + }; + + /** + * Whether the user is currently editing the field. + * + * @method isEditing + * @return {Boolean} True if edit mode is enabled + */ + PasswordUnmask.prototype.isEditing = function() { + return this.inputField.attr('type') !== 'hidden'; + }; + + /** + * Enable the editing functionality. + * + * @method turnEditingOn + * @return {PasswordUnmask} + * @chainable + */ + PasswordUnmask.prototype.turnEditingOn = function() { + if (this.passwordVisible()) { + this.inputField.attr('type', 'text'); + } else { + this.inputField.attr('type', 'password'); + } + + if (this.editInstructions.length) { + this.inputField.attr('aria-describedby', this.editInstructions.attr('id')); + this.editInstructions.show(); + } + + this.wrapper.attr('data-passwordunmask-visible', 1); + + this.editLink.hide(); + this.inputField + .focus() + .select(); + + // Note, this cannot be added as a delegated listener on init because Firefox does not support the FocusOut + // event (https://bugzilla.mozilla.org/show_bug.cgi?id=687787) and the blur event does not identify the + // relatedTarget. + // The act of focusing the this.inputField means that in Firefox the focusout will be triggered on blur of the edit + // link anchor. + $('body').on('focusout', this.wrapperSelector, $.proxy(this.checkFocusOut, this)); + + return this; + }; + + /** + * Disable the editing functionality, optionally focusing on the edit link. + * + * @method turnEditingOff + * @param {Boolean} focusOnEditLink Whether to focus on the edit link after disabling the editor + * @return {PasswordUnmask} + * @chainable + */ + PasswordUnmask.prototype.turnEditingOff = function(focusOnEditLink) { + $('body').off('focusout', this.wrapperSelector, this.checkFocusOut); + this.inputField + // Hide the field again. + .attr('type', 'hidden') + + // Ensure that the aria-describedby is removed. + .attr('aria-describedby', null); + + this.editInstructions.hide(); + + // Remove the visible attr. + this.wrapper.removeAttr('data-passwordunmask-visible'); + + this.editLink.show(); + this.setDisplayValue(); + + if (focusOnEditLink) { + this.editLink.focus(); + } + + return this; + }; + + /** + * Get the currently value. + * + * @method getDisplayValue + * @return {String} + */ + PasswordUnmask.prototype.getDisplayValue = function() { + return this.inputField.val(); + }; + + /** + * Set the currently value in the display, taking into account the current settings. + * + * @method setDisplayValue + * @return {PasswordUnmask} + * @chainable + */ + PasswordUnmask.prototype.setDisplayValue = function() { + if (this.isEditing()) { + if (this.wrapper.data('unmasked')) { + this.inputField.attr('type', 'text'); + } else { + this.inputField.attr('type', 'password'); + } + } + + // Update the display value. + // Note: This must always be updated. + // The unmask value can be changed whilst editing and the editing can then be disabled. + var value = this.getDisplayValue(); + if (value && this.wrapper.data('unmasked')) { + // There is a value, and we will show it. + this.displayValue.text(value); + } else { + if (!value) { + value = ""; + } + // There is a value, but it will be disguised. + // We use the passwordunmask-fill to allow modification of the fill and to ensure that the display does not + // change as the page loads the JS. + Template.render('core_form/element-passwordunmask-fill', { + element: { + frozen: this.inputField.is('[readonly]'), + value: value, + valuechars: value.split(''), + }, + }).done($.proxy(function(html, js) { + this.displayValue.html(html); + + Template.runTemplateJS(js); + }, this)); + } + + return this; + }; + + return PasswordUnmask; +}); diff --git a/lib/form/passwordunmask.php b/lib/form/passwordunmask.php index 73bee9d211df7..db18e882be4a6 100644 --- a/lib/form/passwordunmask.php +++ b/lib/form/passwordunmask.php @@ -52,7 +52,6 @@ class MoodleQuickForm_passwordunmask extends MoodleQuickForm_password { * or an associative array */ public function __construct($elementName=null, $elementLabel=null, $attributes=null) { - global $CFG; // no standard mform in moodle should allow autocomplete of passwords if (empty($attributes)) { $attributes = array('autocomplete'=>'off'); @@ -63,6 +62,7 @@ public function __construct($elementName=null, $elementLabel=null, $attributes=n $attributes .= ' autocomplete="off" '; } } + $this->_persistantFreeze = true; parent::__construct($elementName, $elementLabel, $attributes); $this->setType('passwordunmask'); @@ -79,25 +79,15 @@ public function MoodleQuickForm_passwordunmask($elementName=null, $elementLabel= } /** - * Returns HTML for password form element. + * Function to export the renderer data in a format that is suitable for a mustache template. * - * @return string + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return stdClass|array */ - function toHtml() { - global $PAGE; + public function export_for_template(renderer_base $output) { + $context = parent::export_for_template($output); + $context['valuechars'] = array_fill(0, strlen($context['value']), 'x'); - if ($this->_flagFrozen) { - return $this->getFrozenHtml(); - } else { - $unmask = get_string('unmaskpassword', 'form'); - //Pass id of the element, so that unmask checkbox can be attached. - $attributes = array('formid' => $this->getAttribute('id'), - 'checkboxlabel' => $unmask, - 'checkboxname' => $this->getAttribute('name')); - $PAGE->requires->yui_module('moodle-form-passwordunmask', 'M.form.passwordunmask', - array($attributes)); - return $this->_getTabs() . '_getAttrString($this->_attributes) . ' />'; - } + return $context; } - } diff --git a/lib/form/templatable_form_element.php b/lib/form/templatable_form_element.php index 2f27d2b341b46..844a90ee6be27 100644 --- a/lib/form/templatable_form_element.php +++ b/lib/form/templatable_form_element.php @@ -69,6 +69,7 @@ public function export_for_template(renderer_base $output) { // Special wierd named property. $context['frozen'] = !empty($this->_flagFrozen); + $context['hardfrozen'] = !empty($this->_flagFrozen) && empty($this->_persistantFreeze); // Other attributes. $otherattributes = []; diff --git a/lib/form/templates/element-passwordunmask-fill.mustache b/lib/form/templates/element-passwordunmask-fill.mustache new file mode 100644 index 0000000000000..56360d18913fa --- /dev/null +++ b/lib/form/templates/element-passwordunmask-fill.mustache @@ -0,0 +1,56 @@ +{{! + 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/element-passwordunmask-fill + + The fill for a passwordunmask form element. + + The purpose of this template is to render the fill for a passwordunmask element. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * element + * valuechars (optional) + + Example context (json): + { + "element": { + "valuechars": [ + "E", + "x", + "a", + "m", + "p", + "l", + "e" + ] + } + } + +}} + +{{# element.valuechars }}•{{/ element.valuechars }} +{{^ element.valuechars }}{{! + }}{{^ element.frozen }}{{# str }} novalueclicktoset, form {{/ str }}{{/ element.frozen }}{{! + }}{{# element.frozen }}{{# str }} novalue, form {{/ str }}{{/ element.frozen }}{{! +}}{{/ element.valuechars }} + diff --git a/lib/form/templates/element-passwordunmask.mustache b/lib/form/templates/element-passwordunmask.mustache new file mode 100644 index 0000000000000..8634e54da1da7 --- /dev/null +++ b/lib/form/templates/element-passwordunmask.mustache @@ -0,0 +1,93 @@ +{{! + 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/element-passwordunmask + + Moodle passwordunmask form element template. + + The purpose of this template is to render a passwordunmask form element. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * element + * id + * name + * value + * size + + Example context (json): + { + "element": { + "id": "example_password_unmask", + "name": "example", + "value": "Password1!", + "size": 40 + } + } + +}} +{{< core_form/element-template }} + {{$ element }} +
+ + + + + + {{^ element.frozen }} + {{# str }} passwordunmaskinstructions, form {{/ str }} + {{/ element.frozen }} + + + {{^ element.frozen }} + + {{/ element.frozen }} + {{> core_form/element-passwordunmask-fill }} + {{^ element.frozen }} + {{# pix }} t/passwordunmask-edit, core, {{ edithint }}{{/ pix }} + + {{/ element.frozen }} + + {{# pix }} t/passwordunmask-reveal, core, {{ edithint }}{{/ pix }} + + +
+ {{/ element }} +{{/ core_form/element-template }} +{{# js }} +require(['core_form/passwordunmask'], function(PasswordUnmask) { + new PasswordUnmask("{{ element.id }}"); +}); +{{/ js }} diff --git a/lib/form/templates/element-template.mustache b/lib/form/templates/element-template.mustache new file mode 100644 index 0000000000000..9463064f2e905 --- /dev/null +++ b/lib/form/templates/element-template.mustache @@ -0,0 +1,64 @@ +{{! + 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/element-template + + Moodle form element wrapper template. + + The purpose of this template is to wrap a form element. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * label + * helpbutton + * error + * element + * id + * name + + Example context (json): + { + "label": "Password", + "error": "No password set", + "element": { + "id": "example_password_unmask", + "name": "example" + } + } + +}} +
+
+ + {{{ helpbutton }}} +
+
+ {{# error }} + + {{{ error }}} + + {{/ error }} + {{$ element }} + + {{/ element }} +
+
diff --git a/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask-debug.js b/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask-debug.js index 482296886716b..2acf8344c5bc1 100644 --- a/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask-debug.js +++ b/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask-debug.js @@ -1,46 +1,10 @@ YUI.add('moodle-form-passwordunmask', function (Y, NAME) { -var PASSWORDUNMASK = function() { - PASSWORDUNMASK.superclass.constructor.apply(this, arguments); -}; - -Y.extend(PASSWORDUNMASK, Y.Base, { - // Initialize checkbox if id is passed. - initializer: function(params) { - if (params && params.formid) { - this.add_checkbox(params.formid, params.checkboxlabel, params.checkboxname); - } - }, - - // Create checkbox for unmasking password. - add_checkbox: function(elementid, checkboxlabel, checkboxname) { - var node = Y.one('#' + elementid); - - // Retaining unmask div from previous implementation. - var unmaskdiv = Y.Node.create('
'); - - // Add checkbox for unmasking to unmaskdiv. - var unmaskchb = Y.Node.create(''); - unmaskdiv.appendChild(unmaskchb); - // Attach event using static javascript function for unmasking password. - unmaskchb.on('click', function() { - window.unmaskPassword(elementid); - }); - - // Add label for checkbox to unmaskdiv. - var unmasklabel = Y.Node.create(''); - unmaskdiv.appendChild(unmasklabel); - - // Insert unmask div in the same div as password input. - node.get('parentNode').insert(unmaskdiv, node.get('lastNode')); - } -}); - M.form = M.form || {}; -M.form.passwordunmask = function(params) { - return new PASSWORDUNMASK(params); +M.form.passwordunmask = function() { + Y.log("The moodle-form-passwordunmask module has been deprecated. " + + "Please use the core_forum/passwordunmask amd module instead.", 'moodle-form-passwordunmask', 'warn'); }; -}, '@VERSION@', {"requires": ["node", "base"]}); +}, '@VERSION@', {"requires": []}); diff --git a/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask-min.js b/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask-min.js index 1c79dd42ba030..d1ffeb68dc811 100644 --- a/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask-min.js +++ b/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask-min.js @@ -1 +1 @@ -YUI.add("moodle-form-passwordunmask",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.extend(n,e.Base,{initializer:function(e){e&&e.formid&&this.add_checkbox(e.formid,e.checkboxlabel,e.checkboxname)},add_checkbox:function(t,n,r){var i=e.one("#"+t),s=e.Node.create('
'),o=e.Node.create('');s.appendChild(o),o.on("click",function(){window.unmaskPassword(t)});var u=e.Node.create('");s.appendChild(u),i.get("parentNode").insert(s,i.get("lastNode"))}}),M.form=M.form||{},M.form.passwordunmask=function(e){return new n(e)}},"@VERSION@",{requires:["node","base"]}); +YUI.add("moodle-form-passwordunmask",function(e,t){M.form=M.form||{},M.form.passwordunmask=function(){}},"@VERSION@",{requires:[]}); diff --git a/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask.js b/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask.js index 482296886716b..d1c1242d101aa 100644 --- a/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask.js +++ b/lib/form/yui/build/moodle-form-passwordunmask/moodle-form-passwordunmask.js @@ -1,46 +1,8 @@ YUI.add('moodle-form-passwordunmask', function (Y, NAME) { -var PASSWORDUNMASK = function() { - PASSWORDUNMASK.superclass.constructor.apply(this, arguments); -}; - -Y.extend(PASSWORDUNMASK, Y.Base, { - // Initialize checkbox if id is passed. - initializer: function(params) { - if (params && params.formid) { - this.add_checkbox(params.formid, params.checkboxlabel, params.checkboxname); - } - }, - - // Create checkbox for unmasking password. - add_checkbox: function(elementid, checkboxlabel, checkboxname) { - var node = Y.one('#' + elementid); - - // Retaining unmask div from previous implementation. - var unmaskdiv = Y.Node.create('
'); - - // Add checkbox for unmasking to unmaskdiv. - var unmaskchb = Y.Node.create(''); - unmaskdiv.appendChild(unmaskchb); - // Attach event using static javascript function for unmasking password. - unmaskchb.on('click', function() { - window.unmaskPassword(elementid); - }); - - // Add label for checkbox to unmaskdiv. - var unmasklabel = Y.Node.create(''); - unmaskdiv.appendChild(unmasklabel); - - // Insert unmask div in the same div as password input. - node.get('parentNode').insert(unmaskdiv, node.get('lastNode')); - } -}); - M.form = M.form || {}; -M.form.passwordunmask = function(params) { - return new PASSWORDUNMASK(params); +M.form.passwordunmask = function() { }; -}, '@VERSION@', {"requires": ["node", "base"]}); +}, '@VERSION@', {"requires": []}); diff --git a/lib/form/yui/src/passwordunmask/js/passwordunmask.js b/lib/form/yui/src/passwordunmask/js/passwordunmask.js index 56097ea1dccc7..e617441070a2a 100644 --- a/lib/form/yui/src/passwordunmask/js/passwordunmask.js +++ b/lib/form/yui/src/passwordunmask/js/passwordunmask.js @@ -1,41 +1,5 @@ -var PASSWORDUNMASK = function() { - PASSWORDUNMASK.superclass.constructor.apply(this, arguments); -}; - -Y.extend(PASSWORDUNMASK, Y.Base, { - // Initialize checkbox if id is passed. - initializer: function(params) { - if (params && params.formid) { - this.add_checkbox(params.formid, params.checkboxlabel, params.checkboxname); - } - }, - - // Create checkbox for unmasking password. - add_checkbox: function(elementid, checkboxlabel, checkboxname) { - var node = Y.one('#' + elementid); - - // Retaining unmask div from previous implementation. - var unmaskdiv = Y.Node.create('
'); - - // Add checkbox for unmasking to unmaskdiv. - var unmaskchb = Y.Node.create(''); - unmaskdiv.appendChild(unmaskchb); - // Attach event using static javascript function for unmasking password. - unmaskchb.on('click', function() { - window.unmaskPassword(elementid); - }); - - // Add label for checkbox to unmaskdiv. - var unmasklabel = Y.Node.create(''); - unmaskdiv.appendChild(unmasklabel); - - // Insert unmask div in the same div as password input. - node.get('parentNode').insert(unmaskdiv, node.get('lastNode')); - } -}); - M.form = M.form || {}; -M.form.passwordunmask = function(params) { - return new PASSWORDUNMASK(params); +M.form.passwordunmask = function() { + Y.log("The moodle-form-passwordunmask module has been deprecated. " + + "Please use the core_forum/passwordunmask amd module instead.", 'moodle-form-passwordunmask', 'warn'); }; diff --git a/lib/form/yui/src/passwordunmask/meta/passwordunmask.json b/lib/form/yui/src/passwordunmask/meta/passwordunmask.json index 1830917887d34..185a757fae525 100644 --- a/lib/form/yui/src/passwordunmask/meta/passwordunmask.json +++ b/lib/form/yui/src/passwordunmask/meta/passwordunmask.json @@ -1,8 +1,5 @@ { "moodle-form-passwordunmask": { - "requires": [ - "node", - "base" - ] + "requires": [] } } diff --git a/pix/t/passwordunmask-edit.png b/pix/t/passwordunmask-edit.png new file mode 100644 index 0000000000000000000000000000000000000000..52d985c97d0fa8efabdf8e2065a4561e494571a6 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=%3?!FCJ6-`&(g8jpu0VPw4!~Ucv<4{6Q4-`A z%pj0Z(9l2s{B7N)4xpHYr;B4q1!FQpV~3c)$txEb7x5T7h(zzM(4KJWg@*}4k(Je~ u19SRV43_hrOJpcK5W+Mgi +]> \ No newline at end of file diff --git a/pix/t/passwordunmask-reveal.png b/pix/t/passwordunmask-reveal.png new file mode 100644 index 0000000000000000000000000000000000000000..166e7919a0b55b3b121da81d76bfabf18d2606e9 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1Oj01c^T!HjV5<$wUZ*4$r0wqCy z!3+Wl1`YuU1r7c4*Y7`n|GlzQQx{Oq*VDx@ghQ6~U?XFb0mFd}e5`^uPyD|=E9Cj1 z{ME0R=7oP{jJW#f=63Gt#Pwq74`Y4LRn1t;TwR-ZFf5trOyFa-8KLVR*YtO;T6OI0 aFXmkjS!>_RUT6mz%;4$j=d#Wzp$Pzzj9sw+ literal 0 HcmV?d00001 diff --git a/pix/t/passwordunmask-reveal.svg b/pix/t/passwordunmask-reveal.svg new file mode 100644 index 0000000000000..64c12770ca43e --- /dev/null +++ b/pix/t/passwordunmask-reveal.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/theme/boost/templates/core_admin/setting_configpasswordunmask.mustache b/theme/boost/templates/core_admin/setting_configpasswordunmask.mustache index b7e755cd58cac..d5325540c22dc 100644 --- a/theme/boost/templates/core_admin/setting_configpasswordunmask.mustache +++ b/theme/boost/templates/core_admin/setting_configpasswordunmask.mustache @@ -15,43 +15,55 @@ along with Moodle. If not, see . }} {{! - Setting configpasswordunmask. -}} -
- -
-
-{{#js}} -(function() { - var id = '{{id}}'; - var unmaskid = id + 'unmask'; - var unmaskdivid = id + 'unmaskdiv'; - var unmaskstr = {{#quote}}{{#str}}unmaskpassword, form{{/str}}{{/quote}}; - var is_ie = (navigator.userAgent.toLowerCase().indexOf("msie") != -1); - - document.getElementById(id).setAttribute("autocomplete", "off"); + @template core_admin/setting_configpasswordunmask - var unmaskdiv = document.getElementById(unmaskdivid); + Admin password unmask setting template. - var unmaskchb = document.createElement("input"); - unmaskchb.setAttribute("type", "checkbox"); - unmaskchb.setAttribute("id", unmaskid); - unmaskchb.onchange = function() {unmaskPassword(id);}; - unmaskdiv.appendChild(unmaskchb); + Context variables required for this template: + * name - form element name + * size - form element size + * value - form element value + * id - element id - var unmasklbl = document.createElement("label"); - unmasklbl.innerHTML = unmaskstr; - if (is_ie) { - unmasklbl.setAttribute("htmlFor", unmaskid); - } else { - unmasklbl.setAttribute("for", unmaskid); + Example context (json): + { + "name": "test", + "id": "test0", + "size": "8", + "value": "secret" } - unmaskdiv.appendChild(unmasklbl); - - if (is_ie) { - // Ugly hack to work around the famous onchange IE bug. - unmaskchb.onclick = function() {this.blur();}; - unmaskdiv.onclick = function() {this.blur();}; - } -})() +}} + +{{#js}} +require(['core_form/passwordunmask'], function(PasswordUnmask) { + new PasswordUnmask("{{ id }}"); +}); {{/js}} diff --git a/theme/boost/templates/core_form/element-passwordunmask.mustache b/theme/boost/templates/core_form/element-passwordunmask.mustache index a759af1a5ef4e..0c2e75694a038 100644 --- a/theme/boost/templates/core_form/element-passwordunmask.mustache +++ b/theme/boost/templates/core_form/element-passwordunmask.mustache @@ -1,12 +1,94 @@ -{{> core_form/element-password }} -{{^element.frozen}} +{{! + 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/element-passwordunmask + + Moodle passwordunmask form element template. + + The purpose of this template is to render a passwordunmask form element. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * element + * id + * name + * value + * size + + Example context (json): + { + "element": { + "id": "example_password_unmask", + "name": "example", + "value": "Password1!", + "size": 40 + } + } + +}} +{{< core_form/element-template }} + {{$ element }} + + + + + + + {{^ element.frozen }} + + {{/ element.frozen }} + {{> core_form/element-passwordunmask-fill }} + {{^ element.frozen }} + {{# pix }} t/passwordunmask-edit, core, {{# str }} passwordunmaskedithint, form {{/ str }}{{/ pix }} + + {{/ element.frozen }} + + {{# pix }} t/passwordunmask-reveal, core, {{# str }} passwordunmaskrevealhint, form {{/ str }}{{/ pix }} + + + + + {{/ element }} +{{/ core_form/element-template }} {{#js}} -require(['core/yui'], function(Y) { - Y.use('moodle-form-passwordunmask', function() { - M.form.passwordunmask({ formid: {{#quote}}{{element.id}}{{/quote}}, - checkboxlabel: {{#quote}}{{#str}}unmaskpassword, form{{/str}}{{/quote}}, - checkboxname: {{#quote}}{{element.name}}{{/quote}} }); - }); +require(['core_form/passwordunmask'], function(PasswordUnmask) { + new PasswordUnmask("{{ element.id }}"); }); -{{/js}} -{{/element.frozen}} +{{/ js }} diff --git a/theme/bootstrapbase/less/moodle/forms.less b/theme/bootstrapbase/less/moodle/forms.less index a75cd9ec46e79..58d48015e777e 100644 --- a/theme/bootstrapbase/less/moodle/forms.less +++ b/theme/bootstrapbase/less/moodle/forms.less @@ -524,3 +524,9 @@ input[size] { textarea[data-auto-rows] { overflow-x: hidden; } + +div[data-passwordunmask="wrapper"] { + height: 30px; + line-height: 30px; + margin-bottom: 10px; +} diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index 2747ac00fbf0f..c40957d8d8635 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -14349,6 +14349,11 @@ input[size] { textarea[data-auto-rows] { overflow-x: hidden; } +div[data-passwordunmask="wrapper"] { + height: 30px; + line-height: 30px; + margin-bottom: 10px; +} body.modal-open { overflow: hidden; }