UNPKG

fortify-js

Version:

A password strength indicator.

229 lines (191 loc) 6.53 kB
(function () { /* * constructor * @public * @param {Element} field - the password <input> field that the user is entering their password on * @param {Object} settings - use to overwrite default settings */ this.Fortify = function (field, confirmField, settings) { // default settings var defaults = { allowSubmission: true, callback: null, feedback: true, keyTimeout: 150, progressBar: true }; // props this.field = field; this.confirmField = confirmField; this.form = this.field.form; this.settings = extendDefaults(defaults, settings); this.bar = document.createElement('div'); this.innerBar = document.createElement('div'); this.confirmBar = document.createElement('div'); this.confirmInnerBar = document.createElement('div'); // apply class names to dom elements this.bar.className = 'fortify-bar'; this.innerBar.className = 'fortify fortify-nothing'; this.confirmBar.className = 'fortify-bar'; this.confirmInnerBar.className = 'fortify fortify-nothing'; // insert elements into dom this.bar.appendChild(this.innerBar); this.field.parentNode.insertBefore(this.bar, this.field.nextSibling); this.confirmBar.appendChild(this.confirmInnerBar); this.confirmField.parentNode.insertBefore(this.confirmBar, this.confirmField.nextSibling); // for use in private event handler functions (referenced as event.target.self) this.field.self = this; this.confirmField.self = this; this.form.self = this; // add event listeners this.field.addEventListener('keypress', handleChange); this.field.addEventListener('keyup', handleChange); this.field.addEventListener('keydown', handleChange); this.field.addEventListener('keypress', handleConfirmChange); this.field.addEventListener('keyup', handleConfirmChange); this.field.addEventListener('keydown', handleConfirmChange); this.confirmField.addEventListener('keypress', handleConfirmChange); this.confirmField.addEventListener('keyup', handleConfirmChange); this.confirmField.addEventListener('keydown', handleConfirmChange); this.form.addEventListener('submit', handleSubmit); }; /* * given two objects, will seek to overwrite first object with anything provided in the second object * alternative to Object.assign() which doesn't work in IE apparently * @private * @param {Object} source - source object to be modified * @param {Object} updates - object with updated values * @returns {Object} */ function extendDefaults(source, properties) { for (property in properties) { if (properties.hasOwnProperty(property)) { source[property] = properties[property]; } } return source; } /* * returns a score and string for interface elements to utilize * @private * @param {String} password - the password the user enters into the field * @returns {Object} */ function getPasswordScore(password) { if (!password) return { score: 0, feedback: 'nothing' } var score = 0; var scoreStr; /* add points to score for every unique char score added is lowered with each subsequent use of the same character */ var letterMap = {}; password.split('').forEach(function (letter) { letterMap[letter] = (letterMap[letter] || 0) + 1; score += (5.0 / letterMap[letter]); }); // multiply score based on variations used, e.g. lower and upper, special chars, and numbers var variations = { digits: /\d/.test(password), lower: /[a-z]/.test(password), upper: /[A-Z]/.test(password), nonWords: /\W/.test(password) }, variationPasses = 0; for (var variance in variations) { variationPasses += (variations[variance] === true) ? 1 : 0; } score += (variationPasses - 1) * 10; // set the user-friendly score string if (score === 0) scoreStr = 'nothing'; else if (score > 80) scoreStr = 'strong'; else if (score > 60 && score <= 80) scoreStr = 'good'; else if (score > 30 && score <= 60) scoreStr = 'okay'; else scoreStr = 'weak'; return { number: parseInt(score), feedback: scoreStr }; }; /* * event handler for this.field on keypress, keyup, and keydown * @private * @param {Event} e */ function handleChange(e) { var _ = e.target.self; var timeout; if (timeout) clearTimeout(timeout); timeout = setTimeout(function () { var score = getPasswordScore(_.field.value); if (_.settings.feedback) { _.innerBar.className = 'fortify fortify-' + score.feedback; _.innerBar.textContent = capitalize(score.feedback); if (_.settings.progressBar) { _.innerBar.style.width = score.number + '%'; } } if (_.settings.callback) { _.settings.callback(score.number, score.feedback); } }, _.settings.keyTimeout); } /* * event handler for this.confirmField on keypress, keyup, and keydown * @private * @param {Event} e */ function handleConfirmChange(e) { var _ = e.target.self; var timeout; if (timeout) clearTimeout(timeout); timeout = setTimeout(function () { var score = getPasswordScore(_.field.value); if (_.settings.feedback) { if (!_.confirmField.value) { _.confirmInnerBar.className = 'fortify fortify-nothing'; _.confirmInnerBar.textContent = ''; return; } else if (!_.field.value) { _.confirmInnerBar.className = 'fortify fortify-weak'; _.confirmInnerBar.textContent = 'There is nothing in the password field'; return; } else if (_.field.value === _.confirmField.value) { if (score.number <= 60) { _.confirmInnerBar.className = 'fortify fortify-' + score.feedback; _.confirmInnerBar.textContent = 'Password matches, but it is not particularly good'; } else { _.confirmInnerBar.className = 'fortify fortify-' + score.feedback; _.confirmInnerBar.textContent = 'Password matches'; } } else { _.confirmInnerBar.className = 'fortify fortify-weak'; _.confirmInnerBar.textContent = 'The passwords do not match'; } } }, _.settings.keyTimeout); } /* * event handler for this.form on submit * @private * @param {Event} e */ function handleSubmit(e) { var _ = e.target.self; if (!_.settings.allowSubmission) { var score = getPasswordScore(_.field.value); if (score.number <= 60 || _.field.value != _.confirmField.value) { e.preventDefault(); } } } /* * returns a capitalized string * @private * @param {String} str - the string to be capitalized * @returns {String} */ function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } }());