/******************************************************************************* * dPassword v0.7 - delayed password masking (iPhone style) * jQuery plugin * * Usage: * e.g. $('input[type=password]').dPassword(options) * The options parameter is optional and can have the following optional entries: * delay: Number of seconds after which to hide input. Defaults to 1. * observeForm: Whether to automatically deactivate when parent form is submitted (default: true). * form: Form element different from parent form to observe for submitting (forces observeForm to true if set). * cloakingCharacter: Character to replace entered characters with. Defaults to the bullet (•). * onChange: Handler when password has been changed. * onStateChange: Handler when masking behaviour changes. * switchToPasswordType: Whether to switch input field back to password type on blur (looks bad in IE). * showIcon: Show a lock icon allowing the user to toggle masking behaviour (defaults to true). * See further options * ICON_TITLE_ON, ICON_TITLE_OFF, ICON_PATH, ICON_STYLES, ICON_STYLES_ON, ICON_STYLES_OFF * for customization. * * Licensed under MIT License. * * Copyright (c) 2009 Stefan Ullrich, DECAF° | http://decaf.de * Dirk Schürjohann, DECAF° | http://decaf.de * Julian Dreissig * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR * THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * Known Issues: - view will not follow cursor if textfield is too small. * * @requires jQuery Library >= 1.3.2 (may work with older versions) */ (function() { jQuery.fn.dPassword = function(options) { // support multiple elements if (this.length > 1) { this.each(function() {jQuery(this).dPassword(options);}); return this; } var options = jQuery.extend(defaultOptions, options); options.cloakingCharacter = options.cloakingCharacter.charAt(0); var _input = $(this); var _value = null, _previousInputValue = null, _timeout = null, _previousSelection = null, _options = null, _observing = false, _form = null, _toggleIcon = null, _keysDown = {}, _inputFieldTypes = {}; // register event listeners registerHandlers(_input); if (options.observeForm || options.form) { if (!_form) _form = options.form ? $(options.form) : _input.closest('form'); if (_form) _form.bind("submit.dPassword", function(){deactivate(true);}); } // create/handle toggle icon if (options.showIcon) { _toggleIcon = jQuery("
").insertAfter(_input); _toggleIcon.css({backgroundImage: "url(" + (options.iconPath || options.ICON_PATH) + ")"}); _toggleIcon.css(options.ICON_STYLES); _toggleIcon.bind("click", function() { _observing ? deactivate() : activate(); _input.focus(); }); } // TODO: public methods // TODO: possible to overwrite val() to outside? activate(); return this; // ------- method declarations follow ---------- /** * Returns the current value of the password field. */ function getValue() { return _observing ? _value : _input.val(); } /** * Switches to active mode. Will be automatically called on initialization. * Use getValue() to retrieve field value once activated. */ function activate() { _observing = true; _value = _input.val(); _cloakInput(); if (_toggleIcon) _switchToggleIcon(true); if (options.onStateChange) options.onStateChange(_observing, this, _toggleIcon); } /** * Deactivates dPassword temporarily/permanently and switches field back to normal password behaviour * to e.g. perform DOM operations or value retrieval. * IMPORTANT: If "temporarily" parameter is set to true will auto-reactivates on any input. */ function deactivate(temporarily) { if (_observing) { if (_timeout) { clearTimeout(_timeout); _timeout = null; } var selection = _getFieldSelection(_input); jQuery.browser.msie ? _switchInputTypeIE("password") : _input.get(0).setAttribute("type", "password"); // override jQuery's behaviour _input.val(getValue()); if (document.activeElement && document.activeElement == _input) _setFieldSelection(_input, selection); if (temporarily !== true) { _observing = false; if (_toggleIcon) _switchToggleIcon(false); if (options.onStateChange) options.onStateChange(_observing, this, _toggleIcon); } } } function _keyDownHandler(event) { if (_observing) { if (!(_isSpecialKey(event.keyCode) || event.metaKey || event.ctrlKey)) { var keyCode = null; for (var keyCode in _keysDown) { _afterInputHandler(keyCode); } _storeSelection(); if (!keyCode) _cloakInput(); if (event.keyCode > 10) _keysDown[event.keyCode] = true; } else { _storeSelection(); if (_timeout) { clearTimeout(_timeout); _timeout = null; _cloakInput(); } } } } function _keyUpHandler(event) { if (_observing) { if (event.type == "paste") { setTimeout(_afterInputHandler, 0); return; } if (_isSpecialKey(event.keyCode) || event.metaKey || event.ctrlKey) return; if (_keysDown[event.keyCode] || event.keyCode < 11) _afterInputHandler(event); } else { var value = _input.val(); if (value != _previousInputValue) { _previousInputValue = value; if (options.onChange) options.onChange(getValue()); } } } function _afterInputHandler(keyCode) { delete _keysDown[keyCode]; var value = _input.val(); var selection = _getFieldSelection(_input); if (_previousInputValue != value) { var sStart = _previousSelection[0], sEnd = _previousSelection[1], sLength = _previousSelection[2], lengthDifference = value.length - _value.length, // > 0: characters added newValue; if (lengthDifference < 0 && sLength == 0) { // single character deletion if (sStart == selection[0]) { // forward deletion newValue = _value.substring(0, sStart) + _value.substring(sEnd + 1); } else { // has to be backward deletion newValue = _value.substring(0, selection[0]) + _value.substring(sEnd); } } else { // a selection has been replaced/deleted newValue = _value.substring(0, sStart) + value.substring(sStart, selection[1]) + _value.substring(sEnd); } _value = newValue; if (options.onChange) options.onChange(getValue()); if (_timeout) { clearTimeout(_timeout); _timeout = null; } if (lengthDifference >= 0) { // leave newly written part uncloaked _cloakInput([sStart + 1, selection[1]]); _timeout = setTimeout(_cloakInput, options.delay * 1000); } } else { _previousSelection = selection; } } function registerHandlers(element) { element.bind("keydown.dPassword", _keyDownHandler); element.bind("keyup.dPassword paste.dPassword", _keyUpHandler); element.bind("select.dPassword focus.dPassword", _storeSelection); if (options.switchToPasswordType) { _input.bind("blur.dPassword", function() {deactivate(true);}); } } function _storeSelection() { if (_observing) _previousSelection = _getFieldSelection(_input); } function _cloakInput(keepRange) { var selection = _getFieldSelection(_input); var value = _input.val(); if (keepRange) { _input.val(value.substring(0, keepRange[0] - 1 ).replace(/./g, options.cloakingCharacter) + value.substring(keepRange[0] - 1, keepRange[1]) + value.substring(keepRange[1]).replace(/./g, options.cloakingCharacter)); } else { _input.val(value.replace(/./g, options.cloakingCharacter)); if (_input.attr("type") != "text") { if (jQuery.browser.msie) { _switchInputTypeIE("text"); } else { _input.get(0).setAttribute("type", "text"); // override jQuery's behaviour } _input.attr("autocomplete", "off"); } } if (document.activeElement && document.activeElement == _input.get(0)) _setFieldSelection(_input, selection); _previousInputValue = _input.val(); } function _switchInputTypeIE(toType) { // create input field (or retrieve from cache) with new type var newInput = _inputFieldTypes[toType]; if (!newInput) { var tempDiv = _input.clone().wrap('
').parent(); if (toType == "password") { tempDiv.html(tempDiv.html().replace(/>/, 'type="password">')); } else { tempDiv.html(tempDiv.html().replace(/type="?password"?/, 'type="text"')); } newInput = tempDiv.children(); registerHandlers(newInput); } // update field newInput.css('width', (_input.get(0).clientWidth - 2*parseInt(_input.get(0).currentStyle.padding, 10)) + "px"); newInput.css('height', (_input.get(0).clientHeight - 2*parseInt(_input.get(0).currentStyle.padding, 10)) + "px"); // fix different widths for password and text inputs in IE var oldInput = _input; oldInput.get(0).replaceNode(newInput.get(0)); // jQuery's replaceWith method gobbles the event handlers, apparently. _input = newInput; _input.val(oldInput.val()); // first time here: store elements in cache if (!_inputFieldTypes) { _inputFieldTypes = { password: (toType == "password") ? newInput : oldInput, text: (toType == "password") ? oldInput : newInput }; } } function _switchToggleIcon(state) { if (state) { _toggleIcon.css(options.ICON_STYLES_OFF).css({className: "dpassword-lock-closed"}); _toggleIcon.attr('title', options.ICON_TITLE_ON); } else { _toggleIcon.css(options.ICON_STYLES_ON).css({className: "dpassword-lock-closed"}); _toggleIcon.attr('title', options.ICON_TITLE_OFF); } } }; var defaultOptions = { delay: 1, observeForm: true, form: null, cloakingCharacter: (navigator.platform == "MacIntel") ? '\u2022' : '\u25CF', onChange: null, onStateChange: null, showIcon: true, switchToPasswordType: !jQuery.browser.msie, /* * Default styles and behaviours for lock icon, see showIcon option. * Override at will. */ ICON_TITLE_ON: "Delayed masking active, click here to switch off.", ICON_TITLE_OFF: "Click to activate delayed masking of input.", ICON_STYLES: { display: "inline", position: "absolute", width: "16px", height: "16px", margin: "-10px 0 0 -12px", overflow: "hidden", cursor: "pointer", backgroundRepeat: "no-repeat" }, ICON_PATH: "lock.gif", // set to your position of icon ICON_STYLES_OFF: { backgroundPosition: "0 0" }, ICON_STYLES_ON: { backgroundPosition: "0 -16px" } }; function _getFieldSelection(element) { if (document.selection) { var range = document.selection.createRange(); var length = range.text.length; range.moveStart('character', -element.val().length); // Move selection start to 0 position var cursorPos = range.text.length; // The caret position is now the selection length return [cursorPos - length, cursorPos, length]; } else { var el = element.get(0); return [el.selectionStart, el.selectionEnd, el.selectionEnd - el.selectionStart]; } } function _setFieldSelection(element, selection) { var el = element.get(0); if (document.selection) { var range = el.createTextRange(); range.collapse(); range.moveStart('character', selection[0]); range.moveEnd('character', selection[1] - selection[0]); range.select(); } else { el.selectionStart = selection[0]; el.selectionEnd = selection[1]; } } function _isSpecialKey(keyCode) { // TODO: Need to check OS? Windows key? return (keyCode >= 9 && keyCode <= 20) || (keyCode >= 33 && keyCode <= 40) || keyCode == 224; } })();