<!DOCTYPE html>
<html ng-app="plunker">

<head>
  <link data-require="bootstrap-css@3.2.0" data-semver="3.2.0" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" />
  <script data-require="angular.js@1.3.0-beta.5" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
  <script data-require="jquery@*" data-semver="2.1.1" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  <script data-require="bootstrap@3.1.1" data-semver="3.1.1" src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>

<body ng-controller="MainCtrl">

  <br>
  <div class="form-group">
    <label for="mask1" class="col-xs-12 control-label">
      <h3>Mask example</h3>
    </label>
    <label for="mask" class="col-xs-3 control-label">Input mixed</label>
    <div class="col-xs-9">
      <input type="text" mask="8;3" ng-model="value1" required id="mask1" class="form-control">
    </div>
  </div>

  <div class="col-xs-12">
    <br>
  </div>

  <div class="form-group">
    <label for="mask2" class="col-xs-3 control-label">Input numeric</label>
    <div class="col-xs-9">
      <input type="text" mask="8;3" numeric ng-model="value2" required id="mask2" class="form-control">
    </div>
  </div>
  
  <div class="col-xs-12">
    <br><div class="col-xs-12">
    <br>
  </div><div class="col-xs-12">
    <br>
  </div>
  </div>
  
  <div class="form-group">
    <label for="mask3" class="col-xs-3 control-label">Example IP Address</label>
    <div class="col-xs-9">
      <input type="text" mask="3;3;3;3" numeric ng-model="value3" required id="mask3" class="form-control">
    </div>
  </div>

</body>

</html>
var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {

});

app.directive('mask', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      var modelTmp = attrs.ngModel + ".tmp";
      var parts = attrs.mask.split(";");
      var strPlaceholder = '';
      var intMaxChars = 0;
      var pauseFormat = false;
      var stepSymbol = "."; //possible signs: [.?*+^$[\]\\(){}|\/-] and space

      // prepare placeholder string
      for (var i = 0; i < parts.length; i++) {
        for (var j = 0; j < parseInt(parts[i]); j++) {
          strPlaceholder += '#';
          if (j < parseInt(parts[i]) - 1) {
            strPlaceholder += ' ';
          }
        }
        intMaxChars += parseInt(parts[i]);
        // at a part's end add additional chars for the step
        if (i < parts.length - 1) {
          strPlaceholder += ' ';
          strPlaceholder += stepSymbol;
          strPlaceholder += ' ';
        }
      }

      //Use HTML5 placeholder attribute if possible
      var placeholderSupport = 'placeholder' in document.createElement("input");
      if (placeholderSupport) attrs.$set("placeholder", strPlaceholder);

      elm.on('keydown', function(e) {
        var strNew = '';
        if (e.keyCode == 8) { //backspace pressed
          if (scope[modelTmp] !== null && scope[modelTmp].length > 0) {
            strNew = scope[modelTmp].substring(0, scope[modelTmp].length - 1);
          }
          var viewValue = formatOutputString(strNew);

          pauseFormat = true; //do not format in $parsers.unshift
          ctrl.$setViewValue(viewValue);
          ctrl.$render();
          scope[modelTmp] = strNew; //write clean string to data
          scope.$digest();
          pauseFormat = false;
          e.preventDefault(); //stop event
        }
      });
      elm.on('blur', function(e) {
        if (scope[modelTmp] === '') {
          pauseFormat = true; //stop formatting in $parsers.unshift
          ctrl.$setViewValue('');
          ctrl.$render();
          scope.$digest();
          pauseFormat = false;
          e.preventDefault(); //stop event
        }
      });
      elm.on('focus', function(e) {
        if (scope[modelTmp] === '') {
          var viewValue = formatOutputString('');

          pauseFormat = true; //stop formatting in $parsers.unshift
          ctrl.$setViewValue(viewValue);
          ctrl.$render();
          scope.$digest();
          pauseFormat = false;
          e.preventDefault(); //stop event
        }
      });

      ctrl.$parsers.unshift(function(viewValue) {
        var strippedViewValue = viewValue;
        var viewValueNew = '';

        strippedViewValue = strippedViewValue.replace(/[^A-Z^a-z^0-9]+/g, '');

        if (attrs.numeric === "") { // allow only numeric chars if attribute is found
          strippedViewValue = strippedViewValue.replace(/\D+/g, '');
        }

        if (pauseFormat) { // blur or focus event clears content if empty (and trigger $parsers.unshift)
          setValidity(false); // as we are sure field is empty it is invalid
          return;
        }

        viewValueNew = formatOutputString(strippedViewValue); // get formatted string

        if (viewValueNew.indexOf('_') != -1) { //set input validity
          setValidity(false);
        } else {
          setValidity(true);
        }

        if (scope[modelTmp] === strippedViewValue) { //'net' content from tmp and viewValue are identical
          var v = viewValue.replace(/\s+/g, '').replace(/([.?*+^$[\]\\(){}|\/-])/g, ''); //remove spaces & symbols
          if (v.length != intMaxChars) { // a different length indicate presence of special characters i.e. '%^?_
            ctrl.$setViewValue(viewValueNew); // so we set newly formatted viewValue once again
            ctrl.$render();
          }
          return viewValueNew; // and then return to avoid loops
        }

        scope[modelTmp] = strippedViewValue; //write clean string to data

        ctrl.$setViewValue(viewValueNew);
        ctrl.$render();

        return viewValueNew;
      });

      function formatOutputString(strippedViewValue) {
        var intSeqNo = 0; //counter for position of the char within the user entered string
        var viewValueNew = '';
        for (var p = 0; p < parts.length; p++) {
          for (var i = 0; i < parseInt(parts[p]); i++) {
            var strPart = strippedViewValue.substr(intSeqNo, parseInt(parts[p]));
            if (i < strPart.length) {
              viewValueNew += strPart[i];
            } else {
              viewValueNew += '_';
            }
            if (i < parseInt(parts[p]) - 1) {
              viewValueNew += ' ';
            }
          }
          intSeqNo += parseInt(parts[p]);
          // at a parts end add symbol for the step
          if (p < parts.length - 1) { // except the last one
            viewValueNew += ' ';
            viewValueNew += stepSymbol;
            viewValueNew += ' ';
          }
        }
        console.log(viewValueNew);
        return viewValueNew;
      }

      function setValidity(isComplete) {
        if (isComplete) {
          ctrl.$setValidity('mask', true);
        } else {
          ctrl.$setValidity('mask', false);
        }
      }
    }
  };
})
/* Styles go here */

.ng-invalid.ng-dirty:focus,
.ng-invalid.ng-pristine:focus {
  outline: 0;
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px rgba(205, 10, 10, 0.6);
}

input.ng-invalid.ng-dirty,
input.ng-invalid.ng-pristine {
  background-color: #fef1ec;
  border-color: #cd0a0a;
  color: #cd0a0a;
}