Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*******************************************************************************
- MaskedPassword.js Converts password fields into "masked" password fields
- ------------------------------------------------------------------------------
- Adapted from FormTools.addPasswordMasking
- Info/Docs http://www.brothercake.com/site/resources/scripts/formtools/
- ------------------------------------------------------------------------------
- *******************************************************************************/
- //masked password constructor
- function MaskedPassword(passfield, symbol)
- {
- //if the browser is unsupported, silently fail
- //[pre-DOM1 browsers generally, and Opera 8 specifically]
- if(typeof document.getElementById == 'undefined'
- || typeof document.styleSheets == 'undefined') { return false; }
- //or if the passfield doesn't exist, silently fail
- if(passfield == null) { return false; }
- //save the masking symbol
- this.symbol = symbol;
- //identify Internet Explorer for a couple of conditions
- this.isIE = typeof document.uniqueID != 'undefined';
- //delete any default value for security (and simplicity!)
- passfield.value = '';
- passfield.defaultValue = '';
- //create a context wrapper, so that we have sole context for modifying the content
- //(ie. we can go context.innerHTML = replacement; without catering for
- // anything else that's there besides the password field itself)
- //and give it a distinctive and underscored name, to prevent conflict
- passfield._contextwrapper = this.createContextWrapper(passfield);
- //create a fullmask flag which will be used from events to determine
- //whether to mask the entire password (true)
- //or to stop at the character limit (false)
- //most events set the flag before being called, except for onpropertychange
- //which uses whatever the setting currently is
- //this used to be an argument, but onpropertychange fires from our modifications
- //as well as manual input, so the blur event that's supposed to have a fullmask
- //triggers in turn an onpropertychange which doesn't, with the end result
- //that fullmask never works; so by doing it like this, we can set it to
- //true from the blur event and that will persist through all subsequent
- //onpropertychange events, until another manual event changes it back to false
- this.fullmask = false;
- //for the code that converts a password field into a masked field
- //I used to have lovely clean elegant code for most browsers, then
- //ugly horrible hacky code for IE; but since the hacky approach does
- //actually work for everyone, and we have to have it here whatever,
- //we may as well just use it for everyone, and get a big saving in code-size
- //it also means we'll get total behavioral consistency, in terms of
- //the preservation (or rather, lack thereof) of existing event handlers
- //save a reference to the wrapper because the passfield reference will be lost soon
- var wrapper = passfield._contextwrapper;
- //create the HTML for the hidden field
- //using the name from the original password field
- var hiddenfield = '<input type="hidden" name="' + passfield.name + '">';
- //copy the HTML from the password field to create the new plain-text field
- var textfield = this.convertPasswordFieldHTML(passfield);
- //write the hiddenfield and textfield HTML into the wrapper, replacing what's there
- wrapper.innerHTML = hiddenfield + textfield;
- //grab back the passfield reference back and save it back to passfield
- //then add the masked-password class
- passfield = wrapper.lastChild;
- passfield.className += ' masked';
- //try to disable autocomplete for this field
- //to prevent firefox from remembering and subsequently offering
- //a menu of useless masking strings, things like "✫✫✫✫✫✫✫f"
- //which of course can't be decoded, they'll just be represented by whatever
- //is in the realfield value at the time, ie. a completely unrelated value!
- passfield.setAttribute('autocomplete', 'off');
- //now grab the hidden field reference,
- //saving it as a property of the passfield
- passfield._realfield = wrapper.firstChild;
- //restore its contextwrapper reference
- passfield._contextwrapper = wrapper;
- //limit the caret position so that you can only edit or select from the end
- //you can't add, edit or select from the beginning or middle of the field
- //otherwise we can't track which masked characters represent which letters
- //(far from ideal I know, but I can't see how else to know
- //which masking symbols represent which letters if you edit from the middle..?)
- this.limitCaretPosition(passfield);
- //save a reference to this
- var self = this;
- //then apply the core events to the visible field
- this.addListener(passfield, 'change', function(e)
- {
- self.fullmask = false;
- self.doPasswordMasking(self.getTarget(e));
- });
- this.addListener(passfield, 'input', function(e)
- {
- self.fullmask = false;
- self.doPasswordMasking(self.getTarget(e));
- });
- //no fullmask setting for onpropertychange (as noted above)
- this.addListener(passfield, 'propertychange', function(e)
- {
- self.doPasswordMasking(self.getTarget(e));
- });
- //for keyup, don't respond to the tab or shift key, otherwise when you [shift/]tab
- //into the field the keyup will cause the fully-masked password to become partially masked
- //which is inconsistent with the mouse since it doesn't happen when you click focus
- //so it's only supposed to happen when you actually edit it; we'll also prevent it
- //from happening in response to arrows keys as well, for visual completeness!
- //and from the other modifiers keys, just cos it feels like the right thing to do
- this.addListener(passfield, 'keyup', function(e)
- {
- if(!/^(9|1[678]|224|3[789]|40)$/.test(e.keyCode.toString()))
- {
- self.fullmask = false;
- self.doPasswordMasking(self.getTarget(e));
- }
- });
- //the blur event completely masks the input password
- //(as opposed to leaving the last n characters plain during input)
- this.addListener(passfield, 'blur', function(e)
- {
- self.fullmask = true;
- self.doPasswordMasking(self.getTarget(e));
- });
- //so between those events we get completely rock-solid behavior
- //with enough redundency to ensure that all input paths are covered
- //and no flickering of text between states :-)
- //force the parent form to reset onload
- //thereby clearing all values after soft refreh
- this.forceFormReset(passfield);
- //return true for success
- return true;
- }
- //associated utility methods
- MaskedPassword.prototype =
- {
- //implement password masking for a textbox event
- doPasswordMasking : function(textbox)
- {
- //create the plain password string
- var plainpassword = '';
- //if we already have a real field value we need to work out the difference
- //between that and the value that's in the input field
- if(textbox._realfield.value != '')
- {
- //run through the characters in the input string
- //and build the plain password out of the corresponding characters
- //from the real field, and any plain characters in the input
- for(var i=0; i<textbox.value.length; i++)
- {
- if(textbox.value.charAt(i) == this.symbol)
- {
- plainpassword += textbox._realfield.value.charAt(i);
- }
- else
- {
- plainpassword += textbox.value.charAt(i);
- }
- }
- }
- //if there's no real field value then we're doing this for the first time
- //so whatever's in the input field is the entire plain password
- else
- {
- plainpassword = textbox.value;
- }
- //get the masked version of the plainpassword, according to fullmask
- //and passing the textbox reference so we have its symbol and limit properties
- var maskedstring = this.encodeMaskedPassword(plainpassword, this.fullmask, textbox);
- //then we modify the textfield values
- //if (AND ONLY IF) one of the values are now different from the original
- //(this condition is essential to avoid infinite repetition
- // leading to stack overflow, from onpropertychange in IE
- // [value changes, fires event, which changes value, which fires event ...])
- //we check both instead of just one, so that we can still allow the action
- //of changing the mask without modifying the password itself
- if(textbox._realfield.value != plainpassword || textbox.value != maskedstring)
- {
- //copy the plain password to the real field
- textbox._realfield.value = plainpassword;
- //then write the masked value to the original textbox
- textbox.value = maskedstring;
- }
- },
- //convert a plain-text password to a masked password
- encodeMaskedPassword : function(passwordstring, fullmask, textbox)
- {
- //the character limit is nominally 1
- //this is how many characters to leave plain at the end
- //but if the fullmask flag is true the limit is zero
- //and the password will be fully masked
- var characterlimit = fullmask === true ? 0 : 1;
- //create the masked password string then iterate
- //through he characters in the plain password
- for(var maskedstring = '', i=0; i<passwordstring.length; i++)
- {
- //if we're below the masking limit,
- //add a masking symbol to represent this character
- if(i < passwordstring.length - characterlimit)
- {
- maskedstring += this.symbol;
- }
- //otherwise just copy across the real character
- else
- {
- maskedstring += passwordstring.charAt(i);
- }
- }
- //return the final masked string
- return maskedstring;
- },
- //create a context wrapper element around a password field
- createContextWrapper : function(passfield)
- {
- //create the wrapper and add its class
- //it has to be an inline element because we don't know its context
- var wrapper = document.createElement('span');
- //enforce relative positioning
- wrapper.style.position = 'relative';
- //insert the wrapper directly before the passfield
- passfield.parentNode.insertBefore(wrapper, passfield);
- //then move the passfield inside it
- wrapper.appendChild(passfield);
- //return the wrapper reference
- return wrapper;
- },
- //force a form to reset its values, so that soft-refresh does not retain them
- forceFormReset : function(textbox)
- {
- //find the parent form from this textbox reference
- //(which may not be a textbox, but that's fine, it just a reference name!)
- while(textbox)
- {
- if(/form/i.test(textbox.nodeName)) { break; }
- textbox = textbox.parentNode;
- }
- //if the reference is not a form then the textbox wasn't wrapped in one
- //so in that case we'll just have to abandon what we're doing here
- if(!/form/i.test(textbox.nodeName)) { return null; }
- //otherwise bind a load event to call the form's reset method
- //we have to defer until load time for IE or it won't work
- //because IE renders the page with empty fields
- //and then adds their values retrospectively!
- //(but in other browsers we can use DOMContentLoaded;
- // and the load listener function takes care of that split)
- this.addSpecialLoadListener(function() { textbox.reset(); });
- //return the now-form reference
- return textbox;
- },
- //copy the HTML from a password field to a plain text field,
- //we have to convert the field this way because of Internet Explorer
- //because it doesn't support setting or changing the type of an input
- convertPasswordFieldHTML : function(passfield, addedattrs)
- {
- //start the HTML for a text field
- var textfield = '<input';
- //now run through the password fields' specified attributes
- //and copy across each one into the textfield HTML
- //*except* for its name and type, and any formtools underscored attributes
- //we need to exclude the name because we'll define that separately
- //depending on the situation, and obviously the type, and formtools attributes
- //because we control them and their meaning in separate conditions too
- for(var fieldattributes = passfield.attributes,
- j=0; j<fieldattributes.length; j++)
- {
- //we have to check .specified otherwise we'll get back every single attribute
- //that the element might possibly have! which is what IE puts in the attributes
- //collection, with default values for unspecified attributes
- if(fieldattributes[j].specified && !/^(_|type|name)/.test(fieldattributes[j].name))
- {
- textfield += ' ' + fieldattributes[j].name + '="' + fieldattributes[j].value + '"';
- }
- }
- //now add the type of "text" to the end, plus an autocomplete attribute, and close it
- //we add autocomplete attribute for added safety, though it probably isnt necessary,
- //since browsers won't offer to remember it anywway, because the field has no name
- //this uses HTML4 empty-element syntax, but we don't need to distinguish by spec
- //because the browser's internal representations will generally be identical anyway
- textfield += ' type="text" autocomplete="off">';
- //return the finished textfield HTML
- return textfield;
- },
- //this crap is what it takes to force the caret in a textbox to stay at the end
- //I'd really rather not to do this, but it's the only way to have reliable encoding
- limitCaretPosition : function(textbox)
- {
- //create a null timer reference and start function
- var timer = null, start = function()
- {
- //prevent multiple instances
- if(timer == null)
- {
- //IE uses this range stuff
- if(this.isIE)
- {
- //create an interval that continually force the position
- //as long as the field has the focus
- timer = window.setInterval(function()
- {
- //we can only force position to the end
- //because there's no way to know whether there's a selection
- //or just a single caret point, because the range methods
- //we could use to determine that don't work on created fields
- //(they generate "Invalid argument" errors)
- var range = textbox.createTextRange(),
- valuelength = textbox.value.length,
- character = 'character';
- range.moveEnd(character, valuelength);
- range.moveStart(character, valuelength);
- range.select();
- //not so fast as to be a major CPU hog
- //but fast enough to do the job effectively
- }, 100);
- }
- //other browsers have these selection properties
- else
- {
- //create an interval that continually force the position
- //as long as the field has the focus
- timer = window.setInterval(function()
- {
- //allow selection from or position at the end
- //otherwise force position to the end
- var valuelength = textbox.value.length;
- if(!(textbox.selectionEnd == valuelength && textbox.selectionStart <= valuelength))
- {
- textbox.selectionStart = valuelength;
- textbox.selectionEnd = valuelength;
- }
- //ditto
- }, 100);
- }
- }
- },
- //and a stop function
- stop = function()
- {
- window.clearInterval(timer);
- timer = null;
- };
- //add events to start and stop the timer
- this.addListener(textbox, 'focus', function() { start(); });
- this.addListener(textbox, 'blur', function() { stop(); });
- },
- //add an event listener
- //this is deliberately not called "addEvent" so that we can
- //compress the name, which would otherwise also effect "addEventListener"
- addListener : function(eventnode, eventname, eventhandler)
- {
- if(typeof document.addEventListener != 'undefined')
- {
- return eventnode.addEventListener(eventname, eventhandler, false);
- }
- else if(typeof document.attachEvent != 'undefined')
- {
- return eventnode.attachEvent('on' + eventname, eventhandler);
- }
- },
- //add a special load listener, split between
- //window load for IE and DOMContentLoaded for others
- //this is only used by the force form reset method, which wants that split
- addSpecialLoadListener : function(eventhandler)
- {
- //we specifically need a browser condition here, not a feature test
- //because we know specifically what should be given to who
- //and that doesn't match general support for these constructs
- if(this.isIE)
- {
- return window.attachEvent('onload', eventhandler);
- }
- else
- {
- return document.addEventListener('DOMContentLoaded', eventhandler, false);
- }
- },
- //get an event target by sniffing for its property name
- //(assuming here that e is already a cross-model reference
- //as it is from addListener because attachEvent in IE
- //automatically provides a corresponding event argument)
- getTarget : function(e)
- {
- //just in case!
- if(!e) { return null; }
- //otherwise return the target
- return e.target ? e.target : e.srcElement;
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement