Password Meter
by Jason Steinshouer
HTML
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootswatch/3.0.2/yeti/bootstrap.min.css">
<script src="//code.jquery.com/jquery.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zxcvbn/4.2.0/zxcvbn.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<body>
<div class="container">
<div class="container">
<form class="form-signin" id="form-signin" role="form" method="post" action="#">
<h2>Sign Up</h2>
<input type="text" name="firstName" class="form-control input-lg" placeholder="First Name" required autofocus value="">
<input type="text" name="lastName" class="form-control input-lg" placeholder="Last Name" required value="">
<input type="text" name="username" class="form-control input-lg" placeholder="Username" required value="">
<div class="input-group">
<input :type="passwordFieldType" name="password" id="password" v-model="password" class="form-control input-lg" placeholder="Password" required>
<span class="input-group-addon pointer" @click="showPassword = !showPassword">
<span class="glyphicon" :class="{'glyphicon-eye-open': !showPassword, 'glyphicon-eye-close': showPassword}" aria-hidden="true"></span>
</span>
</div>
<password-meter :password="password" target="#password"></password-meter>
<button class="btn btn-lg btn-primary btn-block">Sign Up</button>
</form>
</div>
</div>
</body>
CSS
.form-signin {
max-width: 400px;
padding: 15px;
margin: 0 auto;
}
.form-signin .input-group input#password {
margin-top: 0;
}
.form-signin input,
.form-signin button,
.form-signin .input-group {
margin-top: 15px;
}
.pointer {
cursor: pointer;
}
meter {
/* Reset the default appearance */
margin: 0 auto 1em;
width: 100%;
height: .5em;
/* Applicable only to Firefox */
background: none;
background-color: rgba(0, 0, 0, 0.1);
}
meter::-webkit-meter-bar {
background: none;
background-color: rgba(0, 0, 0, 0.1);
}
meter[value="0"]::-webkit-meter-optimum-value,
meter[value="1"]::-webkit-meter-optimum-value {
background: #007bff;
}
meter[value="2"]::-webkit-meter-optimum-value {
background: #007bff;
}
meter[value="3"]::-webkit-meter-optimum-value {
background: #007bff;
}
meter[value="4"]::-webkit-meter-optimum-value {
background: #007bff;
}
meter[value="1"]::-moz-meter-bar,
meter[value="1"]::-moz-meter-bar {
background: #007bff;
}
meter[value="2"]::-moz-meter-bar {
background: #007bff;
}
meter[value="3"]::-moz-meter-bar {
background: #007bff;
}
meter[value="4"]::-moz-meter-bar {
background: #007bff;
}
meter::-webkit-meter-optimum-value {
transition: width .4s ease-out;
}
#password-strength-meter {
display: inline;
}
#password-strength-meter .glyphicon {
font-size: 1.1em;
margin-left: 5px;
}
#password-strength-meter .glyphicon:hover {
cursor: pointer;
}
JavaScript
Vue.component('password-meter', {
template: ' <div id="password-strength-meter" @click="showInfo">\
<meter max="4" :value="score"></meter> \
<span class="text-muted">Password Strength: {{strength}} \
<span class="glyphicon glyphicon-info-sign" v-if="info"></span> \
</span> \
</div>',
props: ["password", "target", "estimateEndpoint"],
data: function() {
return {
score: 0,
info: ""
}
},
computed: {
strength: function() {
switch (this.score) {
case 0:
case 1:
return "Weak";
break;
case 2:
return "Medium";
break;
case 3:
return "Good";
break;
case 4:
return "Great";
break;
}
}
},
created: function() {
this.debouncedGetEstimate = _.debounce(this.getEstimate, 200);
},
watch: {
password: function() {
this.debouncedGetEstimate();
}
},
methods: {
showInfo: function() {
var vm = this;
if (vm.info) {
$(this.target).popover({
content: function() {
return vm.info;
},
title: vm.strength,
trigger: "manual",
container: 'body'
});
$(this.target).popover("toggle");
}
},
getEstimate: function() {
var estimate = zxcvbn(this.password);
this.score = estimate.score;
if (estimate.feedback.warning && estimate.feedback.warning.length && this.score < 3) {
this.info = estimate.feedback.warning + "\n";
this.info += "\n\n" + estimate.feedback.suggestions.join("\n");
} else if (estimate.feedback.suggestions && estimate.feedback.suggestions.length && this.score < 3) {
this.info = estimate.feedback.suggestions.join("\n");
} else {
this.info = "";
}
$(this.target).popover("destroy");
}
}
});
$(document).ready(function() {
var app = new Vue({
el: '#form-signin',
...