{"version":3,"file":"passwordunmask.min.js","sources":["../src/passwordunmask.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Password Unmask functionality.\n *\n * @module     core_form/passwordunmask\n * @package    core_form\n * @class      passwordunmask\n * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since      3.2\n */\ndefine(['jquery', 'core/templates'], function($, Template) {\n\n    /**\n     * Constructor for PasswordUnmask.\n     *\n     * @param   {String}    elementid   The element to apply the PasswordUnmask to\n     */\n    var PasswordUnmask = function(elementid) {\n        // Setup variables.\n        this.wrapperSelector = '[data-passwordunmask=\"wrapper\"][data-passwordunmaskid=\"' + elementid + '\"]';\n        this.wrapper = $(this.wrapperSelector);\n        this.editorSpace = this.wrapper.find('[data-passwordunmask=\"editor\"]');\n        this.editLink = this.wrapper.find('a[data-passwordunmask=\"edit\"]');\n        this.editInstructions = this.wrapper.find('[data-passwordunmask=\"instructions\"]');\n        this.displayValue = this.wrapper.find('[data-passwordunmask=\"displayvalue\"]');\n        this.inputFieldLabel = $('label[for=\"' + elementid + '\"]');\n\n        this.inputField = this.editorSpace.find(document.getElementById(elementid));\n        this.inputField.attr('type', 'hidden');\n        this.inputField.removeClass('hiddenifjs');\n\n        if (!this.editInstructions.attr('id')) {\n            this.editInstructions.attr('id', elementid + '_instructions');\n        }\n        this.editInstructions.hide();\n\n        this.setDisplayValue();\n\n        // Add the listeners.\n        this.addListeners();\n    };\n\n    /**\n     * Add the event listeners required for PasswordUnmask.\n     *\n     * @method  addListeners\n     * @return  {PasswordUnmask}\n     * @chainable\n     */\n    PasswordUnmask.prototype.addListeners = function() {\n        this.wrapper.on('click keypress', '[data-passwordunmask=\"edit\"]', $.proxy(function(e) {\n            if (e.type === 'keypress' && e.keyCode !== 13) {\n                return;\n            }\n            e.stopImmediatePropagation();\n            e.preventDefault();\n\n            if (this.inputField.attr('type') !== 'hidden') {\n                // Only focus on the edit link if the event was not a click, and the new target is not an input field.\n                if (e.type !== 'click' && !$(e.relatedTarget).is(':input')) {\n                    this.turnEditingOff(true);\n                } else {\n                    this.turnEditingOff(false);\n                }\n            } else {\n                this.turnEditingOn();\n            }\n        }, this));\n\n        this.wrapper.on('click keypress', '[data-passwordunmask=\"unmask\"]', $.proxy(function(e) {\n            if (e.type === 'keypress' && e.keyCode !== 13) {\n                return;\n            }\n            e.stopImmediatePropagation();\n            e.preventDefault();\n\n            // Toggle the data attribute.\n            this.wrapper.data('unmasked', !this.wrapper.data('unmasked'));\n\n            this.setDisplayValue();\n        }, this));\n\n        this.wrapper.on('keydown', 'input', $.proxy(function(e) {\n            if (e.type === 'keydown' && e.keyCode !== 13) {\n                return;\n            }\n\n            e.stopImmediatePropagation();\n            e.preventDefault();\n\n            this.turnEditingOff(true);\n        }, this));\n\n        this.inputFieldLabel.on('click', $.proxy(function(e) {\n            e.preventDefault();\n\n            this.turnEditingOn();\n        }, this));\n\n        return this;\n    };\n\n    /**\n     * Check whether focus was lost from the PasswordUnmask and turn editing off if required.\n     *\n     * @method  checkFocusOut\n     * @param   {EventFacade}   e       The EventFacade generating the suspsected Focus Out\n     */\n    PasswordUnmask.prototype.checkFocusOut = function(e) {\n        if (!this.isEditing()) {\n            // Ignore - not editing.\n            return;\n        }\n\n        window.setTimeout($.proxy(function() {\n            // Firefox does not have the focusout event. Instead jQuery falls back to the 'blur' event.\n            // The blur event does not have a relatedTarget, so instead we use a timeout and the new activeElement.\n            var relatedTarget = e.relatedTarget || document.activeElement;\n            if (this.wrapper.has($(relatedTarget)).length) {\n                // Ignore, some part of the element is still active.\n                return;\n            }\n\n            // Only focus on the edit link if the new related target is not an input field or anchor.\n            this.turnEditingOff(!$(relatedTarget).is(':input,a'));\n        }, this), 100);\n    };\n\n    /**\n     * Whether the password is currently visible (unmasked).\n     *\n     * @method  passwordVisible\n     * @return  {Boolean}            True if the password is unmasked\n     */\n    PasswordUnmask.prototype.passwordVisible = function() {\n        return !!this.wrapper.data('unmasked');\n    };\n\n    /**\n     * Whether the user is currently editing the field.\n     *\n     * @method  isEditing\n     * @return  {Boolean}            True if edit mode is enabled\n     */\n    PasswordUnmask.prototype.isEditing = function() {\n        return this.inputField.attr('type') !== 'hidden';\n    };\n\n    /**\n     * Enable the editing functionality.\n     *\n     * @method  turnEditingOn\n     * @return  {PasswordUnmask}\n     * @chainable\n     */\n    PasswordUnmask.prototype.turnEditingOn = function() {\n        var value = this.getDisplayValue();\n        if (this.passwordVisible()) {\n            this.inputField.attr('type', 'text');\n        } else {\n            this.inputField.attr('type', 'password');\n        }\n        this.inputField.val(value);\n        this.inputField.attr('size', this.inputField.attr('data-size'));\n\n        if (this.editInstructions.length) {\n            this.inputField.attr('aria-describedby', this.editInstructions.attr('id'));\n            this.editInstructions.show();\n        }\n\n        this.wrapper.attr('data-passwordunmask-visible', 1);\n\n        this.editLink.hide();\n        this.inputField\n            .focus()\n            .select();\n\n        // Note, this cannot be added as a delegated listener on init because Firefox does not support the FocusOut\n        // event (https://bugzilla.mozilla.org/show_bug.cgi?id=687787) and the blur event does not identify the\n        // relatedTarget.\n        // The act of focusing the this.inputField means that in Firefox the focusout will be triggered on blur of the edit\n        // link anchor.\n        $('body').on('focusout', this.wrapperSelector, $.proxy(this.checkFocusOut, this));\n\n        return this;\n    };\n\n    /**\n     * Disable the editing functionality, optionally focusing on the edit link.\n     *\n     * @method  turnEditingOff\n     * @param   {Boolean}       focusOnEditLink     Whether to focus on the edit link after disabling the editor\n     * @return  {PasswordUnmask}\n     * @chainable\n     */\n    PasswordUnmask.prototype.turnEditingOff = function(focusOnEditLink) {\n        $('body').off('focusout', this.wrapperSelector, this.checkFocusOut);\n        var value = this.getDisplayValue();\n        this.inputField\n            // Hide the field again.\n            .attr('type', 'hidden')\n\n            // Ensure that the aria-describedby is removed.\n            .attr('aria-describedby', null);\n        this.inputField.val(value);\n\n        this.editInstructions.hide();\n\n        // Remove the visible attr.\n        this.wrapper.removeAttr('data-passwordunmask-visible');\n\n        // Remove the size attr.\n        this.inputField.removeAttr('size');\n\n        this.editLink.show();\n        this.setDisplayValue();\n\n        if (focusOnEditLink) {\n            this.editLink.focus();\n        }\n\n        return this;\n    };\n\n    /**\n     * Get the currently value.\n     *\n     * @method  getDisplayValue\n     * @return  {String}\n     */\n    PasswordUnmask.prototype.getDisplayValue = function() {\n        return this.inputField.val();\n    };\n\n    /**\n     * Set the currently value in the display, taking into account the current settings.\n     *\n     * @method  setDisplayValue\n     * @return  {PasswordUnmask}\n     * @chainable\n     */\n    PasswordUnmask.prototype.setDisplayValue = function() {\n        var value = this.getDisplayValue();\n        if (this.isEditing()) {\n            if (this.wrapper.data('unmasked')) {\n                this.inputField.attr('type', 'text');\n            } else {\n                this.inputField.attr('type', 'password');\n            }\n            this.inputField.val(value);\n        }\n\n        // Update the display value.\n        // Note: This must always be updated.\n        // The unmask value can be changed whilst editing and the editing can then be disabled.\n        if (value && this.wrapper.data('unmasked')) {\n            // There is a value, and we will show it.\n            this.displayValue.text(value);\n        } else {\n            if (!value) {\n                value = \"\";\n            }\n            // There is a value, but it will be disguised.\n            // We use the passwordunmask-fill to allow modification of the fill and to ensure that the display does not\n            // change as the page loads the JS.\n            Template.render('core_form/element-passwordunmask-fill', {\n                element: {\n                    frozen:     this.inputField.is('[readonly]'),\n                    value:      value,\n                    valuechars: value.split(''),\n                },\n            }).done($.proxy(function(html, js) {\n                this.displayValue.html(html);\n\n                Template.runTemplateJS(js);\n            }, this));\n        }\n\n        return this;\n    };\n\n    return PasswordUnmask;\n});\n"],"names":["define","$","Template","PasswordUnmask","elementid","wrapperSelector","wrapper","this","editorSpace","find","editLink","editInstructions","displayValue","inputFieldLabel","inputField","document","getElementById","attr","removeClass","hide","setDisplayValue","addListeners","prototype","on","proxy","e","type","keyCode","stopImmediatePropagation","preventDefault","relatedTarget","is","turnEditingOff","turnEditingOn","data","checkFocusOut","isEditing","window","setTimeout","activeElement","has","length","passwordVisible","value","getDisplayValue","val","show","focus","select","focusOnEditLink","off","removeAttr","text","render","element","frozen","valuechars","split","done","html","js","runTemplateJS"],"mappings":"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"}