pyc-website
main website for pyc inc.
git clone https://9o.is/git/pyc-website.git
commit 3c5f30bc6703c5a797629fb2523a9740d51ebc5b parent 6890f8fcb07852033e97032cd8dbe9192f7d7d2f Author: Jul <jul@9o.is> Date: Thu, 3 Apr 2014 02:12:09 -0400 implmented angularjs for alerts and forms; added ngSanitize and ui-mask modules. Diffstat:
15 files changed, 1130 insertions(+), 398 deletions(-)
diff --git a/build.config.js b/build.config.js @@ -63,7 +63,9 @@ module.exports = { "<%= dirs.vendor %>/jquery.bsFormAlerts.min.js", "<%= dirs.vendor %>/liftAjax.js", "<%= dirs.vendor %>/angular1.3.0-beta.3.min.js", - "<%= dirs.vendor %>/ui-bootstrap-tpls-0.10.0.min.js" + "<%= dirs.vendor %>/ui-bootstrap-tpls-0.10.0.min.js", + "<%= dirs.vendor %>/ui-mask.js", + "<%= dirs.vendor %>/ngSanitize.js" ], css: [ ], diff --git a/src/main/scala/com/pyd/model/AtmApplication.scala b/src/main/scala/com/pyd/model/AtmApplication.scala @@ -6,10 +6,7 @@ import lib.RogueMetaRecord import net.liftweb._ import mongodb.record._ import mongodb.record.field._ -import util._ import record.field._ -import common._ -import http.Templates class AtmApplication private () extends MongoRecord[AtmApplication] with ObjectIdPk[AtmApplication] { def meta = AtmApplication @@ -17,7 +14,6 @@ class AtmApplication private () extends MongoRecord[AtmApplication] with ObjectI object name extends StringField(this, 64) { override def validations = valMaxLen(64, "Business name must be 64 characters or less") _ :: - valMinLen(1, "* required") _ :: super.validations } @@ -38,14 +34,12 @@ class AtmApplication private () extends MongoRecord[AtmApplication] with ObjectI object address extends StringField(this, 255) { override def validations = valMaxLen(255, "Business address must be 255 characters or less") _ :: - valMinLen(1, "* required") _ :: super.validations } object city extends StringField(this, 64) { override def validations = valMaxLen(64, "City must be 64 characters or less") _ :: - valMinLen(1, "* required") _ :: super.validations } diff --git a/src/main/scala/com/pyd/model/field/AngularJsField.scala b/src/main/scala/com/pyd/model/field/AngularJsField.scala @@ -1,252 +0,0 @@ -package com.pyd -package model -package field - -import xml._ -import net.liftweb._ -import record.{BaseField, Record, Field, TypedField} -import util._, Helpers._ -import util.CssSel -import json.JsonAST._ -import http._, S.SFuncHolder -import common._ -import js._ -import JsCmds._ -import SHtml._ -import net.liftmodules.extras._ - - - -object AngularJs { - - type NgExpression = String - - val appName: Box[String] = Props.get("ng.app") - - trait NgFieldContainer extends FieldContainer { - override def allFields: Seq[NgField] - } - - trait NgRecord[T <: Record[T]] { - def ngFormControllers: List[NgFormController] - } - - trait NgField extends BaseField { - override def formElemAttrs: Seq[ElemAttr] = Seq( - "name" -> name, - "type" -> formInputType) - - override def displayHtml: NodeSeq = formElemAttrs.foldLeft(<input/>) { - (el, elemAttr) => el % elemAttr - } - - override def noValueErrorMessage: String = name+" is not valid" - - override def notOptionalErrorMessage: String = name+" is required" - - override def helpAsHtml: Box[NodeSeq] = { - def requiredHelp = Text(name+" is required") - //def - - Full(<em></em>) - } - } - - - - trait NgTypedField[T, OwnerType <: Record[OwnerType]] extends Field[T, OwnerType] with NgField { - // TODO: Include type inputs and validations - } - - // TODO: create default NgTypedFields (or should field traits be mixed in record) - - /** Basic Angular Directive */ - trait NgDirective - - /** Angular Directive for inputs or fields. */ - trait NgFieldDirective extends NgField with NgDirective - - trait NgModel extends NgFieldDirective { - override def formElemAttrs: Seq[ElemAttr] = Seq("ng-model" -> name) - } - - trait NgRequired extends NgFieldDirective { - def ngRequired: Boolean - override def formElemAttrs = Seq("ng-required" -> ngRequired.toString) - } - - trait NgMinLength extends NgFieldDirective { - def ngMinLength: Int - override def formElemAttrs = Seq("ng-minlength" -> ngMinLength.toString) - } - - trait NgMaxLength extends NgFieldDirective { - def ngMaxLength: Int - override def formElemAttrs = Seq("ng-maxlength" -> ngMaxLength.toString) - } - - trait NgController extends NgDirective { - def ngControllerName: String - def ngControllerAttrs: Seq[ElemAttr] = Seq("ng-controller" -> ngControllerName) - } - - trait NgForm extends NgDirective with NgSubmit { - def ngFormName: String - def noValidate: Boolean = true - - def ngFormAttrs: Seq[ElemAttr] = Seq("ng-form" -> ngFormName, "novalidate" -> noValidate.toString) - } - - trait NgShow extends NgDirective { - def ngShow: NgExpression - def ngHideDefault: Boolean = true - - def ngShowAttrs: Seq[ElemAttr] = - Seq("ng-show" -> ngShow) ++ - {if(ngHideDefault) Seq("class" -> "ng-hide") else Nil} - } - - trait NgSubmit extends NgDirective { - def ngSubmitFuncName: String - def ngSubmitFuncParams: List[String] = Nil - def ngSubmitCallback = submit - - def submit() // TODO - - def ngSubmitAttrs: Seq[ElemAttr] = - Seq("ng-submit" -> s"$ngSubmitFuncName(${ngSubmitFuncParams.mkString(",")})") - } - - trait NgClass extends NgDirective { - - /** A sequence of class name to expression. */ - def ngClasses: Seq[(String, NgExpression)] = Nil - - private def ngClassToString = { - val res = - ngClasses.map { - case (className, expr) => s"'$className':$expr" - }.mkString(",") - - s"{$res}" - } - - def ngClassAttrs: Seq[ElemAttr] = Seq("ng-class" -> ngClassToString) - } - - /** Angular's Form Controller */ - abstract class NgFormController(container: NgFieldContainer) extends NgForm with NgController { - - def roundTripName: String = "serverFuncs" - - protected def preExpr(f: NgField): NgExpression = s"$ngFormName.${f.name}." - protected def error(f: NgField): NgExpression = preExpr(f)+"$error." - - def pristine(f: NgField): NgExpression = preExpr(f)+"$pristine" - def dirty(f: NgField): NgExpression = preExpr(f)+"$dirty" - def valid(f: NgField): NgExpression = preExpr(f)+"$valid" - def invalid(f: NgField): NgExpression = preExpr(f)+"$invalid" - def errorEmail(f: NgField): NgExpression = error(f)+"$email" - def errorMax(f: NgField): NgExpression = error(f)+"$max" - def errorMaxLength(f: NgField): NgExpression = error(f)+"$maxlength" - def errorMin(f: NgField): NgExpression = error(f)+"$min" - def errorMinLength(f: NgField): NgExpression = error(f)+"$minlength" - def errorNumber(f: NgField): NgExpression = error(f)+"$number" - def errorPattern(f: NgField): NgExpression = error(f)+"$pattern" - def errorRequired(f: NgField): NgExpression = error(f)+"$required" - def errorUrl(f: NgField): NgExpression = error(f)+"$url" - - def moduleScript: Box[String] = appName.map { - _+".controller('"+ngControllerName+"',['$scope',function($scope){"+ - "$scope."+ngSubmitFuncName+"=function(){"+ - roundTripName+"."+ngSubmitCallback+"({"+ - container.allFields.map(f => f.name+":$scope."+f.name).mkString(",")+ - "});"+ - "};"+ - "}]);" - } - } - - /** - * Extra parameters to extend Field's usability such as: - * Placeholder - */ - trait FieldExtras extends BaseField { - def placeholder_? = false - def placeholder: String = displayName - - override def formElemAttrs: Seq[ElemAttr] = - if (placeholder_?) Seq("placeholder" -> placeholder) else Nil - } - - abstract class AngularJsScreen[T <: Record[T]] extends SnippetHelper { - - val record: T - - def formName: String = "form" - def modelName: String = "model" - def controllerName: String = "Controller" - def roundTripName: String = "serverFuncs" - - def allFields: List[BaseField] = record.allFields - - def roundTrips: List[RoundTripInfo] = List("save" -> finish _) - - def finish(info: JValue): JValue = { - //allFields.flatMap(f) - JNull - } - - def render: CssSel = { - for (sess <- S.session) { - val script = JsCrVar(roundTripName, sess.buildRoundtrip(roundTrips)) - S.appendGlobalJs(script) - } - - allFields match { - case f: BaseField => ("@" + f.name) #> "" - case _ => "*" #> "" - } - - } - } - - - - - trait SmartFormField[T, OwnerType <: Record[OwnerType]] extends Field[T, OwnerType] { - override def label: NodeSeq = labelAttrs.foldLeft(<label>{displayHtml}</label>) { - (el, elemAttr) => el % elemAttr - } - - def labelAttrs: Seq[ElemAttr] = Seq("class" -> {if(formInputType == "text") "input" else formInputType}) - } - - /** Requires FontAwesome */ - trait SmartFormFieldIcon extends ReadableField { - def prependIcon_? = false - def whiteIcon_? = false - def icon: String - - private def positionClass = if(prependIcon_?) "icon-prepend" else "icon-append" - private def iconClass = icon.replace("_", "-").map(i => if(whiteIcon_?) i+"-o") - private def classes = List(positionClass, "fa", "fa-"+iconClass) - private def smartFormIconAttrs: Seq[ElemAttr] = Seq("class" -> classes.mkString(" ")) - - override def displayHtml: NodeSeq = - smartFormIconAttrs.foldLeft(<i></i>)((el,attr) => el % attr) ++ super.displayHtml - } - - trait NgSmartFormField[T, OwnerType <: Record[OwnerType]] extends SmartFormField[T, OwnerType] with NgField with NgClass { - def stateError: NgExpression - def stateSuccess: NgExpression - - override def ngClasses = Seq( - "state-error" -> stateError, - "state-success" -> stateSuccess) - - override def labelAttrs: Seq[ElemAttr] = ngClassAttrs - } - - -} -\ No newline at end of file diff --git a/src/main/scala/com/pyd/model/field/USStateField.scala b/src/main/scala/com/pyd/model/field/USStateField.scala @@ -1,10 +1,12 @@ package com.pyd.model.field -import net.liftweb.http._ -import net.liftweb.record._ +import net.liftweb._ +import http._ +import util.Helpers._ +import record._ import field._ - - +import json.JsonAST._ +import common._ object USStates extends Enumeration { @@ -17,12 +19,23 @@ object USStates extends Enumeration { class States extends Val { override def toString = super.toString.replace("_", " ") } + + def options: List[(String, String)] = + USStates.values.map(i => (i.toString, i.toString)).toList + + def stateSelect(s: String) = tryo(USStates.withName(s)) } class USStatesField[OwnerType <: Record[OwnerType]](rec: OwnerType) extends EnumField(rec, USStates) { -} - -class OptionalUSStatesField[OwnerType <: Record[OwnerType]](rec: OwnerType) extends OptionalEnumField(rec, USStates) { + override def setFromJValue(jvalue: JValue): Box[USStatesField.this.MyType] = { + val stringToInt = jvalue transform { + case JString(s) => + val value: Box[Int] = USStates.stateSelect(s).map(_.id) + value.map(JInt(_)).openOr(JNothing) + case _ => JNothing + } + super.setFromJValue(stringToInt) + } } diff --git a/src/main/scala/com/pyd/snippet/Alert.scala b/src/main/scala/com/pyd/snippet/Alert.scala @@ -0,0 +1,35 @@ +package com.pyd +package snippet + +import xml._ +import net.liftweb._ +import util._ +import common._ +import json.JsonAST._ + +object Alert extends Logger { + + private def msgBox(msgType: String, msg: NodeSeq): JValue = + JObject(List( + JField("msg_type", JString(msgType)), + JField("msg", JString(msg.toString)))) + + def success(msg: NodeSeq): JValue = msgBox("success", msg) + def success(msg: String): JValue = success(Text(msg)) + + def danger(msg: NodeSeq, errors: List[FieldError]): JValue = { + debug(errors) + + msgBox("danger", msg ++ + errors.foldLeft(<ul></ul>)((el,err) => el.copy(child = el.child :+ <li>{err.msg}</li>))) + } + + def danger(msg: String, errors: List[FieldError] = Nil): JValue = danger(Text(msg), errors) + + def info(msg: NodeSeq): JValue = msgBox("info", msg) + def info(msg: String): JValue = info(Text(msg)) + + def warning(msg: NodeSeq): JValue = msgBox("warning", msg) + def warning(msg: String): JValue = warning(Text(msg)) + +} +\ No newline at end of file diff --git a/src/main/scala/com/pyd/snippet/ApplyAtmSnip.scala b/src/main/scala/com/pyd/snippet/ApplyAtmSnip.scala @@ -1,61 +0,0 @@ -package com.pyd -package snippet - -import model._ -import field._ -import net.liftweb._ -import common._ -import util._ -import Helpers._ -import http._ -import js._ -import SHtml._ -import JE.JsRaw - -class ApplyAtmSnip extends BaseSnippet[AtmApplication] with Logger { - - override val t = Full(AtmApplication.createRecord) - - val options: List[(String, String)] = - USStates.values.map(i => (i.toString, i.toString)).toList - - def render: CssSel = serve { - t => - - def stateSelection(id: String): Unit = tryo { - t.state.set(USStates.withName(id)) - } - - "@name" #> text(t.name.get, t.name.set _) & - "@email" #> text(t.email.get, t.email.set _) & - "@phone" #> email(t.phone.get, t.phone.set _) & - "@besttime" #> textarea(t.bestTime.get, t.bestTime.set _) & - "@address" #> text(t.address.get, t.address.set _) & - "@city" #> text(t.city.get, t.city.set _) & - "@state" #> select(options, Empty, stateSelection) & - "@website" #> text(t.website.get, t.website.set _) & - ":submit" #> ajaxSubmit("Apply for Bitcoin ATM", process _) - } - - private def process: JsCmd = serve { - t => - t.validate match { - case Nil => - t.save - S.notice( - <div> - <i class="fa-fw fa fa-check"></i> - <strong>Your Bitcoin ATM application has been received.</strong> - {xml.Text(" We will review and contact you for further information. Thank you for applying.")} - </div>) - - case errors => - S.error(errors) - /*JsRaw(""" - |$(".invalid-field").each(function() { - | $(this).closest("label.input").addClass("state-error"); - |}); - """.stripMargin).cmd*/ - } - } -} -\ No newline at end of file diff --git a/src/main/scala/com/pyd/snippet/AtmApplicationSnip.scala b/src/main/scala/com/pyd/snippet/AtmApplicationSnip.scala @@ -0,0 +1,48 @@ +package com.pyd +package snippet + +import model._ +import field._ +import net.liftweb._ +import common._ +import json.JsonAST._ +import util._ +import Helpers._ +import http._ +import js._ +import JsCmds._ +import SHtml._ +import JE.JsVar + +class AtmApplicationSnip { + + def save(model: JValue): JValue = { + val rec = AtmApplication.createRecord + rec.setFieldsFromJValue(model) + + rec.validate match { + case Nil => + rec.save + Alert.success( + <i class="fa-fw fa fa-thumbs-o-up"></i> ++ + <strong>{ s"Your Bitcoin ATM application for ${rec.name.get} has been received." }</strong> ++ + <span>{s" We will contact you when we are ready. Thank you for applying."}</span> + ) + case errors => + Alert.danger( + <i class="fa-fw fa fa-thumbs-o-down"></i> ++ + <strong>{ s"Your application could not be processed." }</strong>, + errors + ) + } + } + + def render: CssSel = { + for (sess <- S.session) { + val script = SetExp(JsVar("window", "backend"), sess.buildRoundtrip(List[RoundTripInfo]("save" -> save _))) + S.appendGlobalJs(script) + } + + "@state" #> select(USStates.options, Empty, USStates.stateSelect, "name" -> "state") + } +} +\ No newline at end of file diff --git a/src/main/scala/com/pyd/snippet/NearAtmNotifySnip.scala b/src/main/scala/com/pyd/snippet/NearAtmNotifySnip.scala @@ -13,14 +13,8 @@ import js._ import JsCmds._ import SHtml._ import JE.JsVar -import xml._ -class NearAtmNotifySnip extends Logger { - - val options: List[(String, String)] = - USStates.values.map(i => (i.toString, i.toString)).toList - - def stateSelect(id: String) = tryo { USStates.withName(id) } +class NearAtmNotifySnip { def save(model: JValue): JValue = { val rec = NearAtmNotify.createRecord @@ -29,17 +23,17 @@ class NearAtmNotifySnip extends Logger { rec.validate match { case Nil => rec.save - S.notice( - <div> - <i class="fa-fw fa fa-thumbs-o-up"></i> - <strong>{ s"Hi ${rec.fname.get}, your notification has been received." }</strong> - { xml.Text(s" We will notify you when there is an ATM near ${rec.city.get} ${rec.state.get}.") } - </div>) - error("success") - Alert.success("CONGRATS, your form was submitted!") + Alert.success( + <i class="fa-fw fa fa-thumbs-o-up"></i> ++ + <strong>{ s"Hi ${rec.fname.get}, your notification has been received." }</strong> ++ + <span>{s" We will notify you when there is an ATM near ${rec.city.get}, ${rec.state.get}."}</span> + ) case errors => - error("failed"+errors.map(_.msg).mkString) - Alert.danger("FAIL, your form was not submitted!") + Alert.danger( + <i class="fa-fw fa fa-thumbs-o-down"></i> ++ + <strong>{ s"Hi ${rec.fname.get}, your notification was not submitted successfully." }</strong>, + errors + ) } } @@ -49,24 +43,6 @@ class NearAtmNotifySnip extends Logger { S.appendGlobalJs(script) } - "@state" #> select(options, Empty, stateSelect, "name" -> "state") + "@state" #> select(USStates.options, Empty, USStates.stateSelect, "name" -> "state") } - -} - -object Alert { - private def msgBox(msgType: String, msg: NodeSeq): JValue = - JObject(List(JField("msg_type", JString(msgType)), JField("msg", JString(msg.toString)), JField("clear", JBool(true)))) - - def success(msg: NodeSeq): JValue = msgBox("success", msg) - def success(msg: String): JValue = success(Text(msg)) - - def danger(msg: NodeSeq): JValue = msgBox("danger", msg) - def danger(msg: String): JValue = danger(Text(msg)) - - def info(msg: NodeSeq): JValue = msgBox("info", msg) - def info(msg: String): JValue = info(Text(msg)) - - def warning(msg: NodeSeq): JValue = msgBox("warning", msg) - def warning(msg: String): JValue = warning(Text(msg)) } \ No newline at end of file diff --git a/src/main/scala/com/pyd/snippet/UtilSnips.scala b/src/main/scala/com/pyd/snippet/UtilSnips.scala @@ -37,8 +37,6 @@ abstract class BaseSnippet[T] extends SnippetHelper { object Assets extends AssetLoader -object Notices extends BsAlerts - object Menus extends BsMenu object ProductionOnly { diff --git a/src/main/webapp/app/App.js b/src/main/webapp/app/App.js @@ -1,18 +1,16 @@ -var app = angular.module("app", ['ui.bootstrap']); +var app = angular.module("app", ['ui.bootstrap', 'ui.mask', 'ngSanitize']); var ZIP_CODE_REGEXP = /^(\d{5}(-\d{4})?|[A-Z]\d[A-Z] *\d[A-Z]\d)$/; app.controller('AlertCtrl', ['$scope', function($scope) { $scope.alerts = []; - + $scope.$on('alertDialog', function(event, alert) { $scope.addAlert(alert); }); $scope.addAlert = function(alert) { - if(alert.clear === true) { - $scope.alerts = []; - } + $scope.alerts = []; $scope.alerts.push({type: alert.msg_type, msg: alert.msg}); }; @@ -43,10 +41,10 @@ app.controller('FormCtrl', ['$scope', '$rootScope', function($scope, $rootScope) }; }]); -app.controller('NearAtmNotifyCtrl', ['$scope', '$controller', function($scope, $controller) { - $controller('FormCtrl', {$scope: $scope}); +app.controller('NearAtmNotifyCtrl', ['$scope', '$controller', '$rootScope', function($scope, $controller, $rootScope) { + $controller('FormCtrl', {$scope: $scope, $rootScope: $rootScope}); }]); -app.controller('AtmApplicationCtrl', ['$scope', '$controller', function($scope, $controller) { - $controller('FormCtrl', {$scope: $scope}); +app.controller('AtmApplicationCtrl', ['$scope', '$controller', '$rootScope', function($scope, $controller, $rootScope) { + $controller('FormCtrl', {$scope: $scope, $rootScope: $rootScope}); }]); \ No newline at end of file diff --git a/src/main/webapp/templates-hidden/parts/alert.html b/src/main/webapp/templates-hidden/parts/alert.html @@ -1,3 +1,3 @@ -<div ng-controller="AlertCtrl"> - <alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{alert.msg}}</alert> +<div ng-controller="AlertCtrl" ng-cloak> + <alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)" ng-bind-html="alert.msg"></alert> </div> \ No newline at end of file diff --git a/src/main/webapp/templates-hidden/parts/apply-atm-form.html b/src/main/webapp/templates-hidden/parts/apply-atm-form.html @@ -1,55 +1,58 @@ -<div data-lift="ApplyAtmSnip"> - <form class="lift:form.ajax?class=smart-form+client-form"> +<div data-lift="AtmApplicationSnip" ng-controller="AtmApplicationCtrl" ng-cloak> + <form name="form" class="smart-form client-form" ng-submit="save()" novalidate> <fieldset> <section> - <label class="input"> <i class="icon-append fa fa-briefcase"></i> - <input name="name" placeholder="Business Name" type="text"> + <label class="input" ng-class="{'state-success' : form.name.$valid && !form.name.$pristine}"> + <i class="icon-append fa fa-briefcase"></i> + <input name="name" ng-model="model.name" placeholder="Business Name" type="text" required> </label> </section> <section> - <label class="input"> <i class="icon-append fa fa-envelope-o"></i> - <input name="email" placeholder="E-Mail" type="email"> + <label class="input" ng-class="{'state-error' : form.email.$invalid && !form.email.$pristine, 'state-success' : form.email.$valid && !form.email.$pristine}"> + <i class="icon-append fa fa-envelope-o"></i> + <input name="email" ng-model="model.email" placeholder="E-Mail" type="email" required> </label> </section> <section> - <label class="input"> + <label class="input" ng-class="{'state-error' : form.phone.$invalid && !form.phone.$pristine, 'state-success' : form.phone.$valid && !form.phone.$pristine}"> <i class="icon-append fa fa-phone"></i> - <input name="phone" placeholder="Phone Number" type="text"> + <input name="phone" ng-model="model.phone" ui-mask="(999) 999-9999" placeholder="(xxx) xxx-xxxx" type="text" required> </label> </section> <section> - <label class="input"> - <input name="address" placeholder="Address" type="text"> + <label class="input" ng-class="{'state-error' : form.address.$invalid && !form.address.$pristine, 'state-success' : form.address.$valid && !form.address.$pristine}"> + <input name="address" ng-model="model.address" placeholder="Address" type="text" required> </label> </section> <div class="row"> <section class="col col-6"> - <label class="input"> - <input name="city" placeholder="City"> + <label class="input" ng-class="{'state-error' : form.city.$invalid && !form.city.$pristine, 'state-success' : form.city.$valid && !form.city.$pristine}"> + <input name="city" ng-model="model.city" placeholder="City" type="text" required> </label> </section> - <section class="col col-6"> + <section class="col col-6" ng-class="{'state-error' : form.state.$invalid && !form.state.$pristine, 'state-success' : form.state.$valid && !form.state.$pristine}"> <label class="select"> - <select name="state"></select> <i></i> + <select name="state" ng-model="model.state" required></select> + <i></i> </label> </section> </div> <section> - <label class="input"> + <label class="input" ng-class="{'state-error' : form.website.$invalid && !form.website.$pristine, 'state-success' : form.website.$valid && !form.website.$pristine}"> <i class="icon-append fa fa-globe"></i> - <input name="website" placeholder="Website URL" type="text"> + <input name="website" ng-model="model.website" placeholder="Website URL" type="url"> </label> </section> <section> - <label class="textarea"> + <label class="textarea" ng-class="{'state-success' : form.bestTime.$valid && form.bestTime.$dirty}"> <i class="icon-append fa fa-comment"></i> - <textarea name="besttime" + <textarea name="bestTime" ng-model="model.bestTime" placeholder="When is the best time to contact you?" rows="2"></textarea> </label> <em>Enter time of day and/or week you would like to be contacted.</em> @@ -58,7 +61,7 @@ </fieldset> <footer> - <button type="submit" class="btn btn-primary"></button> + <button type="submit" class="btn btn-primary" ng-disabled="form.$invalid">Apply for Bitcoin ATM</button> </footer> </form> </div> \ No newline at end of file diff --git a/src/main/webapp/templates-hidden/parts/notify-atm-form.html b/src/main/webapp/templates-hidden/parts/notify-atm-form.html @@ -19,7 +19,7 @@ <div class="row"> <section class="col col-6"> - <label class="input" ng-class="{'state-error' : form.email.$invalid && form.email.$dirty, 'state-success' : form.email.$valid && !form.postal.$pristine}"> + <label class="input" ng-class="{'state-error' : form.email.$invalid && !form.email.$pristine, 'state-success' : form.email.$valid && !form.email.$pristine}"> <i class="icon-append fa fa-envelope-o"></i> <input name="email" ng-model="model.email" placeholder="E-mail" type="email" required> </label> diff --git a/src/main/webapp/vendor/ngSanitize.js b/src/main/webapp/vendor/ngSanitize.js @@ -0,0 +1,467 @@ +'use strict'; + +var $sanitizeMinErr = angular.$$minErr('$sanitize'); + +/** + * @ngdoc module + * @name ngSanitize + * @description + * + * # ngSanitize + * + * The `ngSanitize` module provides functionality to sanitize HTML. + * + * + * <div doc-module-components="ngSanitize"></div> + * + * See {@link ngSanitize.$sanitize `$sanitize`} for usage. + */ + +/* + * HTML Parser By Misko Hevery (misko@hevery.com) + * based on: HTML Parser By John Resig (ejohn.org) + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + * + * // Use like so: + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + */ + + +/** + * @ngdoc service + * @name $sanitize + * @function + * + * @description + * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string, however, since our parser is more strict than a typical browser + * parser, it's possible that some obscure input, which would be recognized as valid HTML by a + * browser, won't make it through the sanitizer. + * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and + * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. + * + * @param {string} html Html input. + * @returns {string} Sanitized html. + * + * @example + <example module="ngSanitize" deps="angular-sanitize.js"> + <file name="index.html"> + <script> + function Ctrl($scope, $sce) { + $scope.snippet = + '<p style="color:blue">an html\n' + + '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + + 'snippet</p>'; + $scope.deliberatelyTrustDangerousSnippet = function() { + return $sce.trustAsHtml($scope.snippet); + }; + } + </script> + <div ng-controller="Ctrl"> + Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> + <table> + <tr> + <td>Directive</td> + <td>How</td> + <td>Source</td> + <td>Rendered</td> + </tr> + <tr id="bind-html-with-sanitize"> + <td>ng-bind-html</td> + <td>Automatically uses $sanitize</td> + <td><pre><div ng-bind-html="snippet"><br/></div></pre></td> + <td><div ng-bind-html="snippet"></div></td> + </tr> + <tr id="bind-html-with-trust"> + <td>ng-bind-html</td> + <td>Bypass $sanitize by explicitly trusting the dangerous value</td> + <td> + <pre><div ng-bind-html="deliberatelyTrustDangerousSnippet()"> +</div></pre> + </td> + <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td> + </tr> + <tr id="bind-default"> + <td>ng-bind</td> + <td>Automatically escapes</td> + <td><pre><div ng-bind="snippet"><br/></div></pre></td> + <td><div ng-bind="snippet"></div></td> + </tr> + </table> + </div> + </file> + <file name="protractor.js" type="protractor"> + it('should sanitize the html snippet by default', function() { + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('<p>an html\n<em>click here</em>\nsnippet</p>'); + }); + + it('should inline raw snippet if bound to a trusted value', function() { + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should escape snippet without any filter', function() { + expect(element(by.css('#bind-default div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>'); + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('new <b>text</b>'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + 'new <b onclick="alert(1)">text</b>'); + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + "new <b onclick=\"alert(1)\">text</b>"); + }); + </file> + </example> + */ +function $SanitizeProvider() { + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; +} + +function sanitizeText(chars) { + var buf = []; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} + + +// Regular Expressions for parsing tags and attributes +var START_TAG_REGEXP = + /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, + END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, + ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, + BEGIN_TAG_REGEXP = /^</, + BEGING_END_TAGE_REGEXP = /^<\s*\//, + COMMENT_REGEXP = /<!--(.*?)-->/g, + DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i, + CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g, + // Match everything outside of normal chars and " (quote character) + NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; + + +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements + +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = makeMap("area,br,col,hr,img,wbr"); + +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = makeMap("rp,rt"), + optionalEndTagElements = angular.extend({}, + optionalEndTagInlineElements, + optionalEndTagBlockElements); + +// Safe Block Elements - HTML5 +var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); + +// Inline Elements - HTML5 +var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); + + +// Special Elements (can contain anything) +var specialElements = makeMap("script,style"); + +var validElements = angular.extend({}, + voidElements, + blockElements, + inlineElements, + optionalEndTagElements); + +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); +var validAttrs = angular.extend({}, uriAttrs, makeMap( + 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ + 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+ + 'valign,value,vspace,width')); + +function makeMap(str) { + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) obj[items[i]] = true; + return obj; +} + + +/** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser( html, handler ) { + var index, chars, match, stack = [], last = html; + stack.last = function() { return stack[ stack.length - 1 ]; }; + + while ( html ) { + chars = true; + + // Make sure we're not in a script or style element + if ( !stack.last() || !specialElements[ stack.last() ] ) { + + // Comment + if ( html.indexOf("<!--") === 0 ) { + // comments containing -- are not allowed unless they terminate the comment + index = html.indexOf("--", 4); + + if ( index >= 0 && html.lastIndexOf("-->", index) === index) { + if (handler.comment) handler.comment( html.substring( 4, index ) ); + html = html.substring( index + 3 ); + chars = false; + } + // DOCTYPE + } else if ( DOCTYPE_REGEXP.test(html) ) { + match = html.match( DOCTYPE_REGEXP ); + + if ( match ) { + html = html.replace( match[0], ''); + chars = false; + } + // end tag + } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { + match = html.match( END_TAG_REGEXP ); + + if ( match ) { + html = html.substring( match[0].length ); + match[0].replace( END_TAG_REGEXP, parseEndTag ); + chars = false; + } + + // start tag + } else if ( BEGIN_TAG_REGEXP.test(html) ) { + match = html.match( START_TAG_REGEXP ); + + if ( match ) { + html = html.substring( match[0].length ); + match[0].replace( START_TAG_REGEXP, parseStartTag ); + chars = false; + } + } + + if ( chars ) { + index = html.indexOf("<"); + + var text = index < 0 ? html : html.substring( 0, index ); + html = index < 0 ? "" : html.substring( index ); + + if (handler.chars) handler.chars( decodeEntities(text) ); + } + + } else { + html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), + function(all, text){ + text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); + + if (handler.chars) handler.chars( decodeEntities(text) ); + + return ""; + }); + + parseEndTag( "", stack.last() ); + } + + if ( html == last ) { + throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + + "of html: {0}", html); + } + last = html; + } + + // Clean up any remaining tags + parseEndTag(); + + function parseStartTag( tag, tagName, rest, unary ) { + tagName = angular.lowercase(tagName); + if ( blockElements[ tagName ] ) { + while ( stack.last() && inlineElements[ stack.last() ] ) { + parseEndTag( "", stack.last() ); + } + } + + if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { + parseEndTag( "", tagName ); + } + + unary = voidElements[ tagName ] || !!unary; + + if ( !unary ) + stack.push( tagName ); + + var attrs = {}; + + rest.replace(ATTR_REGEXP, + function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { + var value = doubleQuotedValue + || singleQuotedValue + || unquotedValue + || ''; + + attrs[name] = decodeEntities(value); + }); + if (handler.start) handler.start( tagName, attrs, unary ); + } + + function parseEndTag( tag, tagName ) { + var pos = 0, i; + tagName = angular.lowercase(tagName); + if ( tagName ) + // Find the closest opened tag of the same type + for ( pos = stack.length - 1; pos >= 0; pos-- ) + if ( stack[ pos ] == tagName ) + break; + + if ( pos >= 0 ) { + // Close all the open elements, up the stack + for ( i = stack.length - 1; i >= pos; i-- ) + if (handler.end) handler.end( stack[ i ] ); + + // Remove the open elements from the stack + stack.length = pos; + } + } +} + +var hiddenPre=document.createElement("pre"); +var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; +/** + * decodes all entities into regular string + * @param value + * @returns {string} A string with decoded entities. + */ +function decodeEntities(value) { + if (!value) { return ''; } + + // Note: IE8 does not preserve spaces at the start/end of innerHTML + // so we must capture them and reattach them afterward + var parts = spaceRe.exec(value); + var spaceBefore = parts[1]; + var spaceAfter = parts[3]; + var content = parts[2]; + if (content) { + hiddenPre.innerHTML=content.replace(/</g,"<"); + // innerText depends on styling as it doesn't display hidden elements. + // Therefore, it's better to use textContent not to cause unnecessary + // reflows. However, IE<9 don't support textContent so the innerText + // fallback is necessary. + content = 'textContent' in hiddenPre ? + hiddenPre.textContent : hiddenPre.innerText; + } + return spaceBefore + content + spaceAfter; +} + +/** + * Escapes all potentially dangerous characters, so that the + * resulting string can be safely inserted into attribute or + * element text. + * @param value + * @returns {string} escaped text + */ +function encodeEntities(value) { + return value. + replace(/&/g, '&'). + replace(NON_ALPHANUMERIC_REGEXP, function(value){ + return '&#' + value.charCodeAt(0) + ';'; + }). + replace(/</g, '<'). + replace(/>/g, '>'); +} + +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.jain('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf, uriValidator){ + var ignore = false; + var out = angular.bind(buf, buf.push); + return { + start: function(tag, attrs, unary){ + tag = angular.lowercase(tag); + if (!ignore && specialElements[tag]) { + ignore = tag; + } + if (!ignore && validElements[tag] === true) { + out('<'); + out(tag); + angular.forEach(attrs, function(value, key){ + var lkey=angular.lowercase(key); + var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); + if (validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out(unary ? '/>' : '>'); + } + }, + end: function(tag){ + tag = angular.lowercase(tag); + if (!ignore && validElements[tag] === true) { + out('</'); + out(tag); + out('>'); + } + if (tag == ignore) { + ignore = false; + } + }, + chars: function(chars){ + if (!ignore) { + out(encodeEntities(chars)); + } + } + }; +} + + +// define ngSanitize module and register $sanitize service +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); diff --git a/src/main/webapp/vendor/ui-mask.js b/src/main/webapp/vendor/ui-mask.js @@ -0,0 +1,511 @@ +'use strict'; + +/* + Attaches input mask onto input element + */ +angular.module('ui.mask', []) + .value('uiMaskConfig', { + 'maskDefinitions': { + '9': /\d/, + 'A': /[a-zA-Z]/, + '*': /[a-zA-Z0-9]/ + } + }) + .directive('uiMask', ['uiMaskConfig', '$parse', function (maskConfig, $parse) { + return { + priority: 100, + require: 'ngModel', + restrict: 'A', + compile: function uiMaskCompilingFunction(){ + var options = maskConfig; + + return function uiMaskLinkingFunction(scope, iElement, iAttrs, controller){ + var maskProcessed = false, eventsBound = false, + maskCaretMap, maskPatterns, maskPlaceholder, maskComponents, + // Minimum required length of the value to be considered valid + minRequiredLength, + value, valueMasked, isValid, + // Vars for initializing/uninitializing + originalPlaceholder = iAttrs.placeholder, + originalMaxlength = iAttrs.maxlength, + // Vars used exclusively in eventHandler() + oldValue, oldValueUnmasked, oldCaretPosition, oldSelectionLength; + + function initialize(maskAttr){ + if (!angular.isDefined(maskAttr)) { + return uninitialize(); + } + processRawMask(maskAttr); + if (!maskProcessed) { + return uninitialize(); + } + initializeElement(); + bindEventListeners(); + return true; + } + + function initPlaceholder(placeholderAttr) { + if(! angular.isDefined(placeholderAttr)) { + return; + } + + maskPlaceholder = placeholderAttr; + + // If the mask is processed, then we need to update the value + if (maskProcessed) { + eventHandler(); + } + } + + function formatter(fromModelValue){ + if (!maskProcessed) { + return fromModelValue; + } + value = unmaskValue(fromModelValue || ''); + isValid = validateValue(value); + controller.$setValidity('mask', isValid); + return isValid && value.length ? maskValue(value) : undefined; + } + + function parser(fromViewValue){ + if (!maskProcessed) { + return fromViewValue; + } + value = unmaskValue(fromViewValue || ''); + isValid = validateValue(value); + // We have to set viewValue manually as the reformatting of the input + // value performed by eventHandler() doesn't happen until after + // this parser is called, which causes what the user sees in the input + // to be out-of-sync with what the controller's $viewValue is set to. + controller.$viewValue = value.length ? maskValue(value) : ''; + controller.$setValidity('mask', isValid); + if (value === '' && controller.$error.required !== undefined) { + controller.$setValidity('required', false); + } + return isValid ? value : undefined; + } + + var linkOptions = {}; + + if (iAttrs.uiOptions) { + linkOptions = scope.$eval('[' + iAttrs.uiOptions + ']'); + if (angular.isObject(linkOptions[0])) { + // we can't use angular.copy nor angular.extend, they lack the power to do a deep merge + linkOptions = (function(original, current){ + for(var i in original) { + if (Object.prototype.hasOwnProperty.call(original, i)) { + if (!current[i]) { + current[i] = angular.copy(original[i]); + } else { + angular.extend(current[i], original[i]); + } + } + } + return current; + })(options, linkOptions[0]); + } + } else { + linkOptions = options; + } + + iAttrs.$observe('uiMask', initialize); + iAttrs.$observe('placeholder', initPlaceholder); + var modelViewValue = false; + iAttrs.$observe('modelViewValue', function(val) { + if(val === 'true') { + modelViewValue = true; + } + }); + scope.$watch(iAttrs.ngModel, function(val) { + if(modelViewValue && val) { + var model = $parse(iAttrs.ngModel); + model.assign(scope, controller.$viewValue); + } + }); + controller.$formatters.push(formatter); + controller.$parsers.push(parser); + + function uninitialize(){ + maskProcessed = false; + unbindEventListeners(); + + if (angular.isDefined(originalPlaceholder)) { + iElement.attr('placeholder', originalPlaceholder); + } else { + iElement.removeAttr('placeholder'); + } + + if (angular.isDefined(originalMaxlength)) { + iElement.attr('maxlength', originalMaxlength); + } else { + iElement.removeAttr('maxlength'); + } + + iElement.val(controller.$modelValue); + controller.$viewValue = controller.$modelValue; + return false; + } + + function initializeElement(){ + value = oldValueUnmasked = unmaskValue(controller.$modelValue || ''); + valueMasked = oldValue = maskValue(value); + isValid = validateValue(value); + var viewValue = isValid && value.length ? valueMasked : ''; + if (iAttrs.maxlength) { // Double maxlength to allow pasting new val at end of mask + iElement.attr('maxlength', maskCaretMap[maskCaretMap.length - 1] * 2); + } + iElement.attr('placeholder', maskPlaceholder); + iElement.val(viewValue); + controller.$viewValue = viewValue; + // Not using $setViewValue so we don't clobber the model value and dirty the form + // without any kind of user interaction. + } + + function bindEventListeners(){ + if (eventsBound) { + return; + } + iElement.bind('blur', blurHandler); + iElement.bind('mousedown mouseup', mouseDownUpHandler); + iElement.bind('input keyup click focus', eventHandler); + eventsBound = true; + } + + function unbindEventListeners(){ + if (!eventsBound) { + return; + } + iElement.unbind('blur', blurHandler); + iElement.unbind('mousedown', mouseDownUpHandler); + iElement.unbind('mouseup', mouseDownUpHandler); + iElement.unbind('input', eventHandler); + iElement.unbind('keyup', eventHandler); + iElement.unbind('click', eventHandler); + iElement.unbind('focus', eventHandler); + eventsBound = false; + } + + function validateValue(value){ + // Zero-length value validity is ngRequired's determination + return value.length ? value.length >= minRequiredLength : true; + } + + function unmaskValue(value){ + var valueUnmasked = '', + maskPatternsCopy = maskPatterns.slice(); + // Preprocess by stripping mask components from value + value = value.toString(); + angular.forEach(maskComponents, function (component){ + value = value.replace(component, ''); + }); + angular.forEach(value.split(''), function (chr){ + if (maskPatternsCopy.length && maskPatternsCopy[0].test(chr)) { + valueUnmasked += chr; + maskPatternsCopy.shift(); + } + }); + return valueUnmasked; + } + + function maskValue(unmaskedValue){ + var valueMasked = '', + maskCaretMapCopy = maskCaretMap.slice(); + + angular.forEach(maskPlaceholder.split(''), function (chr, i){ + if (unmaskedValue.length && i === maskCaretMapCopy[0]) { + valueMasked += unmaskedValue.charAt(0) || '_'; + unmaskedValue = unmaskedValue.substr(1); + maskCaretMapCopy.shift(); + } + else { + valueMasked += chr; + } + }); + return valueMasked; + } + + function getPlaceholderChar(i) { + var placeholder = iAttrs.placeholder; + + if (typeof placeholder !== 'undefined' && placeholder[i]) { + return placeholder[i]; + } else { + return '_'; + } + } + + // Generate array of mask components that will be stripped from a masked value + // before processing to prevent mask components from being added to the unmasked value. + // E.g., a mask pattern of '+7 9999' won't have the 7 bleed into the unmasked value. + // If a maskable char is followed by a mask char and has a mask + // char behind it, we'll split it into it's own component so if + // a user is aggressively deleting in the input and a char ahead + // of the maskable char gets deleted, we'll still be able to strip + // it in the unmaskValue() preprocessing. + function getMaskComponents() { + return maskPlaceholder.replace(/[_]+/g, '_').replace(/([^_]+)([a-zA-Z0-9])([^_])/g, '$1$2_$3').split('_'); + } + + function processRawMask(mask){ + var characterCount = 0; + + maskCaretMap = []; + maskPatterns = []; + maskPlaceholder = ''; + + if (typeof mask === 'string') { + minRequiredLength = 0; + + var isOptional = false, + splitMask = mask.split(''); + + angular.forEach(splitMask, function (chr, i){ + if (linkOptions.maskDefinitions[chr]) { + + maskCaretMap.push(characterCount); + + maskPlaceholder += getPlaceholderChar(i); + maskPatterns.push(linkOptions.maskDefinitions[chr]); + + characterCount++; + if (!isOptional) { + minRequiredLength++; + } + } + else if (chr === '?') { + isOptional = true; + } + else { + maskPlaceholder += chr; + characterCount++; + } + }); + } + // Caret position immediately following last position is valid. + maskCaretMap.push(maskCaretMap.slice().pop() + 1); + + maskComponents = getMaskComponents(); + maskProcessed = maskCaretMap.length > 1 ? true : false; + } + + function blurHandler(){ + oldCaretPosition = 0; + oldSelectionLength = 0; + if (!isValid || value.length === 0) { + valueMasked = ''; + iElement.val(''); + scope.$apply(function (){ + controller.$setViewValue(''); + }); + } + } + + function mouseDownUpHandler(e){ + if (e.type === 'mousedown') { + iElement.bind('mouseout', mouseoutHandler); + } else { + iElement.unbind('mouseout', mouseoutHandler); + } + } + + iElement.bind('mousedown mouseup', mouseDownUpHandler); + + function mouseoutHandler(){ + /*jshint validthis: true */ + oldSelectionLength = getSelectionLength(this); + iElement.unbind('mouseout', mouseoutHandler); + } + + function eventHandler(e){ + /*jshint validthis: true */ + e = e || {}; + // Allows more efficient minification + var eventWhich = e.which, + eventType = e.type; + + // Prevent shift and ctrl from mucking with old values + if (eventWhich === 16 || eventWhich === 91) { return;} + + var val = iElement.val(), + valOld = oldValue, + valMasked, + valUnmasked = unmaskValue(val), + valUnmaskedOld = oldValueUnmasked, + valAltered = false, + + caretPos = getCaretPosition(this) || 0, + caretPosOld = oldCaretPosition || 0, + caretPosDelta = caretPos - caretPosOld, + caretPosMin = maskCaretMap[0], + caretPosMax = maskCaretMap[valUnmasked.length] || maskCaretMap.slice().shift(), + + selectionLenOld = oldSelectionLength || 0, + isSelected = getSelectionLength(this) > 0, + wasSelected = selectionLenOld > 0, + + // Case: Typing a character to overwrite a selection + isAddition = (val.length > valOld.length) || (selectionLenOld && val.length > valOld.length - selectionLenOld), + // Case: Delete and backspace behave identically on a selection + isDeletion = (val.length < valOld.length) || (selectionLenOld && val.length === valOld.length - selectionLenOld), + isSelection = (eventWhich >= 37 && eventWhich <= 40) && e.shiftKey, // Arrow key codes + + isKeyLeftArrow = eventWhich === 37, + // Necessary due to "input" event not providing a key code + isKeyBackspace = eventWhich === 8 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === -1)), + isKeyDelete = eventWhich === 46 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === 0 ) && !wasSelected), + + // Handles cases where caret is moved and placed in front of invalid maskCaretMap position. Logic below + // ensures that, on click or leftward caret placement, caret is moved leftward until directly right of + // non-mask character. Also applied to click since users are (arguably) more likely to backspace + // a character when clicking within a filled input. + caretBumpBack = (isKeyLeftArrow || isKeyBackspace || eventType === 'click') && caretPos > caretPosMin; + + oldSelectionLength = getSelectionLength(this); + + // These events don't require any action + if (isSelection || (isSelected && (eventType === 'click' || eventType === 'keyup'))) { + return; + } + + // Value Handling + // ============== + + // User attempted to delete but raw value was unaffected--correct this grievous offense + if ((eventType === 'input') && isDeletion && !wasSelected && valUnmasked === valUnmaskedOld) { + while (isKeyBackspace && caretPos > caretPosMin && !isValidCaretPosition(caretPos)) { + caretPos--; + } + while (isKeyDelete && caretPos < caretPosMax && maskCaretMap.indexOf(caretPos) === -1) { + caretPos++; + } + var charIndex = maskCaretMap.indexOf(caretPos); + // Strip out non-mask character that user would have deleted if mask hadn't been in the way. + valUnmasked = valUnmasked.substring(0, charIndex) + valUnmasked.substring(charIndex + 1); + valAltered = true; + } + + // Update values + valMasked = maskValue(valUnmasked); + + oldValue = valMasked; + oldValueUnmasked = valUnmasked; + iElement.val(valMasked); + if (valAltered) { + // We've altered the raw value after it's been $digest'ed, we need to $apply the new value. + scope.$apply(function (){ + controller.$setViewValue(valUnmasked); + }); + } + + // Caret Repositioning + // =================== + + // Ensure that typing always places caret ahead of typed character in cases where the first char of + // the input is a mask char and the caret is placed at the 0 position. + if (isAddition && (caretPos <= caretPosMin)) { + caretPos = caretPosMin + 1; + } + + if (caretBumpBack) { + caretPos--; + } + + // Make sure caret is within min and max position limits + caretPos = caretPos > caretPosMax ? caretPosMax : caretPos < caretPosMin ? caretPosMin : caretPos; + + // Scoot the caret back or forth until it's in a non-mask position and within min/max position limits + while (!isValidCaretPosition(caretPos) && caretPos > caretPosMin && caretPos < caretPosMax) { + caretPos += caretBumpBack ? -1 : 1; + } + + if ((caretBumpBack && caretPos < caretPosMax) || (isAddition && !isValidCaretPosition(caretPosOld))) { + caretPos++; + } + oldCaretPosition = caretPos; + setCaretPosition(this, caretPos); + } + + function isValidCaretPosition(pos){ return maskCaretMap.indexOf(pos) > -1; } + + function getCaretPosition(input){ + if (!input) return 0; + if (input.selectionStart !== undefined) { + return input.selectionStart; + } else if (document.selection) { + // Curse you IE + input.focus(); + var selection = document.selection.createRange(); + selection.moveStart('character', -input.value.length); + return selection.text.length; + } + return 0; + } + + function setCaretPosition(input, pos){ + if (!input) return 0; + if (input.offsetWidth === 0 || input.offsetHeight === 0) { + return; // Input's hidden + } + if (input.setSelectionRange) { + input.focus(); + input.setSelectionRange(pos, pos); + } + else if (input.createTextRange) { + // Curse you IE + var range = input.createTextRange(); + range.collapse(true); + range.moveEnd('character', pos); + range.moveStart('character', pos); + range.select(); + } + } + + function getSelectionLength(input){ + if (!input) return 0; + if (input.selectionStart !== undefined) { + return (input.selectionEnd - input.selectionStart); + } + if (document.selection) { + return (document.selection.createRange().text.length); + } + return 0; + } + + // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */){ + if (this === null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n !== 0 && n !== Infinity && n !== -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + }; + } + + }; + } + }; + } +]);