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
This commit is contained in:
Andrew Nicols
2016-10-24 09:24:39 +08:00
parent ac157b0bbe
commit ca25005c69
27 changed files with 791 additions and 228 deletions
@@ -34,40 +34,35 @@
}
}}
<div class="form-password">
<input type="password" name="{{name}}" size="{{size}}" id="{{id}}" value="{{value}}">
<div class="unmask" id="{{id}}unmaskdiv"></div>
<span data-passwordunmask="wrapper" data-passwordunmaskid="{{ id }}">
<noscript>
<!-- Backwards compatability for Behat -->
<input type="password"
name="{{ name }}"
id="{{ id }}"
value="{{ value }}"
size="{{ size }}"
>
</noscript>
<span class="visibleifjs">
<span data-passwordunmask="editor">
<!-- The input in the noscript will be moved here as part of the page load -->
</span>
<a href="#" data-passwordunmask="edit" title="{{ edithint }}">
<span data-passwordunmask="displayvalue">{{> core_form/element-passwordunmask-fill }}</span>
{{# pix }} t/passwordunmask-edit, core, {{# str }} passwordunmaskedithint, form {{/ str }}{{/ pix }}
</a>
<a href="#" data-passwordunmask="unmask" title="{{ unmaskhint }}">
{{# pix }} t/passwordunmask-reveal, core, {{# str }} passwordunmaskrevealhint, form {{/ str }}{{/ pix }}
</a>
<span data-passwordunmask="instructions" class="form-text text-muted" style="display: none;">
{{# str }} passwordunmaskinstructions, form {{/ str }}
</span>
</span>
</span>
</div>
{{#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}}
+1
View File
@@ -34,3 +34,4 @@ downloadoptions,core_table
downloadtsv,core_table
downloadxhtml,core_table
invalidpersistent,core_competency
revealpassword,core_form
+8 -1
View File
@@ -51,10 +51,14 @@ $string['nomethodforaddinghelpbutton'] = 'There is no method for adding a help b
$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['timeunit'] = 'Time unit';
$string['timing'] = 'Timing';
$string['unmaskpassword'] = 'Unmask';
$string['year'] = 'Year';
// Deprecated since 3.2.
$string['revealpassword'] = 'Reveal';
+4 -6
View File
@@ -2406,13 +2406,11 @@ class admin_setting_configpasswordunmask extends admin_setting_configtext {
}
/**
* 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;
+4
View File
@@ -228,6 +228,10 @@ class behat_field_manager {
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')) {
@@ -186,6 +186,11 @@ XPATH
'filemanager' => <<<XPATH
.//*[@data-fieldtype = 'filemanager' or @data-fieldtype = 'filepicker']
/descendant::input[@id = //label[contains(normalize-space(string(.)), %locator%)]/@for]
XPATH
,
'passwordunmask' => <<<XPATH
.//*[@data-passwordunmask='wrapper']
/descendant::input[@id = //label[contains(normalize-space(string(.)), %locator%)]/@for]
XPATH
],
];
@@ -0,0 +1,64 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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 = <<<JS
require(["jquery"], function($) {
var wrapper = $(document.getElementById("{$id}")).closest('[data-passwordunmask="wrapper"]');
wrapper.find('[data-passwordunmask="edit"]').trigger("click");
});
JS;
$this->session->executeScript($js);
}
$this->field->setValue($value);
}
}
+1
View File
@@ -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});
+286
View File
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Password Unmask functionality.
*
* @module core_form/passwordunmask
* @package core_form
* @class passwordunmask
* @copyright 2016 Andrew Nicols <[email protected]>
* @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;
});
+8 -18
View File
@@ -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 @@ class MoodleQuickForm_passwordunmask extends MoodleQuickForm_password {
$attributes .= ' autocomplete="off" ';
}
}
$this->_persistantFreeze = true;
parent::__construct($elementName, $elementLabel, $attributes);
$this->setType('passwordunmask');
@@ -79,25 +79,15 @@ class MoodleQuickForm_passwordunmask extends MoodleQuickForm_password {
}
/**
* 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() . '<input' . $this->_getAttrString($this->_attributes) . ' />';
}
return $context;
}
}
+1
View File
@@ -69,6 +69,7 @@ trait templatable_form_element {
// Special wierd named property.
$context['frozen'] = !empty($this->_flagFrozen);
$context['hardfrozen'] = !empty($this->_flagFrozen) && empty($this->_persistantFreeze);
// Other attributes.
$otherattributes = [];
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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"
]
}
}
}}
<span>
{{# element.valuechars }}&bull;{{/ element.valuechars }}
{{^ element.valuechars }}<em>{{!
}}{{^ element.frozen }}{{# str }} novalueclicktoset, form {{/ str }}{{/ element.frozen }}{{!
}}{{# element.frozen }}{{# str }} novalue, form {{/ str }}{{/ element.frozen }}{{!
}}</em>{{/ element.valuechars }}
</span>
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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 }}
<div data-passwordunmask="wrapper" data-passwordunmaskid="{{ element.id }}">
<noscript>
<!-- Backwards compatability for Behat -->
<input type="password"
{{# element.frozen }}readonly{{/ element.frozen }}
{{^ element.hardfrozen}} name="{{ element.name }}"{{/ element.hardfrozen }}
id="{{ element.id }}"
value="{{element.value }}"
size="{{ element.size }}"
{{# error }}
autofocus aria-describedby="id_error_{{ element.name }}"
{{/ error }}
{{{ attributes }}}
>
</noscript>
<span class="visibleifjs">
<span data-passwordunmask="editor">
<!-- The input in the noscript will be moved here as part of the page load -->
<span data-passwordunmask="instructions" class="editinstructions">
{{^ element.frozen }}
{{# str }} passwordunmaskinstructions, form {{/ str }}
{{/ element.frozen }}
</span>
</span>
{{^ element.frozen }}
<a href="#" data-passwordunmask="edit" title="{{ edithint }}">
{{/ element.frozen }}
<span data-passwordunmask="displayvalue">{{> core_form/element-passwordunmask-fill }}</span>
{{^ element.frozen }}
{{# pix }} t/passwordunmask-edit, core, {{ edithint }}{{/ pix }}
</a>
{{/ element.frozen }}
<a href="#" data-passwordunmask="unmask" title="{{ unmaskhint }}">
{{# pix }} t/passwordunmask-reveal, core, {{ edithint }}{{/ pix }}
</a>
</span>
</div>
{{/ element }}
{{/ core_form/element-template }}
{{# js }}
require(['core_form/passwordunmask'], function(PasswordUnmask) {
new PasswordUnmask("{{ element.id }}");
});
{{/ js }}
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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"
}
}
}}
<div id="fitem_{{ element.id }}" class="fitem fitem_{{ element.type }}">
<div class="fitemtitle">
<label for="{{element.id}}">{{{ label }}}</label>
{{{ helpbutton }}}
</div>
<div class="felement {{# error }} error{{/ error }} f{{ element.type }}">
{{# error }}
<span class="error" tabindex="0" id="id_error_{{ element.name }}">
{{{ error }}}
</span>
{{/ error }}
{{$ element }}
<!-- Element goes here -->
{{/ element }}
</div>
</div>
@@ -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('<div id="' + elementid + 'unmaskdiv" class="unmask"></div>');
// Add checkbox for unmasking to unmaskdiv.
var unmaskchb = Y.Node.create('<input id="' + elementid + 'unmask" type="checkbox" name="' +
checkboxname + 'unmask">');
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('<label for="' + elementid + 'unmask">' + checkboxlabel + '</label>');
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": []});
@@ -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('<div id="'+t+'unmaskdiv" class="unmask"></div>'),o=e.Node.create('<input id="'+t+'unmask" type="checkbox" name="'+r+'unmask">');s.appendChild(o),o.on("click",function(){window.unmaskPassword(t)});var u=e.Node.create('<label for="'+t+'unmask">'+n+"</label>");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:[]});
@@ -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('<div id="' + elementid + 'unmaskdiv" class="unmask"></div>');
// Add checkbox for unmasking to unmaskdiv.
var unmaskchb = Y.Node.create('<input id="' + elementid + 'unmask" type="checkbox" name="' +
checkboxname + 'unmask">');
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('<label for="' + elementid + 'unmask">' + checkboxlabel + '</label>');
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": []});
+3 -39
View File
@@ -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('<div id="' + elementid + 'unmaskdiv" class="unmask"></div>');
// Add checkbox for unmasking to unmaskdiv.
var unmaskchb = Y.Node.create('<input id="' + elementid + 'unmask" type="checkbox" name="' +
checkboxname + 'unmask">');
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('<label for="' + elementid + 'unmask">' + checkboxlabel + '</label>');
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');
};
@@ -1,8 +1,5 @@
{
"moodle-form-passwordunmask": {
"requires": [
"node",
"base"
]
"requires": []
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

+3
View File
@@ -0,0 +1,3 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M5.2 11H12v1H4.2l1-1zm1.4-8.4L0 9.1V12h2.8l6.6-6.6-2.8-2.8zm4.8.8l-1 1-2.8-2.8 1-1c.8-.8 2.1-.8 2.8 0 .8.8.8 2 0 2.8z" fill="#999"/></svg>

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

+3
View File
@@ -0,0 +1,3 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M11.4 9.4L9.9 7.9c-.1-.1-.2-.1-.3-.2.4-.8.7-1.6.7-2.5C10.3 2.3 8 0 5.2 0 2.3 0 0 2.3 0 5.2s2.3 5.2 5.2 5.2c.9 0 1.7-.2 2.4-.6.1 0 .1.1.2.2l1.5 1.5c.6.6 1.5.6 2.1 0s.6-1.5 0-2.1zM5.2 7.3C4 7.3 3 6.4 3 5.2 3 4 4 3 5.2 3c1.2 0 2.2 1 2.2 2.2-.1 1.2-1 2.1-2.2 2.1z" fill="#999"/></svg>

After

Width:  |  Height:  |  Size: 584 B

@@ -15,43 +15,55 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
Setting configpasswordunmask.
@template core_admin/setting_configpasswordunmask
Admin password unmask setting template.
Context variables required for this template:
* name - form element name
* size - form element size
* value - form element value
* id - element id
Example context (json):
{
"name": "test",
"id": "test0",
"size": "8",
"value": "secret"
}
}}
<div class="form-password">
<input type="password" name="{{name}}" size="{{size}}" id="{{id}}" value="{{value}}" class="form-control d-inline-block">
<div class="unmask" id="{{id}}unmaskdiv"></div>
<span data-passwordunmask="wrapper" data-passwordunmaskid="{{ id }}">
<noscript>
<!-- Backwards compatability for Behat -->
<input type="password"
name="{{ name }}"
id="{{ id }}"
value="{{ value }}"
size="{{ size }}"
class="form-control d-inline-block"
>
</noscript>
<span class="visibleifjs">
<span data-passwordunmask="editor">
<!-- The input in the noscript will be moved here as part of the page load -->
</span>
<a href="#" data-passwordunmask="edit" title="{{ edithint }}">
<span data-passwordunmask="displayvalue">{{> core_form/element-passwordunmask-fill }}</span>
{{# pix }} t/passwordunmask-edit, core, {{# str }} passwordunmaskedithint, form {{/ str }}{{/ pix }}
</a>
<a href="#" data-passwordunmask="unmask" title="{{ unmaskhint }}">
{{# pix }} t/passwordunmask-reveal, core, {{# str }} passwordunmaskrevealhint, form {{/ str }}{{/ pix }}
</a>
<span data-passwordunmask="instructions" class="form-text text-muted" style="display: none;">
{{# str }} passwordunmaskinstructions, form {{/ str }}
</span>
</span>
</span>
</div>
{{#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}}
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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 }}
<span data-passwordunmask="wrapper" data-passwordunmaskid="{{ element.id }}">
<noscript>
<!-- Backwards compatability for Behat -->
<input type="password"
{{# element.frozen }}readonly{{/ element.frozen }}
{{^ element.hardfrozen}} name="{{ element.name }}"{{/ element.hardfrozen }}
id="{{ element.id }}"
value="{{ element.value }}"
class="form-control d-inline-block {{# error }}form-control-danger{{/ error }}"
size="{{ element.size }}"
{{# error }}
autofocus aria-describedby="id_error_{{ element.name }}"
{{/ error }}
{{{ attributes }}}
>
</noscript>
<span class="visibleifjs">
<span data-passwordunmask="editor">
<!-- The input in the noscript will be moved here as part of the page load -->
</span>
{{^ element.frozen }}
<a href="#" data-passwordunmask="edit" title="{{ edithint }}">
{{/ element.frozen }}
<span data-passwordunmask="displayvalue">{{> core_form/element-passwordunmask-fill }}</span>
{{^ element.frozen }}
{{# pix }} t/passwordunmask-edit, core, {{# str }} passwordunmaskedithint, form {{/ str }}{{/ pix }}
</a>
{{/ element.frozen }}
<a href="#" data-passwordunmask="unmask" title="{{ unmaskhint }}">
{{# pix }} t/passwordunmask-reveal, core, {{# str }} passwordunmaskrevealhint, form {{/ str }}{{/ pix }}
</a>
<span data-passwordunmask="instructions" class="form-text text-muted" style="display: none;">
{{^ element.frozen }}
{{# str }} passwordunmaskinstructions, form {{/ str }}
{{/ element.frozen }}
</span>
</span>
</span>
{{/ 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 }}
@@ -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;
}
+5
View File
@@ -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;
}