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',
   ...