Browse Source

form builder

Davide Alberani 7 years ago
parent
commit
3541ed8701
42 changed files with 8427 additions and 7 deletions
  1. 33 0
      angular_app/event-edit.html
  2. 23 4
      angular_app/index.html
  3. 2 1
      angular_app/js/app.js
  4. 9 2
      angular_app/js/controllers.js
  5. 5 0
      static/css/animate.min.css
  6. 0 0
      static/css/dropzone.min.css
  7. 862 0
      static/css/eda.dragdropway.css
  8. 8 0
      static/css/eda.dragdropway.min.css
  9. 70 0
      static/css/eda.easyFormViewer.css
  10. 9 0
      static/css/eda.easyFormViewer.min.css
  11. 591 0
      static/css/eda.stepway.css
  12. 8 0
      static/css/eda.stepway.min.css
  13. 0 0
      static/css/eda.textAngular.min.css
  14. 7 0
      static/css/loading-bar.min.css
  15. 226 0
      static/css/nya-bs-select.css
  16. 6 0
      static/css/nya-bs-select.min.css
  17. 193 0
      static/css/textAngular.css
  18. 0 0
      static/css/textAngular.min.css
  19. 155 0
      static/css/toaster.css
  20. 11 0
      static/css/toaster.min.css
  21. 1 0
      static/js/angular-formly-templates-bootstrap.min.js
  22. 0 0
      static/js/angular-formly-templates-bootstrap.min.js.map
  23. 2 0
      static/js/api-check.min.js
  24. 0 0
      static/js/api-check.min.js.map
  25. 0 0
      static/js/eda.dragdropway.js
  26. 0 0
      static/js/eda.dragdropway.min.js
  27. 0 0
      static/js/eda.easyFormViewer.js
  28. 0 0
      static/js/eda.easyFormViewer.min.js
  29. 0 0
      static/js/eda.stepway.js
  30. 0 0
      static/js/eda.stepway.min.js
  31. 1 0
      static/js/formly.min.js
  32. 0 0
      static/js/formly.min.js.map
  33. 93 0
      static/js/lodash.min.js
  34. 1723 0
      static/js/nya-bs-select.js
  35. 5 0
      static/js/nya-bs-select.min.js
  36. 0 0
      static/js/textAngular-rangy.min.js
  37. 791 0
      static/js/textAngular-sanitize.js
  38. 5 0
      static/js/textAngular-sanitize.min.js
  39. 3069 0
      static/js/textAngular.js
  40. 0 0
      static/js/textAngular.min.js
  41. 507 0
      static/js/toaster.js
  42. 12 0
      static/js/toaster.min.js

+ 33 - 0
angular_app/event-edit.html

@@ -23,6 +23,18 @@
                     <span class="input-group-addon min100">{{'Title' | translate}}</span>
                     <input type="text" class="form-control" placeholder="{{'Title' | translate}}" ng-model="event.title" ng-required="1">
                 </div>
+                <div class="input-group input-group-lg top5">
+                    <span class="input-group-addon min100">{{'Tagline' | translate}}</span>
+                    <input type="text" class="form-control" placeholder="{{'Tagline' | translate}}" ng-model="event.tagline">
+                </div>
+                <div class="input-group input-group-lg top5">
+                    <span class="input-group-addon min100">{{'Short summary' | translate}}</span>
+                    <input type="text" class="form-control" placeholder="{{'Short summary' | translate}}" ng-model="event.summary">
+                </div>
+                <div class="input-group top5">
+                    <span class="input-group-addon min100">{{'Long description' | translate}}</span>
+                    <textarea class="form-control" placeholder="{{'Long description' | translate}}" ng-model="event.description" rows="5"></textarea>
+                </div>
 
                 <div class="input-group top5 well form-horizontal" ng-controller="DatetimePickerCtrl">
                     <div class="form-group">
@@ -56,7 +68,28 @@
                     </div>
                 </div>
 
+                <div class="input-group input-group-lg top5">
+                    <span class="input-group-addon min100">{{'Where' | translate}}</span>
+                    <input type="text" class="form-control" placeholder="{{'Where' | translate}}" ng-model="event.where">
+                </div>
+
+                <div class="panel panel-info top10">
+                    <div class="panel-heading">
+                        <h1>{{'Registration form' | translate}}</h1>
+                    </div>
+                    <div class="panel-body">
+                        <eda-step-way-easy-form-gen eda-easy-form-generator-model="event.formSchema" eda-save-form-event="saveForm(edaEasyFormGeneratorModel)"></eda-step-way-easy-form-gen>
+                    </div>
+                </div>
+
+                <label></label>
                 <input type="submit" class="outside-screen" />
+                <div ng-class="{clearfix: true, alert: true, 'alert-success': !eventForm.$dirty, 'alert-danger': eventForm.$dirty}">
+                    <button type="button" class="btn btn-default pull-right" ng-click="save($event)" ng-disabled="!eventForm.$dirty">
+                        <span class="fa fa-floppy-o vcenter"></span>
+                        {{'save' | translate}}
+                    </button>
+                </div>
             </form>
         </div>
     </div>

+ 23 - 4
angular_app/index.html

@@ -5,17 +5,30 @@
         <meta charset="utf-8">
         <meta http-equiv="X-UA-Compatible" content="IE=edge">
         <meta name="viewport" content="width=device-width, initial-scale=1">
+        <script src="/static/js/jquery-2.1.3.min.js"></script>
         <script src="/static/js/angular.min.js"></script>
+        <script src="/static/js/bootstrap.min.js"></script>
+        <script src="/static/js/ui-bootstrap-tpls-0.14.3.min.js"></script>
         <script src="/static/js/angular-route.min.js"></script>
         <script src="/static/js/angular-animate.min.js"></script>
         <script src="/static/js/angular-touch.min.js"></script>
         <script src="/static/js/angular-resource.min.js"></script>
-        <script src="/static/js/angular-file-upload.min.js"></script> 
-        <script src="/static/js/ui-bootstrap-tpls-0.14.3.min.js"></script>
+        <script src="/static/js/angular-file-upload.min.js"></script>
         <script src="/static/js/angular-ui-router.min.js"></script>
         <script src="/static/js/angular-websocket.js"></script>
         <script src="/static/js/angular-translate.min.js"></script>
         <script src="/static/js/angular-translate-loader-static-files.min.js"></script>
+        <script src="/static/js/nya-bs-select.min.js"></script>
+        <script type="text/javascript" src="/static/js/eda.stepway.min.js"></script>
+        <script type="text/javascript" src="/static/js/textAngular-rangy.min.js"></script>
+        <script type="text/javascript" src="/static/js/textAngular-sanitize.min.js"></script>
+        <script type="text/javascript" src="/static/js/textAngular.min.js"></script>
+        <script type="text/javascript" src="/static/js/lodash.min.js"></script>
+        <script type="text/javascript" src="/static/js/toaster.min.js"></script>
+        <script type="text/javascript" src="/static/js/api-check.min.js"></script>
+        <script type="text/javascript" src="/static/js/formly.min.js"></script>
+        <script type="text/javascript" src="/static/js/angular-formly-templates-bootstrap.min.js"></script>
+
         <script src="/static/js/eventman.js"></script>
         <script src="/js/app.js"></script>
         <script src="/js/i18n.js"></script>
@@ -23,13 +36,19 @@
         <script src="/js/directives.js"></script>
         <script src="/js/services.js"></script>
         <script src="/js/controllers.js"></script>
-        <link href="/static/css/normalize.css" rel="stylesheet">
+
         <link href="/static/css/bootstrap.min.css" rel="stylesheet">
+		<link href="/static/css/animate.min.css" rel="stylesheet">
+		<link href="/static/css/textAngular.css" rel="stylesheet">
+		<link href="/static/css/toaster.min.css" rel="stylesheet">
+        <link href="/static/css/normalize.css" rel="stylesheet">
         <link href="/static/css/bootstrap-theme.min.css" rel="stylesheet">
         <link href="/static/css/font-awesome-4.3.0/css/font-awesome.min.css" rel="stylesheet">
+        <link href="/static/css/eda.stepway.min.css" rel="stylesheet">
+        <link href="/static/css/nya-bs-select.min.css" rel="stylesheet">
         <link href="/static/css/eventman.css" rel="stylesheet">
     </head>
-    
+
     <!--
         Copyright 2015-2016 Davide Alberani <da@erlug.linux.it>
                             RaspiBO <info@raspibo.org>

+ 2 - 1
angular_app/js/app.js

@@ -23,7 +23,8 @@ var eventManApp = angular.module('eventManApp', [
     'ui.router',
     'pascalprecht.translate',
     'angularFileUpload',
-    'angular-websocket'
+    'angular-websocket',
+	'eda.easyformGen.stepway'
 ]);
 
 

+ 9 - 2
angular_app/js/controllers.js

@@ -95,8 +95,8 @@ eventManControllers.controller('EventsListCtrl', ['$scope', 'Event', '$modal', '
 );
 
 
-eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event', 'EventTicket', 'Person', 'EventUpdates', '$stateParams', 'Setting', '$log', '$translate', '$rootScope',
-    function ($scope, $state, Event, EventTicket, Person, EventUpdates, $stateParams, Setting, $log, $translate, $rootScope) {
+eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event', 'EventTicket', 'Person', 'EventUpdates', '$stateParams', 'Setting', '$log', '$translate', '$rootScope', 'easyFormSteWayConfig',
+    function ($scope, $state, Event, EventTicket, Person, EventUpdates, $stateParams, Setting, $log, $translate, $rootScope, easyFormSteWayConfig) {
         $scope.personsOrder = ["name", "surname"];
         $scope.countAttendees = 0;
         $scope.message = {};
@@ -106,6 +106,8 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
 
         $scope.newTicket = $state.is('event.ticket.new');
 
+        $scope.event.formSchema = {};
+
         if ($stateParams.id) {
             $scope.event = Event.get($stateParams, function() {
                 if ($scope.newTicket) {
@@ -348,6 +350,11 @@ eventManControllers.controller('EventDetailsCtrl', ['$scope', '$state', 'Event',
             });
         };
 
+        $scope.saveForm = function(easyFormGeneratorModel) {
+            $scope.event.formSchema = easyFormGeneratorModel;
+            $scope.save();
+        };
+
         $scope.showMessage = function(cfg) {
             $scope.message.show(cfg);
         };

File diff suppressed because it is too large
+ 5 - 0
static/css/animate.min.css


File diff suppressed because it is too large
+ 0 - 0
static/css/dropzone.min.css


+ 862 - 0
static/css/eda.dragdropway.css

@@ -0,0 +1,862 @@
+
+/*!
+ * easyFormGenerator — drag and drop way — version
+ * Version 1.2.0
+ * Author : Erwan Datin (MacKentoch)
+ *Link: https://github.com/MacKentoch/easyFormGenerator
+ * License : 2015 MIT
+*/
+/*=========================================================
+    colors const
+===========================================================*/
+/*=========================================================
+    fonts const
+===========================================================*/
+/*=========================================================
+    common
+===========================================================*/
+/*=========================================================
+    common
+===========================================================*/
+body {
+  padding-top: 50px;
+  padding-bottom: 20px;
+  height: 100%;
+  background-color: #F2F2F2;
+  font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; }
+
+.margintop60px {
+  margin-top: 80px; }
+
+.margintop120px {
+  margin-top: 120px; }
+
+.vAlignMiddle {
+  vertical-align: middle; }
+
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
+  display: none !important; }
+
+/*=========================================================
+    bootstrap overrides
+===========================================================*/
+.nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
+  background-color: #0d87e9;
+  border-color: #2196f3; }
+
+/*correction bug sur theme bootstrap journal*/
+.modal-backdrop {
+  z-index: -1; }
+
+.navbar-right {
+  margin-right: 20px; }
+
+textarea, textarea.form-control, input.form-control,
+input[type=text], input[type=password], input[type=email],
+input[type=number], [type=text].form-control, [type=password].form-control,
+[type=email].form-control, [type=tel].form-control, [contenteditable].form-control {
+  font-size: 13px; }
+
+select, select.form-control {
+  font-size: 13px; }
+
+/*container to 100%*/
+@media (min-width: 1200px) {
+  .container {
+    width: 100%;
+    margin-right: 20px; } }
+
+@media (min-width: 768px) {
+  .container {
+    width: 100%;
+    margin-right: 20px; } }
+
+@media (min-width: 992px) {
+  .container {
+    width: 100%; } }
+
+.container-fluid {
+  margin-right: auto;
+  margin-left: auto; }
+
+.container-fluid padding {
+  left: 30px;
+  right: 30px; }
+
+.panel-default > .panel-heading {
+  color: #212121;
+  background-color: #fff;
+  border-color: #eee; }
+
+.navbar-nav > li > a {
+  line-height: 30px; }
+
+.navbar-default .navbar-brand {
+  line-height: initial; }
+
+.panel-heading {
+  background-color: none;
+  border-bottom: solid 1px #eee; }
+
+.navBtnGroup {
+  padding-top: 20px;
+  padding-bottom: 22px;
+  padding-right: 15px;
+  padding-left: 15px; }
+
+.table > thead > tr > th,
+.table > tbody > tr > th,
+.table > tfoot > tr > th,
+.table > thead > tr > td,
+.table > tbody > tr > td,
+.table > tfoot > tr > td {
+  vertical-align: middle; }
+
+a:hover {
+  color: #fff; }
+
+a:focus {
+  color: #fff; }
+
+/*Forms setup*/
+.form-control {
+  border-radius: 0;
+  box-shadow: none;
+  height: auto; }
+
+.float-label {
+  font-size: 10px; }
+
+.socialIcon {
+  font-size: 32px; }
+  .socialIcon:hover {
+    color: #F2F2F2; }
+
+input[type="text"].form-control,
+input[type="search"].form-control {
+  border: none;
+  border-bottom: 1px dotted #CFCFCF; }
+
+textarea {
+  border: 1px dotted #CFCFCF !important;
+  height: 130px !important; }
+
+/*Content Container*/
+.content-container {
+  background-color: #fff;
+  padding: 35px 20px;
+  margin-bottom: 20px; }
+
+h1.content-title {
+  font-size: 32px;
+  font-weight: 300;
+  text-align: center;
+  margin-top: 0;
+  margin-bottom: 20px;
+  font-family: "Open Sans", sans-serif !important; }
+
+/*paper theme bootstrap*/
+.form-control {
+  font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+  display: block;
+  width: 100%;
+  height: 39px;
+  padding: 8px 12px;
+  font-size: 13px;
+  line-height: 1.42857143;
+  color: #777777;
+  background-color: #ffffff;
+  background-image: none;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
+  -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+  transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; }
+
+.form-control:focus {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); }
+
+.form-control::-moz-placeholder {
+  color: #999999;
+  opacity: 1; }
+
+.form-control:-ms-input-placeholder {
+  color: #999999; }
+
+.form-control::-webkit-input-placeholder {
+  color: #999999; }
+
+.form-control[disabled],
+.form-control[readonly],
+fieldset[disabled] .form-control {
+  background-color: #eeeeee;
+  opacity: 1; }
+
+.form-control[disabled],
+fieldset[disabled] .form-control {
+  cursor: not-allowed; }
+
+textarea.form-control {
+  height: auto; }
+
+.fakeControl[disabled] {
+  background-color: #fff;
+  opacity: 1; }
+
+.fakeControl[disabled] {
+  cursor: all-scroll; }
+
+.radio input[type=radio].fakeCheck {
+  cursor: all-scroll; }
+
+.checkbox input[type=checkbox].fakeCheck {
+  cursor: all-scroll; }
+
+label.fakeCheck {
+  cursor: all-scroll; }
+
+/* animation ng-repeat 
+easy customize animation from here : http://www.nganimate.org/angularjs/ng-repeat/yo-yo-css3-keyframes-animation*/
+.animate-enter {
+  -webkit-animation: enter 600ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
+  animation: enter 600ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
+  display: block;
+  position: relative; }
+
+@-webkit-keyframes enter {
+  from {
+    opacity: 0;
+    height: 0px;
+    left: -70px; }
+  75% {
+    left: 15px; }
+  to {
+    opacity: 1;
+    height: 30px;
+    left: 0px; } }
+
+@keyframes enter {
+  from {
+    opacity: 0;
+    height: 0px;
+    left: -70px; }
+  75% {
+    left: 15px; }
+  to {
+    opacity: 1;
+    height: 30px;
+    left: 0px; } }
+
+.animate-leave {
+  -webkit-animation: leave 600ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
+  animation: leave 600ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
+  display: block;
+  position: relative; }
+
+@-webkit-keyframes leave {
+  to {
+    opacity: 0;
+    height: 0px;
+    left: -70px; }
+  25% {
+    left: 15px; }
+  from {
+    opacity: 1;
+    height: 30px;
+    left: 0px; } }
+
+@keyframes leave {
+  to {
+    opacity: 0;
+    height: 0px;
+    left: -70px; }
+  25% {
+    left: 15px; }
+  from {
+    opacity: 1;
+    height: 30px;
+    left: 0px; } }
+
+/* td table in myprofile */
+.tdCommun, .td40, .td60 {
+  height: 75px;
+  vertical-align: none; }
+
+.td40 {
+  width: 40%; }
+
+.td60 {
+  width: 60%; }
+
+.table-user-information > thead > tr > th,
+.table-user-information > tbody > tr > th,
+.table-user-information > tfoot > tr > th,
+.table-user-information > thead > tr > td,
+.table-user-information > tbody > tr > td,
+.table-user-information > tfoot > tr > td {
+  vertical-align: middle; }
+
+/*=========================================================
+   ANGULAR ANIMATIONS (in pair on animate.css)
+===========================================================*/
+/* animation on ng-switch */
+.switchAngularFadeIn .ng-enter {
+  -webkit-animation: fadeIn 1s;
+  -moz-animation: fadeIn 1s;
+  -ms-animation: fadeIn 1s;
+  animation: fadeIn 1s; }
+
+/*=========================================================
+   wfEdit
+===========================================================*/
+#pageWfEdit {
+  margin-top: 40px;
+  margin-/*    right:2%;
+    left:2%; */
+  padding-top: 30px;
+  padding-bottom: 30px;
+  background-color: #F2F2F2;
+  /* border-top: solid 3px white;
+  border-bottom: solid 3px white; */ }
+
+.lineCommandButtons {
+  padding-bottom: 25px; }
+
+.addNewLine {
+  font-size: 24px;
+  vertical-align: middle;
+  color: #666666; }
+
+#editor {
+  margin-top: 0px;
+  margin-bottom: 0px;
+  height: 150px;
+  background-color: #52B3D9;
+  border-bottom: solid 1px #2C3E50; }
+
+#editor-content {
+  /* padding-top: 25px; */
+  /* padding-bottom: 25px; */
+  text-align: center;
+  color: white;
+  font-size: 13px; }
+
+#preview {
+  margin-top: 0px;
+  margin-bottom: 0px;
+  /* background-color: #E4F1FE; */
+  border: solid 1 px #2C3E50;
+  -moz-box-shadow: 0px 0px 10px 0px #ddd;
+  -webkit-box-shadow: 0px 10px 5px 0px #ddd;
+  -o-box-shadow: 0px 0px 10px 0px #ddd;
+  box-shadow: 0px 0px 10px 0px #ddd;
+  filter: progid:DXImageTransform.Microsoft.Shadow(color=#ddd, Direction=NaN, Strength=10); }
+
+#preview-content {
+  background-color: #FFF;
+  padding-bottom: 25px;
+  /*text-align: center;*/
+  color: black;
+  /*font-size: 13px;*/ }
+
+#commandPanel {
+  padding-top: 25px;
+  padding-bottom: 25px; }
+
+/*  #commandPanel.affix {
+    position: fixed;
+    top: 0;
+    width:25%;
+    margin-top: -45px;
+  } */
+#visualPanel {
+  padding-top: 25px;
+  padding-bottom: 25px; }
+
+.customPagerButton {
+  width: 100px; }
+
+.numberOfColumnsLabel {
+  font-size: 32px;
+  text-align: center;
+  padding-top: 25%;
+  padding-bottom: 25%;
+  font-weight: bold;
+  text-align: center;
+  color: #666666; }
+
+.btnMinusColumns {
+  margin-top: 10px; }
+
+.btnAddColumns {
+  margin-top: 10px; }
+
+.numberOfcolumsText {
+  color: #666666; }
+
+.greyText {
+  color: #666666; }
+
+.blackText {
+  color: #000; }
+
+/*=========================================================
+  ANIMATE NG-SWITCH
+=========================================================*/
+.animate-switch-container {
+  position: relative;
+  background: white;
+  border-top: solid 1px #ECECEC;
+  border-bottom: solid 1px #ECECEC;
+  overflow: scroll; }
+  .animate-switch-container .well {
+    background-color: #89C4F4;
+    margin-bottom: 0px;
+    color: #ECECEC; }
+
+.linesList {
+  margin-top: 5px; }
+
+/*switch 1 : move top*/
+.animate-switch {
+  padding: 10px; }
+
+.animate-switch.ng-animate {
+  -webkit-transition: all cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.6s;
+  transition: all cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.6s;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0; }
+
+.animate-switch.ng-leave.ng-leave-active,
+.animate-switch.ng-enter {
+  left: 0px;
+  top: -500px;
+  opacity: 0; }
+
+.animate-switch.ng-leave,
+.animate-switch.ng-enter.ng-enter-active {
+  left: 0px;
+  top: 0px;
+  opacity: 0.8; }
+
+/*=========================================================
+  STEP INDICATOR
+=========================================================*/
+.stepwizardTopmargin {
+  margin-top: 25px; }
+
+.stepwizard-step p {
+  margin-top: 10px;
+  color: #666666; }
+
+.stepwizard-row {
+  display: table-row; }
+
+.stepwizard {
+  display: table;
+  width: 100%;
+  position: relative; }
+
+.stepwizard-step button[disabled] {
+  opacity: 1 !important;
+  filter: alpha(opacity=100) !important; }
+
+.stepwizard-row:before {
+  top: 14px;
+  bottom: 0;
+  position: absolute;
+  content: " ";
+  width: 100%;
+  height: 1px;
+  background-color: #ccc;
+  z-order: 0; }
+
+.stepwizard-step {
+  display: table-cell;
+  text-align: center;
+  position: relative; }
+
+.btn-circle {
+  width: 30px;
+  height: 30px;
+  text-align: center;
+  padding: 6px 0;
+  font-size: 12px;
+  line-height: 1.428571429;
+  border-radius: 15px; }
+
+.panelDebugScope {
+  color: #000; }
+
+/*=========================================================
+  Modal add control
+=========================================================*/
+.texteRouge {
+  color: #CF000F; }
+
+.editPropertiesLabel {
+  margin-top: 6px; }
+
+.textControlLabel {
+  color: #000; }
+
+.marginTopFivepixels {
+  margin-top: 5px; }
+
+.marginTopTenpixels {
+  margin-top: 5px; }
+
+.checkboxCssCorrection {
+  font-size: 4px; }
+
+.noGroupText {
+  margin-bottom: 0px; }
+
+.nya-bs-select {
+  margin-bottom: 16px; }
+
+.ta-scroll-window {
+  margin-bottom: 16px; }
+
+.formly-field-checkbox {
+  margin-top: 28px; }
+
+#inputAddNewRadioOption {
+  margin-bottom: 20px; }
+
+#inputAddNewBasicOption {
+  margin-bottom: 20px; }
+
+#inputAddNewGroupedOption {
+  margin-bottom: 20px; }
+
+#inputAddNewGroupGroupedOption {
+  margin-bottom: 20px; }
+
+.editGroupedSelectnyaSelect {
+  margin-bottom: 0px; }
+
+.heading-preview {
+  cursor: pointer; }
+
+/*=========================================================
+   specific drag and drop test (will change or be deleted)
+===========================================================*/
+/***************************** pageSlide directive css *****************************/
+/* Needed for hiding crollbars when pushing */
+html {
+  overflow-x: hidden; }
+
+.ng-pageslide {
+  background: #eee;
+  box-shadow: 5px 1px 12px 0px rgba(188, 183, 183, 0.7);
+  -moz-box-shadow: 5px 1px 12px 0px rgba(188, 183, 183, 0.7);
+  -webkit-box-shadow: 5px 1px 12px 0px rgba(188, 183, 183, 0.7); }
+
+#controlEditLeftPanel {
+  margin-top: 95px;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  padding-right: 10px;
+  padding-left: 10px;
+  height: 85%;
+  overflow-y: scroll; }
+
+.separator10pixel {
+  margin-top: 5px;
+  margin-bottom: 5px;
+  height: 25px; }
+
+.panelControlWidth {
+  width: 45%; }
+
+/***************************** Required styles *****************************/
+/**
+ * For the correct positioning of the placeholder element, the dnd-list and
+ * it's children must have position: relative
+ */
+.advancedDemo ul[dnd-list],
+.advancedDemo ul[dnd-list] > li {
+  position: relative;
+  border: solid 1px #F1F1F1; }
+
+/***************************** Dropzone Styling *****************************/
+/**
+ * The dnd-list should always have a min-height,
+ * otherwise you can't drop to it once it's empty
+ */
+.advancedDemo .dropzone ul[dnd-list] {
+  min-height: 42px;
+  margin: 0px;
+  padding-left: 0px; }
+
+/**
+ * The dnd-lists's child elements currently MUST have
+ * position: relative. Otherwise we can not determine
+ * whether the mouse pointer is in the upper or lower
+ * half of the element we are dragging over. In other
+ * browsers we can use event.offsetY for this.
+ */
+.advancedDemo .dropzone li {
+  display: block; }
+
+/**
+ * Reduce opacity of elements during the drag operation. This allows the user
+ * to see where he is dropping his element, even if the element is huge. The
+ * .dndDragging class is automatically set during the drag operation.
+ */
+.advancedDemo .dropzone .dndDragging {
+  opacity: 0.7;
+  border: 2px dotted #000; }
+
+/**
+ * The dndDraggingSource class will be applied to the source element of a drag
+ * operation. It makes sense to hide it to give the user the feeling that he's
+ * actually moving it. Note that the source element has also .dndDragging class.
+ */
+.advancedDemo .dropzone .dndDraggingSource {
+  display: none; }
+
+/**
+ * An element with .dndPlaceholder class will be added as child of the dnd-list
+ * while the user is dragging over it.
+ */
+.advancedDemo .dropzone .dndPlaceholder {
+  background-color: #ddd !important;
+  min-height: 155px;
+  display: block;
+  position: relative;
+  -moz-box-shadow: inset 0px 0px 5px 0px #343434;
+  -webkit-box-shadow: inset 0px 0px 5px 0px #343434;
+  -o-box-shadow: inset 0px 0px 5px 0px #343434;
+  box-shadow: inset 0px 0px 5px 0px #343434;
+  filter: progid:DXImageTransform.Microsoft.Shadow(color=#343434, Direction=NaN, Strength=5); }
+
+/***************************** Element type specific styles *****************************/
+.advancedDemo .dropzone .itemlist {
+  min-height: 115px !important; }
+
+ul.itemlist.ng-scope {
+  border: none; }
+
+.effect1ForDragDropItem, #itemContent {
+  -webkit-box-shadow: 0 10px 6px -6px #777;
+  -moz-box-shadow: 0 10px 6px -6px #777;
+  box-shadow: 0 10px 6px -6px #777;
+  min-height: 115px; }
+
+.advancedDemo .dropzone .itemlist > li {
+  border: none;
+  min-width: 180px;
+  border-radius: .25em;
+  float: left;
+  font-weight: 700;
+  height: 135px; }
+
+#itemContent {
+  background-color: #fff;
+  border: 2px outset #0d87e9;
+  border-radius: .25em;
+  color: #fff;
+  float: left;
+  font-weight: 700;
+  height: 115px;
+  margin: 5px;
+  padding: 3px;
+  text-align: center;
+  width: 100%;
+  cursor: all-scroll; }
+
+.dragItemtextarea {
+  height: 60px; }
+
+textarea {
+  border: 1px dotted #CFCFCF !important;
+  height: 50px !important; }
+
+.advancedDemo .dropzone .container-element {
+  margin: 10px; }
+
+.isCollapsableZone {
+  cursor: pointer; }
+
+.buttonHeaderAddNewLine {
+  margin-top: 15px;
+  margin-bottom: 0px; }
+
+.buttonCloneLineHidden {
+  visibility: hidden; }
+
+.buttonCloseLine {
+  margin-top: -10px;
+  margin-right: -10px;
+  border-radius: 50%;
+  height: 45px; }
+
+.interligne {
+  padding-bottom: 10px; }
+
+.confirmLineDelete {
+  margin-top: 0;
+  margin-bottom: 0;
+  background-color: #F5F5F5;
+  opacity: 1.0;
+  -webkit-animation: shake 1.3s infinite;
+  -moz-animation: shake 1.3s infinite;
+  -ms-animation: shake 1.3s infinite;
+  -o-animation: shake 1.3s infinite;
+  animation: shake 1.3s infinite; }
+
+.demoddDatepicker {
+  padding-left: 0;
+  margin-left: 0; }
+
+.selectfordemo {
+  margin-top: 30px; }
+
+/*=========================================================
+   animation shake
+===========================================================*/
+@-webkit-keyframes shake {
+  0%, 100% {
+    -webkit-transform: translate3d(0, 0, 0);
+    transform: translate3d(0, 0, 0); }
+  10%, 30%, 50%, 70%, 90% {
+    -webkit-transform: translate3d(-10px, 0, 0);
+    transform: translate3d(-10px, 0, 0); }
+  20%, 40%, 60%, 80% {
+    -webkit-transform: translate3d(10px, 0, 0);
+    transform: translate3d(10px, 0, 0); } }
+
+@keyframes shake {
+  0%, 100% {
+    -webkit-transform: translate3d(0, 0, 0);
+    transform: translate3d(0, 0, 0); }
+  10%, 30%, 50%, 70%, 90% {
+    -webkit-transform: translate3d(-10px, 0, 0);
+    transform: translate3d(-10px, 0, 0); }
+  20%, 40%, 60%, 80% {
+    -webkit-transform: translate3d(10px, 0, 0);
+    transform: translate3d(10px, 0, 0); } }
+
+.shake {
+  -webkit-animation-name: shake;
+  animation-name: shake; }
+
+.previewControlinItem {
+  margin-top: 2%;
+  background-color: #fff;
+  padding-top: 5px;
+  padding-bottom: 5px;
+  padding-left: 10px;
+  padding-right: 10px;
+  border-radius: 5px;
+  color: #F1F1F1;
+  font-size: 18px; }
+
+.vertical-line {
+  border-left: thick solid #000; }
+
+/*=========================================================
+   animation glyphicon rotate
+===========================================================*/
+.gly-spin {
+  -webkit-animation: spin 2s infinite linear;
+  -moz-animation: spin 2s infinite linear;
+  -o-animation: spin 2s infinite linear;
+  animation: spin 2s infinite linear; }
+
+@-moz-keyframes spin {
+  0% {
+    -moz-transform: rotate(0deg); }
+  100% {
+    -moz-transform: rotate(359deg); } }
+
+@-webkit-keyframes spin {
+  0% {
+    -webkit-transform: rotate(0deg); }
+  100% {
+    -webkit-transform: rotate(359deg); } }
+
+@-o-keyframes spin {
+  0% {
+    -o-transform: rotate(0deg); }
+  100% {
+    -o-transform: rotate(359deg); } }
+
+@keyframes spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+    transform: rotate(0deg); }
+  100% {
+    -webkit-transform: rotate(359deg);
+    transform: rotate(359deg); } }
+
+.gly-rotate-90 {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
+  -webkit-transform: rotate(90deg);
+  -moz-transform: rotate(90deg);
+  -ms-transform: rotate(90deg);
+  -o-transform: rotate(90deg);
+  transform: rotate(90deg); }
+
+.gly-rotate-180 {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
+  -webkit-transform: rotate(180deg);
+  -moz-transform: rotate(180deg);
+  -ms-transform: rotate(180deg);
+  -o-transform: rotate(180deg);
+  transform: rotate(180deg); }
+
+.gly-rotate-270 {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
+  -webkit-transform: rotate(270deg);
+  -moz-transform: rotate(270deg);
+  -ms-transform: rotate(270deg);
+  -o-transform: rotate(270deg);
+  transform: rotate(270deg); }
+
+.gly-flip-horizontal {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);
+  -webkit-transform: scale(-1, 1);
+  -moz-transform: scale(-1, 1);
+  -ms-transform: scale(-1, 1);
+  -o-transform: scale(-1, 1);
+  transform: scale(-1, 1); }
+
+.gly-flip-vertical {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);
+  -webkit-transform: scale(1, -1);
+  -moz-transform: scale(1, -1);
+  -ms-transform: scale(1, -1);
+  -o-transform: scale(1, -1);
+  transform: scale(1, -1); }
+
+.rightClickCtrl {
+  opacity: 1.0;
+  -webkit-transition: opacity .3s ease-in;
+  -moz-transition: opacity .3s ease-in;
+  -o-transition: opacity .3s ease-in;
+  transition: opacity .3s ease-in; }
+
+.rightClickCtrlSelected {
+  opacity: 0.5; }
+
+
+/*=========================================================
+    colors const
+===========================================================*/
+/*=========================================================
+    fonts const
+===========================================================*/
+/*=========================================================
+    common
+===========================================================*/

File diff suppressed because it is too large
+ 8 - 0
static/css/eda.dragdropway.min.css


+ 70 - 0
static/css/eda.easyFormViewer.css

@@ -0,0 +1,70 @@
+
+/*!
+   * easyFormViewer
+   * Version 1.2.0
+   * Author : Erwan Datin (MacKentoch)
+   *Link: https://github.com/MacKentoch/easyFormGenerator
+   * License : 2015 MIT
+  */
+  /*=========================================================
+    fonts const
+===========================================================*/
+body {
+  margin: 20px;
+  font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; }
+
+.ta-scroll-window {
+  margin-bottom: 16px; }
+
+/**
+ * formly fields css
+ */
+.formly-field {
+  margin-bottom: 20px; }
+
+.validation {
+  position: absolute;
+  font-size: .8em;
+  color: #a94442; }
+
+.formly-template-wrapper {
+  position: relative; }
+
+.error-messages {
+  position: relative; }
+
+.error-messages, .message, .validation {
+  opacity: 1;
+  transition: .3s linear all; }
+
+.message {
+  font-size: .8em;
+  position: absolute;
+  width: 100%;
+  color: #a94442;
+  margin-top: 2px; }
+
+.error-messages.ng-enter.ng-enter-active,
+.message.ng-enter.ng-enter-active,
+.validation.ng-enter.ng-enter-active {
+  opacity: 1; }
+
+.error-messages.ng-enter,
+.message.ng-enter,
+.validation.ng-enter {
+  opacity: 0; }
+
+.error-messages.ng-leave,
+.message.ng-leave,
+.validation.ng-leave {
+  opacity: 1; }
+
+.error-messages.ng-leave-active,
+.message.ng-leave-active,
+.validation.ng-leave-active {
+  opacity: 0; }
+
+
+/*=========================================================
+    fonts const
+===========================================================*/

+ 9 - 0
static/css/eda.easyFormViewer.min.css

@@ -0,0 +1,9 @@
+
+/*!
+   * easyFormViewer
+   * Version 1.2.0
+   * Author : Erwan Datin (MacKentoch)
+   *Link: https://github.com/MacKentoch/easyFormGenerator
+   * License : 2015 MIT
+  */
+  .message,.validation{font-size:.8em;color:#a94442}body{margin:20px;font-family:Roboto,"Helvetica Neue",Helvetica,Arial,sans-serif}.ta-scroll-window{margin-bottom:16px}.formly-field{margin-bottom:20px}.validation{position:absolute}.error-messages,.formly-template-wrapper{position:relative}.error-messages,.message,.validation{opacity:1;transition:.3s linear all}.message{position:absolute;width:100%;margin-top:2px}.error-messages.ng-enter.ng-enter-active,.message.ng-enter.ng-enter-active,.validation.ng-enter.ng-enter-active{opacity:1}.error-messages.ng-enter,.message.ng-enter,.validation.ng-enter{opacity:0}.error-messages.ng-leave,.message.ng-leave,.validation.ng-leave{opacity:1}.error-messages.ng-leave-active,.message.ng-leave-active,.validation.ng-leave-active{opacity:0}

+ 591 - 0
static/css/eda.stepway.css

@@ -0,0 +1,591 @@
+
+/*!
+ * easyFormGenerator — step way — version
+ * Version 1.2.1-beta1
+ * Author : Erwan Datin (MacKentoch)
+ *Link: https://github.com/MacKentoch/easyFormGenerator
+ * License : 2015 MIT
+*/
+/*=========================================================
+    colors const
+===========================================================*/
+/*=========================================================
+    fonts const
+===========================================================*/
+/*=========================================================
+    common
+===========================================================*/
+/*=========================================================
+    common
+===========================================================*/
+body {
+  padding-top: 50px;
+  padding-bottom: 20px;
+  height: 100%;
+  background-color: #F2F2F2;
+  font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; }
+
+.margintop60px {
+  margin-top: 80px; }
+
+.margintop120px {
+  margin-top: 120px; }
+
+.vAlignMiddle {
+  vertical-align: middle; }
+
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
+  display: none !important; }
+
+/*=========================================================
+    bootstrap overrides
+===========================================================*/
+.nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
+  background-color: #0d87e9;
+  border-color: #2196f3; }
+
+/*correction bug sur theme bootstrap journal*/
+.modal-backdrop {
+  z-index: -1; }
+
+.navbar-right {
+  margin-right: 20px; }
+
+textarea, textarea.form-control, input.form-control,
+input[type=text], input[type=password], input[type=email],
+input[type=number], [type=text].form-control, [type=password].form-control,
+[type=email].form-control, [type=tel].form-control, [contenteditable].form-control {
+  font-size: 13px; }
+
+select, select.form-control {
+  font-size: 13px; }
+
+/*container to 100%*/
+@media (min-width: 1200px) {
+  .container {
+    width: 100%;
+    margin-right: 20px; } }
+
+@media (min-width: 768px) {
+  .container {
+    width: 100%;
+    margin-right: 20px; } }
+
+@media (min-width: 992px) {
+  .container {
+    width: 100%; } }
+
+.container-fluid {
+  margin-right: auto;
+  margin-left: auto; }
+
+.container-fluid padding {
+  left: 30px;
+  right: 30px; }
+
+.panel-default > .panel-heading {
+  color: #212121;
+  background-color: #fff;
+  border-color: #eee; }
+
+.navbar-nav > li > a {
+  line-height: 30px; }
+
+.navbar-default .navbar-brand {
+  line-height: initial; }
+
+.panel-heading {
+  background-color: none;
+  border-bottom: solid 1px #eee; }
+
+.navBtnGroup {
+  padding-top: 20px;
+  padding-bottom: 22px;
+  padding-right: 15px;
+  padding-left: 15px; }
+
+.table > thead > tr > th,
+.table > tbody > tr > th,
+.table > tfoot > tr > th,
+.table > thead > tr > td,
+.table > tbody > tr > td,
+.table > tfoot > tr > td {
+  vertical-align: middle; }
+
+a:hover {
+  color: #fff; }
+
+a:focus {
+  color: #fff; }
+
+/*Forms setup*/
+.form-control {
+  border-radius: 0;
+  box-shadow: none;
+  height: auto; }
+
+.float-label {
+  font-size: 10px; }
+
+.socialIcon {
+  font-size: 32px; }
+  .socialIcon:hover {
+    color: #F2F2F2; }
+
+input[type="text"].form-control,
+input[type="search"].form-control {
+  border: none;
+  border-bottom: 1px dotted #CFCFCF; }
+
+textarea {
+  border: 1px dotted #CFCFCF !important;
+  height: 130px !important; }
+
+/*Content Container*/
+.content-container {
+  background-color: #fff;
+  padding: 35px 20px;
+  margin-bottom: 20px; }
+
+h1.content-title {
+  font-size: 32px;
+  font-weight: 300;
+  text-align: center;
+  margin-top: 0;
+  margin-bottom: 20px;
+  font-family: "Open Sans", sans-serif !important; }
+
+/*paper them bootstrap*/
+.form-control {
+  font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+  display: block;
+  width: 100%;
+  height: 39px;
+  padding: 8px 12px;
+  font-size: 13px;
+  line-height: 1.42857143;
+  color: #777777;
+  background-color: #ffffff;
+  background-image: none;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
+  -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+  transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; }
+
+.form-control:focus {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); }
+
+.form-control::-moz-placeholder {
+  color: #999999;
+  opacity: 1; }
+
+.form-control:-ms-input-placeholder {
+  color: #999999; }
+
+.form-control::-webkit-input-placeholder {
+  color: #999999; }
+
+.form-control[disabled],
+.form-control[readonly],
+fieldset[disabled] .form-control {
+  background-color: #eeeeee;
+  opacity: 1; }
+
+.form-control[disabled],
+fieldset[disabled] .form-control {
+  cursor: not-allowed; }
+
+textarea.form-control {
+  height: auto; }
+
+/* animation ng-repeat in mailbox 
+easy customize animation from here : http://www.nganimate.org/angularjs/ng-repeat/yo-yo-css3-keyframes-animation*/
+.animate-enter {
+  -webkit-animation: enter 600ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
+  animation: enter 600ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
+  display: block;
+  position: relative; }
+
+@-webkit-keyframes enter {
+  from {
+    opacity: 0;
+    height: 0px;
+    left: -70px; }
+  75% {
+    left: 15px; }
+  to {
+    opacity: 1;
+    height: 30px;
+    left: 0px; } }
+
+@keyframes enter {
+  from {
+    opacity: 0;
+    height: 0px;
+    left: -70px; }
+  75% {
+    left: 15px; }
+  to {
+    opacity: 1;
+    height: 30px;
+    left: 0px; } }
+
+.animate-leave {
+  -webkit-animation: leave 600ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
+  animation: leave 600ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
+  display: block;
+  position: relative; }
+
+@-webkit-keyframes leave {
+  to {
+    opacity: 0;
+    height: 0px;
+    left: -70px; }
+  25% {
+    left: 15px; }
+  from {
+    opacity: 1;
+    height: 30px;
+    left: 0px; } }
+
+@keyframes leave {
+  to {
+    opacity: 0;
+    height: 0px;
+    left: -70px; }
+  25% {
+    left: 15px; }
+  from {
+    opacity: 1;
+    height: 30px;
+    left: 0px; } }
+
+/* td table in myprofile */
+.tdCommun, .td40, .td60 {
+  height: 75px;
+  vertical-align: none; }
+
+.td40 {
+  width: 40%; }
+
+.td60 {
+  width: 60%; }
+
+.table-user-information > thead > tr > th,
+.table-user-information > tbody > tr > th,
+.table-user-information > tfoot > tr > th,
+.table-user-information > thead > tr > td,
+.table-user-information > tbody > tr > td,
+.table-user-information > tfoot > tr > td {
+  vertical-align: middle; }
+
+/*=========================================================
+   ANGULAR ANIMATIONS (in pair on animate.css)
+===========================================================*/
+/* animation on ng-switch */
+.switchAngularFadeIn .ng-enter {
+  -webkit-animation: fadeIn 1s;
+  -moz-animation: fadeIn 1s;
+  -ms-animation: fadeIn 1s;
+  animation: fadeIn 1s; }
+
+/*=========================================================
+   wfEdit
+===========================================================*/
+#pageWfEdit {
+  margin-top: 40px;
+  margin-/*    right:2%;
+    left:2%; */
+  padding-top: 30px;
+  padding-bottom: 30px;
+  background-color: #F2F2F2;
+  /* border-top: solid 3px white;
+  border-bottom: solid 3px white; */ }
+
+.lineCommandButtons {
+  padding-bottom: 25px; }
+
+.addNewLine {
+  font-size: 24px;
+  vertical-align: middle;
+  color: #666666; }
+
+#editor {
+  margin-top: 0px;
+  margin-bottom: 0px;
+  height: 150px;
+  background-color: #52B3D9;
+  border-bottom: solid 1px #2C3E50; }
+
+#editor-content {
+  /* padding-top: 25px; */
+  /* padding-bottom: 25px; */
+  text-align: center;
+  color: white;
+  font-size: 13px; }
+
+#preview {
+  margin-top: 0px;
+  margin-bottom: 0px;
+  /* background-color: #E4F1FE; */
+  border: solid 1 px #2C3E50;
+  -moz-box-shadow: 0px 0px 10px 0px #ddd;
+  -webkit-box-shadow: 0px 10px 5px 0px #ddd;
+  -o-box-shadow: 0px 0px 10px 0px #ddd;
+  box-shadow: 0px 0px 10px 0px #ddd;
+  filter: progid:DXImageTransform.Microsoft.Shadow(color=#ddd, Direction=NaN, Strength=10); }
+
+#preview-content {
+  background-color: #FFF;
+  padding-bottom: 25px;
+  /*text-align: center;*/
+  color: black;
+  /*font-size: 13px;*/ }
+
+#commandPanel {
+  padding-top: 25px;
+  padding-bottom: 25px; }
+
+/*  #commandPanel.affix {
+    position: fixed;
+    top: 0;
+    width:25%;
+    margin-top: -45px;
+  } */
+#visualPanel {
+  padding-top: 25px;
+  padding-bottom: 25px; }
+
+.customPagerButton {
+  width: 145px;
+  margin-left: 2px;
+  margin-right: 2px; }
+
+.numberOfColumnsLabel {
+  font-size: 32px;
+  text-align: center;
+  padding-top: 25%;
+  padding-bottom: 25%;
+  font-weight: bold;
+  text-align: center;
+  color: #666666; }
+
+.btnMinusColumns {
+  margin-top: 10px; }
+
+.btnAddColumns {
+  margin-top: 10px; }
+
+.numberOfcolumsText {
+  color: #666666; }
+
+.greyText {
+  color: #666666; }
+
+.blackText {
+  color: #000; }
+
+/*=========================================================
+  ANIMATE NG-SWITCH
+=========================================================*/
+.animate-switch-container {
+  position: relative;
+  background: white;
+  border-top: solid 1px #ECECEC;
+  border-bottom: solid 1px #ECECEC;
+  height: 750px;
+  overflow: scroll; }
+  .animate-switch-container .well {
+    background-color: #89C4F4;
+    margin-bottom: 0px;
+    color: #ECECEC; }
+
+.linesList {
+  margin-top: 5px; }
+
+/*switch 1 : move top*/
+.animate-switch {
+  padding: 10px; }
+
+.animate-switch.ng-animate {
+  -webkit-transition: all cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.6s;
+  transition: all cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.6s;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0; }
+
+.animate-switch.ng-leave.ng-leave-active,
+.animate-switch.ng-enter {
+  left: 0px;
+  top: -500px;
+  opacity: 0; }
+
+.animate-switch.ng-leave,
+.animate-switch.ng-enter.ng-enter-active {
+  left: 0px;
+  top: 0px;
+  opacity: 0.8; }
+
+/*=========================================================
+  STEP INDICATOR
+=========================================================*/
+.stepwizardTopmargin {
+  margin-top: 25px; }
+
+.stepwizard-step p {
+  margin-top: 10px;
+  color: #666666; }
+
+.stepwizard-row {
+  display: table-row; }
+
+.stepwizard {
+  display: table;
+  width: 100%;
+  position: relative; }
+
+.stepwizard-step button[disabled] {
+  opacity: 1 !important;
+  filter: alpha(opacity=100) !important; }
+
+.stepwizard-row:before {
+  top: 14px;
+  bottom: 0;
+  position: absolute;
+  content: " ";
+  width: 100%;
+  height: 1px;
+  background-color: #ccc;
+  z-order: 0; }
+
+.stepwizard-step {
+  display: table-cell;
+  text-align: center;
+  position: relative; }
+
+.btn-circle {
+  width: 30px;
+  height: 30px;
+  text-align: center;
+  padding: 6px 0;
+  font-size: 12px;
+  line-height: 1.428571429;
+  border-radius: 15px; }
+
+.panelDebugScope {
+  color: #000; }
+
+/*=========================================================
+  Modal add control
+=========================================================*/
+.texteRouge {
+  color: #CF000F; }
+
+.editPropertiesLabel {
+  margin-top: 6px; }
+
+.textControlLabel {
+  color: #000; }
+
+.marginTopFivepixels {
+  margin-top: 5px; }
+
+.marginTopTenpixels {
+  margin-top: 5px; }
+
+.checkboxCssCorrection {
+  font-size: 4px; }
+
+.noGroupText {
+  margin-bottom: 0px; }
+
+.nya-bs-select {
+  margin-bottom: 16px; }
+
+.ta-scroll-window {
+  margin-bottom: 16px; }
+
+.formly-field-checkbox {
+  margin-top: 28px; }
+
+#inputAddNewRadioOption {
+  margin-bottom: 20px; }
+
+#inputAddNewBasicOption {
+  margin-bottom: 20px; }
+
+#inputAddNewGroupedOption {
+  margin-bottom: 20px; }
+
+#inputAddNewGroupGroupedOption {
+  margin-bottom: 20px; }
+
+.editGroupedSelectnyaSelect {
+  margin-bottom: 0px; }
+
+/**
+ * formly fiedl css
+ */
+.formly-field {
+  margin-bottom: 20px; }
+
+.validation {
+  position: absolute;
+  font-size: .8em;
+  color: #a94442; }
+
+.formly-template-wrapper {
+  position: relative; }
+
+.error-messages {
+  position: relative; }
+
+.error-messages, .message, .validation {
+  opacity: 1;
+  transition: .3s linear all; }
+
+.message {
+  font-size: .8em;
+  position: absolute;
+  width: 100%;
+  color: #a94442;
+  margin-top: 2px; }
+
+.error-messages.ng-enter.ng-enter-active,
+.message.ng-enter.ng-enter-active,
+.validation.ng-enter.ng-enter-active {
+  opacity: 1; }
+
+.error-messages.ng-enter,
+.message.ng-enter,
+.validation.ng-enter {
+  opacity: 0; }
+
+.error-messages.ng-leave,
+.message.ng-leave,
+.validation.ng-leave {
+  opacity: 1; }
+
+.error-messages.ng-leave-active,
+.message.ng-leave-active,
+.validation.ng-leave-active {
+  opacity: 0; }
+
+
+/*=========================================================
+    colors const
+===========================================================*/
+/*=========================================================
+    fonts const
+===========================================================*/
+/*=========================================================
+    common
+===========================================================*/

File diff suppressed because it is too large
+ 8 - 0
static/css/eda.stepway.min.css


File diff suppressed because it is too large
+ 0 - 0
static/css/eda.textAngular.min.css


File diff suppressed because it is too large
+ 7 - 0
static/css/loading-bar.min.css


+ 226 - 0
static/css/nya-bs-select.css

@@ -0,0 +1,226 @@
+/**
+ * nya-bootstrap-select v2.1.6
+ * Copyright 2014 Nyasoft
+ * Licensed under MIT license
+ */
+.nya-bs-select {
+  /*width: 220px\9; IE8 and below*/
+  width: 220px \0;
+  /*IE9 and below*/
+  padding: 0;
+  margin: 0;
+}
+.nya-bs-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) {
+  width: 220px;
+}
+.nya-bs-select.form-control {
+  margin-bottom: 0;
+  padding: 0;
+  border: none;
+}
+.nya-bs-select.form-control:not([class*="col-"]) {
+  width: 100%;
+}
+.nya-bs-select .dropdown-toggle {
+  width: 100%;
+  padding-right: 25px;
+}
+.nya-bs-select .dropdown-toggle .filter-option,
+.nya-bs-select .dropdown-toggle .special-title {
+  overflow: hidden;
+  width: 100%;
+  text-align: left;
+}
+.nya-bs-select .dropdown-toggle .filter-option {
+  display: inline-block;
+}
+.nya-bs-select .dropdown-toggle .special-title {
+  display: none;
+}
+.nya-bs-select .dropdown-toggle.show-special-title .filter-option {
+  display: none;
+}
+.nya-bs-select .dropdown-toggle.show-special-title .special-title {
+  display: inline-block;
+}
+.nya-bs-select .dropdown-toggle .caret {
+  position: absolute;
+  top: 50%;
+  right: 12px;
+  margin-top: -2px;
+  vertical-align: middle;
+}
+.nya-bs-select.fit-width {
+  width: auto !important;
+}
+.nya-bs-select.btn-group:not(.input-group-btn),
+.nya-bs-select.btn-group[class*="col-"] {
+  float: none;
+  display: inline-block;
+  margin-left: 0;
+}
+.nya-bs-select.btn-group.dropdown-menu-right,
+.nya-bs-select.btn-group[class*="col-"].dropdown-menu-right,
+.row-fluid .nya-bs-select.btn-group[class*="col-"].dropdown-menu-right {
+  float: right;
+}
+.nya-bs-select.btn-group[class*="col-"] .btn {
+  width: 100%;
+}
+.nya-bs-select.btn-group .dropdown-menu {
+  min-width: 100%;
+  z-index: 1035;
+  box-sizing: border-box;
+}
+.nya-bs-select.btn-group .dropdown-menu.inner {
+  position: static;
+  border: 0;
+  padding: 0;
+  margin: 0;
+  border-radius: 0;
+  box-shadow: none;
+}
+.nya-bs-select.btn-group .dropdown-menu li {
+  position: relative;
+}
+.nya-bs-select.btn-group .dropdown-menu li.disabled a {
+  cursor: not-allowed;
+}
+.nya-bs-select.btn-group .dropdown-menu li.not-match {
+  display: none;
+}
+.nya-bs-select.btn-group .dropdown-menu li.no-search-result {
+  display: none;
+  padding: 3px;
+  background: #f5f5f5;
+  margin: 0 5px;
+}
+.nya-bs-select.btn-group .dropdown-menu li.no-search-result.show {
+  display: list-item;
+}
+.nya-bs-select.btn-group .dropdown-menu li.group-item a {
+  padding-left: 2.25em;
+}
+.nya-bs-select.btn-group .dropdown-menu li a {
+  cursor: pointer;
+}
+.nya-bs-select.btn-group .dropdown-menu li a span.check-mark {
+  display: none;
+}
+.nya-bs-select.btn-group .dropdown-menu li a span.text {
+  display: inline-block;
+}
+.nya-bs-select.btn-group .dropdown-menu li small {
+  padding-left: 0.5em;
+}
+.nya-bs-select.btn-group .dropdown-menu li.selected a .check-mark {
+  position: absolute;
+  display: inline-block;
+  right: 15px;
+  margin-top: 5px;
+}
+.nya-bs-select.btn-group .dropdown-menu li a span.text {
+  margin-right: 34px;
+}
+.nya-bs-select.btn-group .dropdown-menu li .dropdown-header {
+  display: none;
+}
+.nya-bs-select.btn-group .dropdown-menu li.first-in-group {
+  margin-top: 1.75em;
+}
+.nya-bs-select.btn-group .dropdown-menu li.first-in-group .dropdown-header {
+  display: block;
+  position: absolute;
+  top: -1.75em;
+  left: 0;
+}
+.nya-bs-select.show-menu-arrow.open > .btn {
+  z-index: 1036;
+}
+.nya-bs-select.show-menu-arrow .dropdown-toggle:before {
+  content: " ";
+  border-left: 7px solid transparent;
+  border-right: 7px solid transparent;
+  border-bottom-width: 7px;
+  border-bottom-style: solid;
+  border-bottom-color: rgba(204, 204, 204, 0.2);
+  position: absolute;
+  bottom: -4px;
+  left: 9px;
+  display: none;
+}
+.nya-bs-select.show-menu-arrow .dropdown-toggle:after {
+  content: '';
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  border-bottom: 6px solid white;
+  position: absolute;
+  bottom: -4px;
+  left: 10px;
+  display: none;
+}
+.nya-bs-select.show-menu-arrow.dropup .dropdown-toggle:before {
+  bottom: auto;
+  top: -3px;
+  border-bottom: 0;
+  border-top-width: 7px;
+  border-top-style: solid;
+  border-top-color: rgba(204, 204, 204, 0.2);
+}
+.nya-bs-select.show-menu-arrow.dropup .dropdown-toggle:after {
+  bottom: auto;
+  top: -3px;
+  border-top: 6px solid white;
+  border-bottom: 0;
+}
+.nya-bs-select.show-menu-arrow.pull-right .dropdown-toggle:before {
+  right: 12px;
+  left: auto;
+}
+.nya-bs-select.show-menu-arrow.pull-right .dropdown-toggle:after {
+  right: 13px;
+  left: auto;
+}
+.nya-bs-select.show-menu-arrow.open > .dropdown-toggle:before,
+.nya-bs-select.show-menu-arrow.open > .dropdown-toggle:after {
+  display: block;
+}
+.nya-bs-select .bs-searchbox,
+.nya-bs-select .bs-actionsbox {
+  padding: 4px 8px;
+}
+.nya-bs-select .bs-actionsbox {
+  float: left;
+  width: 100%;
+  box-sizing: border-box;
+}
+.nya-bs-select .bs-actionsbox .btn-group button {
+  width: 50%;
+}
+.nya-bs-select .bs-searchbox + .bs-actionsbox {
+  padding: 0 8px 4px;
+}
+.nya-bs-select .bs-searchbox input.form-control {
+  margin-bottom: 0;
+  width: 100%;
+  float: none;
+  z-index: inherit;
+  display: block;
+  position: static;
+  border-radius: 4px;
+}
+.nya-bs-select.fit-width .btn .filter-option,
+.nya-bs-select.fit-width .btn .special-title {
+  position: static;
+}
+.nya-bs-select.fit-width .btn .caret {
+  position: static;
+  top: auto;
+  margin-top: -1px;
+}
+.nya-bs-select.on-modal .dropdown-menu {
+  z-index: 1040;
+}
+.nya-bs-select.on-modal.open > .btn {
+  z-index: 1041;
+}

File diff suppressed because it is too large
+ 6 - 0
static/css/nya-bs-select.min.css


+ 193 - 0
static/css/textAngular.css

@@ -0,0 +1,193 @@
+.ta-hidden-input {
+	width: 1px;
+	height: 1px;
+	border: none;
+	margin: 0;
+	padding: 0;
+	position: absolute;
+	top: -10000px;
+	left: -10000px;
+	opacity: 0;
+	overflow: hidden;
+}
+
+/* add generic styling for the editor */
+.ta-root.focussed > .ta-scroll-window.form-control {
+	border-color: #66afe9;
+	outline: 0;
+	-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+	-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+	box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+
+.ta-editor.ta-html, .ta-scroll-window.form-control {
+	min-height: 300px;
+	height: auto;
+	overflow: auto;
+	font-family: inherit;
+	font-size: 100%;
+}
+
+.ta-scroll-window.form-control {
+	position: relative;
+	padding: 0;
+}
+
+.ta-scroll-window > .ta-bind {
+	height: auto;
+	min-height: 300px;
+	padding: 6px 12px;
+}
+
+.ta-editor:focus {
+	user-select: text;
+}
+
+/* add the styling for the awesomness of the resizer */
+.ta-resizer-handle-overlay {
+	z-index: 100;
+	position: absolute;
+	display: none;
+}
+
+.ta-resizer-handle-overlay > .ta-resizer-handle-info {
+	position: absolute;
+	bottom: 16px;
+	right: 16px;
+	border: 1px solid black;
+	background-color: #FFF;
+	padding: 0 4px;
+	opacity: 0.7;
+}
+
+.ta-resizer-handle-overlay > .ta-resizer-handle-background {
+	position: absolute;
+	bottom: 5px;
+	right: 5px;
+	left: 5px;
+	top: 5px;
+	border: 1px solid black;
+	background-color: rgba(0, 0, 0, 0.2);
+}
+
+.ta-resizer-handle-overlay > .ta-resizer-handle-corner {
+	width: 10px;
+	height: 10px;
+	position: absolute;
+}
+
+.ta-resizer-handle-overlay > .ta-resizer-handle-corner-tl{
+	top: 0;
+	left: 0;
+	border-left: 1px solid black;
+	border-top: 1px solid black;
+}
+
+.ta-resizer-handle-overlay > .ta-resizer-handle-corner-tr{
+	top: 0;
+	right: 0;
+	border-right: 1px solid black;
+	border-top: 1px solid black;
+}
+
+.ta-resizer-handle-overlay > .ta-resizer-handle-corner-bl{
+	bottom: 0;
+	left: 0;
+	border-left: 1px solid black;
+	border-bottom: 1px solid black;
+}
+
+.ta-resizer-handle-overlay > .ta-resizer-handle-corner-br{
+	bottom: 0;
+	right: 0;
+	border: 1px solid black;
+	cursor: se-resize;
+	background-color: white;
+}
+
+/* copy the popover code from bootstrap so this will work even without it */
+.popover {
+	position: absolute;
+	top: 0;
+	left: 0;
+	z-index: 1060;
+	display: none;
+	max-width: 276px;
+	padding: 1px;
+	font-size: 14px;
+	font-weight: normal;
+	line-height: 1.42857143;
+	text-align: left;
+	white-space: normal;
+	background-color: #fff;
+	-webkit-background-clip: padding-box;
+			background-clip: padding-box;
+	border: 1px solid #ccc;
+	border: 1px solid rgba(0, 0, 0, .2);
+	border-radius: 6px;
+	-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
+			box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
+}
+.popover.top {
+	margin-top: -10px;
+}
+.popover.bottom {
+	margin-top: 10px;
+}
+.popover-title {
+	padding: 8px 14px;
+	margin: 0;
+	font-size: 14px;
+	background-color: #f7f7f7;
+	border-bottom: 1px solid #ebebeb;
+	border-radius: 5px 5px 0 0;
+}
+.popover-content {
+	padding: 9px 14px;
+}
+.popover > .arrow,
+.popover > .arrow:after {
+	position: absolute;
+	display: block;
+	width: 0;
+	height: 0;
+	border-color: transparent;
+	border-style: solid;
+}
+.popover > .arrow {
+	border-width: 11px;
+}
+.popover > .arrow:after {
+	content: "";
+	border-width: 10px;
+}
+.popover.top > .arrow {
+	bottom: -11px;
+	left: 50%;
+	margin-left: -11px;
+	border-top-color: #999;
+	border-top-color: rgba(0, 0, 0, .25);
+	border-bottom-width: 0;
+}
+.popover.top > .arrow:after {
+	bottom: 1px;
+	margin-left: -10px;
+	content: " ";
+	border-top-color: #fff;
+	border-bottom-width: 0;
+}
+.popover.bottom > .arrow {
+	top: -11px;
+	left: 50%;
+	margin-left: -11px;
+	border-top-width: 0;
+	border-bottom-color: #999;
+	border-bottom-color: rgba(0, 0, 0, .25);
+}
+.popover.bottom > .arrow:after {
+	top: 1px;
+	margin-left: -10px;
+	content: " ";
+	border-top-width: 0;
+	border-bottom-color: #fff;
+}

File diff suppressed because it is too large
+ 0 - 0
static/css/textAngular.min.css


File diff suppressed because it is too large
+ 155 - 0
static/css/toaster.css


File diff suppressed because it is too large
+ 11 - 0
static/css/toaster.min.css


File diff suppressed because it is too large
+ 1 - 0
static/js/angular-formly-templates-bootstrap.min.js


File diff suppressed because it is too large
+ 0 - 0
static/js/angular-formly-templates-bootstrap.min.js.map


File diff suppressed because it is too large
+ 2 - 0
static/js/api-check.min.js


File diff suppressed because it is too large
+ 0 - 0
static/js/api-check.min.js.map


File diff suppressed because it is too large
+ 0 - 0
static/js/eda.dragdropway.js


File diff suppressed because it is too large
+ 0 - 0
static/js/eda.dragdropway.min.js


File diff suppressed because it is too large
+ 0 - 0
static/js/eda.easyFormViewer.js


File diff suppressed because it is too large
+ 0 - 0
static/js/eda.easyFormViewer.min.js


File diff suppressed because it is too large
+ 0 - 0
static/js/eda.stepway.js


File diff suppressed because it is too large
+ 0 - 0
static/js/eda.stepway.min.js


File diff suppressed because it is too large
+ 1 - 0
static/js/formly.min.js


File diff suppressed because it is too large
+ 0 - 0
static/js/formly.min.js.map


File diff suppressed because it is too large
+ 93 - 0
static/js/lodash.min.js


+ 1723 - 0
static/js/nya-bs-select.js

@@ -0,0 +1,1723 @@
+/**
+ * nya-bootstrap-select v2.1.6
+ * Copyright 2014 Nyasoft
+ * Licensed under MIT license
+ */
+(function(){
+  'use strict';
+
+
+var uid = 0;
+
+function nextUid() {
+  return ++uid;
+}
+
+/**
+ * Checks if `obj` is a window object.
+ *
+ * @private
+ * @param {*} obj Object to check
+ * @returns {boolean} True if `obj` is a window obj.
+ */
+function isWindow(obj) {
+  return obj && obj.window === obj;
+}
+
+/**
+ * @ngdoc function
+ * @name angular.isString
+ * @module ng
+ * @kind function
+ *
+ * @description
+ * Determines if a reference is a `String`.
+ *
+ * @param {*} value Reference to check.
+ * @returns {boolean} True if `value` is a `String`.
+ */
+function isString(value){return typeof value === 'string';}
+
+/**
+ * @param {*} obj
+ * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments,
+ *                   String ...)
+ */
+function isArrayLike(obj) {
+  if (obj == null || isWindow(obj)) {
+    return false;
+  }
+
+  var length = obj.length;
+
+  if (obj.nodeType === 1 && length) {
+    return true;
+  }
+
+  return isString(obj) || Array.isArray(obj) || length === 0 ||
+    typeof length === 'number' && length > 0 && (length - 1) in obj;
+}
+
+/**
+ * Creates a new object without a prototype. This object is useful for lookup without having to
+ * guard against prototypically inherited properties via hasOwnProperty.
+ *
+ * Related micro-benchmarks:
+ * - http://jsperf.com/object-create2
+ * - http://jsperf.com/proto-map-lookup/2
+ * - http://jsperf.com/for-in-vs-object-keys2
+ *
+ * @returns {Object}
+ */
+function createMap() {
+  return Object.create(null);
+}
+
+/**
+ * Computes a hash of an 'obj'.
+ * Hash of a:
+ *  string is string
+ *  number is number as string
+ *  object is either result of calling $$hashKey function on the object or uniquely generated id,
+ *         that is also assigned to the $$hashKey property of the object.
+ *
+ * @param obj
+ * @returns {string} hash string such that the same input will have the same hash string.
+ *         The resulting string key is in 'type:hashKey' format.
+ */
+function hashKey(obj, nextUidFn) {
+  var objType = typeof obj,
+    key;
+
+  if (objType == 'function' || (objType == 'object' && obj !== null)) {
+    if (typeof (key = obj.$$hashKey) == 'function') {
+      // must invoke on object to keep the right this
+      key = obj.$$hashKey();
+    } else if (key === undefined) {
+      key = obj.$$hashKey = (nextUidFn || nextUid)();
+    }
+  } else {
+    key = obj;
+  }
+
+  return objType + ':' + key;
+}
+
+//TODO: use with caution. if an property of element in array doesn't exist in group, the resultArray may lose some element.
+function sortByGroup(array ,group, property) {
+  var unknownGroup = [],
+    i, j,
+    resultArray = [];
+  for(i = 0; i < group.length; i++) {
+    for(j = 0; j < array.length;j ++) {
+      if(!array[j][property]) {
+        unknownGroup.push(array[j]);
+      } else if(array[j][property] === group[i]) {
+        resultArray.push(array[j]);
+      }
+    }
+  }
+
+  resultArray = resultArray.concat(unknownGroup);
+
+  return resultArray;
+}
+
+/**
+ * Return the DOM siblings between the first and last node in the given array.
+ * @param {Array} array like object
+ * @returns {jqLite} jqLite collection containing the nodes
+ */
+function getBlockNodes(nodes) {
+  // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original
+  //             collection, otherwise update the original collection.
+  var node = nodes[0];
+  var endNode = nodes[nodes.length - 1];
+  var blockNodes = [node];
+
+  do {
+    node = node.nextSibling;
+    if (!node) break;
+    blockNodes.push(node);
+  } while (node !== endNode);
+
+  return angular.element(blockNodes);
+}
+
+var getBlockStart = function(block) {
+  return block.clone[0];
+};
+
+var getBlockEnd = function(block) {
+  return block.clone[block.clone.length - 1];
+};
+
+var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength, group) {
+  // TODO(perf): generate setters to shave off ~40ms or 1-1.5%
+  scope[valueIdentifier] = value;
+  if (keyIdentifier) scope[keyIdentifier] = key;
+  scope.$index = index;
+  scope.$first = (index === 0);
+  scope.$last = (index === (arrayLength - 1));
+  scope.$middle = !(scope.$first || scope.$last);
+  // jshint bitwise: false
+  scope.$odd = !(scope.$even = (index&1) === 0);
+  // jshint bitwise: true
+
+  if(group) {
+    scope.$group = group;
+  }
+};
+
+var setElementIsolateScope = function(element, scope) {
+  element.data('isolateScope', scope);
+};
+
+var contains = function(array, element) {
+  var length = array.length,
+    i;
+  if(length === 0) {
+    return false;
+  }
+  for(i = 0;i < length; i++) {
+    if(deepEquals(element, array[i])) {
+      return true;
+    }
+  }
+  return false;
+};
+
+var indexOf = function(array, element) {
+  var length = array.length,
+    i;
+  if(length === 0) {
+    return -1;
+  }
+  for(i = 0; i < length; i++) {
+    if(deepEquals(element, array[i])) {
+      return i;
+    }
+  }
+  return -1;
+};
+
+/**
+ * filter the event target for the nya-bs-option element.
+ * Use this method with event delegate. (attach a event handler on an parent element and listen the special children elements)
+ * @param target event.target node
+ * @param parent {object} the parent, where the event handler attached.
+ * @param selector {string}|{object} a class or DOM element
+ * @return the filtered target or null if no element satisfied the selector.
+ */
+var filterTarget = function(target, parent, selector) {
+  var elem = target,
+    className, type = typeof selector;
+
+  if(target == parent) {
+    return null;
+  } else {
+    do {
+      if(type === 'string') {
+        className = ' ' + elem.className + ' ';
+        if(elem.nodeType === 1 && className.replace(/[\t\r\n\f]/g, ' ').indexOf(selector) >= 0) {
+          return elem;
+        }
+      } else {
+        if(elem == selector) {
+          return elem;
+        }
+      }
+
+    } while((elem = elem.parentNode) && elem != parent && elem.nodeType !== 9);
+
+    return null;
+  }
+
+};
+
+var getClassList = function(element) {
+  var classList,
+    className = element.className.replace(/[\t\r\n\f]/g, ' ').trim();
+  classList = className.split(' ');
+  for(var i = 0; i < classList.length; i++) {
+    if(/\s+/.test(classList[i])) {
+      classList.splice(i, 1);
+      i--;
+    }
+  }
+  return classList;
+
+};
+
+// work with node element
+var hasClass = function(element, className) {
+  var classList = getClassList(element);
+  return classList.indexOf(className) !== -1;
+};
+
+// query children by class(one or more)
+var queryChildren = function(element, classList) {
+  var children = element.children(),
+    length = children.length,
+    child,
+    valid,
+    classes;
+  if(length > 0) {
+    for(var i = 0; i < length; i++) {
+      child = children.eq(i);
+      valid = true;
+      classes = getClassList(child[0]);
+      if(classes.length > 0) {
+        for(var j = 0; j < classList.length; j++) {
+          if(classes.indexOf(classList[j]) === -1) {
+            valid = false;
+            break;
+          }
+        }
+      }
+      if(valid) {
+        return child;
+      }
+    }
+  }
+  return [];
+};
+
+/**
+ * Current support only drill down one level.
+ * case insensitive
+ * @param element
+ * @param keyword
+ */
+var hasKeyword = function(element, keyword) {
+  var childElements,
+    index, length;
+  if(element.text().toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
+    return true;
+  } else {
+    childElements = element.children();
+    length = childElements.length;
+    for(index = 0; index < length; index++) {
+      if(childElements.eq(index).text().toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
+        return true;
+      }
+    }
+    return false;
+  }
+};
+
+function sibling( cur, dir ) {
+  while ( (cur = cur[dir]) && cur.nodeType !== 1) {}
+  return cur;
+}
+
+
+// map global property to local variable.
+var jqLite = angular.element;
+
+var deepEquals = angular.equals;
+
+var deepCopy = angular.copy;
+
+var extend = angular.extend;
+
+var nyaBsSelect = angular.module('nya.bootstrap.select', []);
+
+/**
+ * A service for configuration. the configuration is shared globally.
+ * Testing ci build --jpmckearin
+ */
+nyaBsSelect.provider('nyaBsConfig', function() {
+
+  var locale = null;
+
+  // default localized text. cannot be modified.
+  var defaultText = {
+    'en-us': {
+      defaultNoneSelection: 'Nothing selected',
+      noSearchResult: 'NO SEARCH RESULT',
+      numberItemSelected: '%d items selected',
+      selectAll: 'Select All',
+      deselectAll: 'Deselect All'
+    }
+  };
+
+  // localized text which actually being used.
+  var interfaceText = deepCopy(defaultText);
+
+  /**
+   * Merge with default localized text.
+   * @param localeId a string formatted as languageId-countryId
+   * @param obj localized text object.
+   */
+  this.setLocalizedText = function(localeId, obj) {
+    if(!localeId) {
+      throw new Error('localeId must be a string formatted as languageId-countryId');
+    }
+    if(!interfaceText[localeId]) {
+      interfaceText[localeId] = {};
+    }
+    interfaceText[localeId] = extend(interfaceText[localeId], obj);
+  };
+
+  /**
+   * Force to use a special locale id. if localeId is null. reset to user-agent locale.
+   * @param localeId a string formatted as languageId-countryId
+   */
+  this.useLocale = function(localeId) {
+    locale = localeId;
+  };
+
+  /**
+   * get the localized text according current locale or forced locale
+   * @returns localizedText
+   */
+  this.$get = ['$locale', function($locale){
+    var localizedText;
+    if(locale) {
+      localizedText = interfaceText[locale];
+    } else {
+      localizedText = interfaceText[$locale.id];
+    }
+    if(!localizedText) {
+      localizedText = defaultText['en-us'];
+    }
+    return localizedText;
+  }];
+
+});
+
+
+nyaBsSelect.controller('nyaBsSelectCtrl', function(){
+
+  var self = this;
+
+  // keyIdentifier and valueIdentifier are set by nyaBsOption directive
+  // used by nyaBsSelect directive to retrieve key and value from each nyaBsOption's child scope.
+  self.keyIdentifier = null;
+  self.valueIdentifier = null;
+
+  self.isMultiple = false;
+
+  // Should be override by nyaBsSelect directive and called by nyaBsOption directive when collection is changed.
+  self.onCollectionChange = function(){};
+
+  // for debug
+  self.setId = function(id) {
+    self.id = id || 'id#' + Math.floor(Math.random() * 10000);
+  };
+
+});
+nyaBsSelect.directive('nyaBsSelect', ['$parse', '$document', '$timeout', '$compile', 'nyaBsConfig', function ($parse, $document, $timeout, $compile, nyaBsConfig) {
+
+  var DEFAULT_NONE_SELECTION = 'Nothing selected';
+
+  var DROPDOWN_TOGGLE = '<button class="btn btn-default dropdown-toggle" type="button">' +
+    '<span class="pull-left filter-option"></span>' +
+    '<span class="pull-left special-title"></span>' +
+    '&nbsp;' +
+    '<span class="caret"></span>' +
+    '</button>';
+
+  var DROPDOWN_CONTAINER = '<div class="dropdown-menu open"></div>';
+
+  var SEARCH_BOX = '<div class="bs-searchbox">' +
+    '<input type="text" class="form-control">' +
+    '</div>';
+
+  var DROPDOWN_MENU = '<ul class="dropdown-menu inner"></ul>';
+
+  var NO_SEARCH_RESULT = '<li class="no-search-result"><span>NO SEARCH RESULT</span></li>';
+
+  var ACTIONS_BOX = '<div class="bs-actionsbox">' +
+    '<div class="btn-group btn-group-sm btn-block">' +
+    '<button class="actions-btn bs-select-all btn btn-default">SELECT ALL</button>' +
+    '<button class="actions-btn bs-deselect-all btn btn-default">DESELECT ALL</button>' +
+    '</div>' +
+    '</div>';
+
+  return {
+    restrict: 'ECA',
+    require: ['ngModel', 'nyaBsSelect'],
+    controller: 'nyaBsSelectCtrl',
+    compile: function nyaBsSelectCompile (tElement, tAttrs){
+      
+
+      tElement.addClass('btn-group');
+
+
+      /**
+       * get the default text when nothing is selected. can be template
+       * @param scope, if provided, will try to compile template with given scope, will not attempt to compile the pure text.
+       * @returns {*}
+       */
+      var getDefaultNoneSelectionContent = function(scope) {
+        // text node or jqLite element.
+        var content;
+
+        if(tAttrs.titleTpl) {
+          // use title-tpl attribute value.
+          content = jqLite(tAttrs.titleTpl);
+        } else if(tAttrs.title) {
+          // use title attribute value.
+          content = document.createTextNode(tAttrs.title);
+        } else if(localizedText.defaultNoneSelectionTpl){
+          // use localized text template.
+          content = jqLite(localizedText.defaultNoneSelectionTpl);
+        } else if(localizedText.defaultNoneSelection) {
+          // use localized text.
+          content = document.createTextNode(localizedText.defaultNoneSelection);
+        } else {
+          // use default.
+          content = document.createTextNode(DEFAULT_NONE_SELECTION);
+        }
+
+        if(scope && (tAttrs.titleTpl || localizedText.defaultNoneSelectionTpl)) {
+          
+          return $compile(content)(scope);
+        }
+
+        return content;
+      };
+
+      var options = tElement.children(),
+        dropdownToggle = jqLite(DROPDOWN_TOGGLE),
+        dropdownContainer = jqLite(DROPDOWN_CONTAINER),
+        dropdownMenu = jqLite(DROPDOWN_MENU),
+        searchBox,
+        noSearchResult,
+        actionsBox,
+        classList,
+        length,
+        index,
+        liElement,
+        localizedText = nyaBsConfig,
+        isMultiple = typeof tAttrs.multiple !== 'undefined',
+        nyaBsOptionValue;
+
+      classList = getClassList(tElement[0]);
+      classList.forEach(function(className) {
+        if(/btn-(?:primary|info|success|warning|danger|inverse)/.test(className)) {
+          tElement.removeClass(className);
+          dropdownToggle.removeClass('btn-default');
+          dropdownToggle.addClass(className);
+        }
+
+        if(/btn-(?:lg|sm|xs)/.test(className)) {
+          tElement.removeClass(className);
+          dropdownToggle.addClass(className);
+        }
+
+        if(className === 'form-control') {
+          dropdownToggle.addClass(className);
+        }
+      });
+
+      dropdownMenu.append(options);
+
+      // add tabindex to children anchor elements if not present.
+      // tabindex attribute will give an anchor element ability to be get focused.
+      length = options.length;
+      for(index = 0; index < length; index++) {
+        liElement = options.eq(index);
+        if(liElement.hasClass('nya-bs-option') || liElement.attr('nya-bs-option')) {
+          liElement.find('a').attr('tabindex', '0');
+          // In order to be compatible with old version, we should copy value of value attribute into data-value attribute.
+          // For the reason we use data-value instead, see http://nya.io/AngularJS/Beware-Of-Using-value-Attribute-On-list-element/
+          nyaBsOptionValue = liElement.attr('value');
+          if(angular.isString(nyaBsOptionValue) && nyaBsOptionValue !== '') {
+            liElement.attr('data-value', nyaBsOptionValue);
+            liElement.removeAttr('value');
+          }
+        }
+      }
+
+      if(tAttrs.liveSearch === 'true') {
+        searchBox = jqLite(SEARCH_BOX);
+
+        if(tAttrs.noSearchTitle) {
+            NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', tAttrs.noSearchTitle);
+        } else if (tAttrs.noSearchTitleTpl) {
+            NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', tAttrs.noSearchTitleTpl);
+        }else {
+          // set localized text
+          if(localizedText.noSearchResultTpl) {
+            NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', localizedText.noSearchResultTpl);
+          } else if(localizedText.noSearchResult) {
+            NO_SEARCH_RESULT = NO_SEARCH_RESULT.replace('NO SEARCH RESULT', localizedText.noSearchResult);
+          }
+        }
+
+        noSearchResult = jqLite(NO_SEARCH_RESULT);
+        dropdownContainer.append(searchBox);
+        dropdownMenu.append(noSearchResult);
+      }
+
+      if (tAttrs.actionsBox === 'true' && isMultiple) {
+        // set localizedText
+        if (localizedText.selectAllTpl) {
+          ACTIONS_BOX = ACTIONS_BOX.replace('SELECT ALL', localizedText.selectAllTpl);
+        } else if (localizedText.selectAll) {
+          ACTIONS_BOX = ACTIONS_BOX.replace('SELECT ALL', localizedText.selectAll);
+        }
+
+        if (localizedText.deselectAllTpl) {
+          ACTIONS_BOX = ACTIONS_BOX.replace('DESELECT ALL', localizedText.deselectAllTpl);
+        } else if (localizedText.selectAll) {
+          ACTIONS_BOX = ACTIONS_BOX.replace('DESELECT ALL', localizedText.deselectAll);
+        }
+
+        actionsBox = jqLite(ACTIONS_BOX);
+        dropdownContainer.append(actionsBox);
+      }
+
+      // set default none selection text
+      jqLite(dropdownToggle[0].querySelector('.special-title')).append(getDefaultNoneSelectionContent());
+
+      dropdownContainer.append(dropdownMenu);
+
+      tElement.append(dropdownToggle);
+      tElement.append(dropdownContainer);
+
+      return function nyaBsSelectLink ($scope, $element, $attrs, ctrls) {
+        
+        var ngCtrl = ctrls[0],
+          nyaBsSelectCtrl = ctrls[1],
+          liHeight,
+          isDisabled = false,
+          previousTabIndex,
+          valueExpFn,
+          valueExpGetter = $parse(nyaBsSelectCtrl.valueExp),
+          isMultiple = typeof $attrs.multiple !== 'undefined';
+
+        // find element from current $element root. because the compiled element may be detached from DOM tree by ng-if or ng-switch.
+        var dropdownToggle = jqLite($element[0].querySelector('.dropdown-toggle')),
+          dropdownContainer = dropdownToggle.next(),
+          dropdownMenu = jqLite(dropdownContainer[0].querySelector('.dropdown-menu.inner')),
+          searchBox = jqLite(dropdownContainer[0].querySelector('.bs-searchbox')),
+          noSearchResult = jqLite(dropdownMenu[0].querySelector('.no-search-result')),
+          actionsBox = jqLite(dropdownContainer[0].querySelector('.bs-actionsbox'));
+
+        if(nyaBsSelectCtrl.valueExp) {
+          valueExpFn = function(scope, locals) {
+            return valueExpGetter(scope, locals);
+          };
+        }
+
+        // for debug
+        nyaBsSelectCtrl.setId($element.attr('id'));
+
+        if (isMultiple) {
+          nyaBsSelectCtrl.isMultiple = true;
+
+          // required validator
+          ngCtrl.$isEmpty = function(value) {
+            return !value || value.length === 0;
+          };
+        }
+        if(typeof $attrs.disabled !== 'undefined') {
+          $scope.$watch($attrs.disabled, function(disabled){
+            if(disabled) {
+              dropdownToggle.addClass('disabled');
+              dropdownToggle.attr('disabled', 'disabled');
+              previousTabIndex = dropdownToggle.attr('tabindex');
+              dropdownToggle.attr('tabindex', '-1');
+              isDisabled = true;
+            } else {
+              dropdownToggle.removeClass('disabled');
+              dropdownToggle.removeAttr('disabled');
+              if(previousTabIndex) {
+                dropdownToggle.attr('tabindex', previousTabIndex);
+              } else {
+                dropdownToggle.removeAttr('tabindex');
+              }
+              isDisabled = false;
+            }
+          });
+        }
+
+        /**
+         * Do some check on modelValue. remove no existing value
+         * @param values
+         * @param deepWatched
+         */
+        nyaBsSelectCtrl.onCollectionChange = function (values, deepWatched) {
+          var valuesForSelect = [],
+            index,
+            modelValueChanged = false,
+            // Due to ngModelController compare reference with the old modelValue, we must set an new array instead of modifying the old one.
+            // See: https://github.com/angular/angular.js/issues/1751
+            modelValue = deepCopy(ngCtrl.$modelValue);
+
+          if(!modelValue) {
+            return;
+          }
+
+          /**
+           * Behavior change, since 2.1.0, we don't want to reset model to null or empty array when options' collection is not prepared.
+           */
+          if(Array.isArray(values) && values.length > 0) {
+            if(valueExpFn) {
+              for(index = 0; index < values.length; index++) {
+                valuesForSelect.push(valueExpFn($scope, values[index]));
+              }
+            } else {
+              for(index = 0; index < values.length; index++) {
+                if(nyaBsSelectCtrl.valueIdentifier) {
+                  valuesForSelect.push(values[index][nyaBsSelectCtrl.valueIdentifier]);
+                } else if(nyaBsSelectCtrl.keyIdentifier) {
+                  valuesForSelect.push(values[index][nyaBsSelectCtrl.keyIdentifier]);
+                }
+              }
+
+            }
+
+            if(isMultiple) {
+              for(index = 0; index < modelValue.length; index++) {
+                if(!contains(valuesForSelect, modelValue[index])) {
+                  modelValueChanged = true;
+                  modelValue.splice(index, 1);
+                  index--;
+                }
+              }
+
+              if(modelValueChanged) {
+                // modelValue changed.
+
+                ngCtrl.$setViewValue(modelValue);
+
+                updateButtonContent();
+              }
+
+            } else {
+              if(!contains(valuesForSelect, modelValue)) {
+                modelValue = valuesForSelect[0];
+
+                ngCtrl.$setViewValue(modelValue);
+
+                updateButtonContent();
+              }
+            }
+
+          }
+
+          /**
+           * if we set deep-watch="true" on nyaBsOption directive,
+           * we need to refresh dropdown button content whenever a change happened in collection.
+           */
+          if(deepWatched) {
+            
+            updateButtonContent();
+          }
+
+        };
+
+        // view --> model
+
+        dropdownMenu.on('click', function menuEventHandler (event) {
+          if(isDisabled) {
+            return;
+          }
+          
+          if(jqLite(event.target).hasClass('dropdown-header')) {
+            return;
+          }
+          var nyaBsOptionNode = filterTarget(event.target, dropdownMenu[0], 'nya-bs-option'),
+            nyaBsOption;
+
+          if(nyaBsOptionNode !== null) {
+            nyaBsOption = jqLite(nyaBsOptionNode);
+            if(nyaBsOption.hasClass('disabled')) {
+              return;
+            }
+            selectOption(nyaBsOption);
+          }
+        });
+
+        // if click the outside of dropdown menu, close the dropdown menu
+        var outClick = function(event) {
+          if(filterTarget(event.target, $element.parent()[0], $element[0]) === null) {
+            if($element.hasClass('open')) {
+              $element.triggerHandler('blur');
+            }
+            $element.removeClass('open');
+          }
+        };
+        $document.on('click', outClick);
+
+        
+
+        dropdownToggle.on('blur', function() {
+          if(!$element.hasClass('open')) {
+            $element.triggerHandler('blur');
+          }
+        });
+        dropdownToggle.on('click', function() {
+          var nyaBsOptionNode;
+          $element.toggleClass('open');
+          if($element.hasClass('open') && typeof liHeight === 'undefined') {
+            calcMenuSize();
+          }
+          if($attrs.liveSearch === 'true' && $element.hasClass('open')) {
+            searchBox.children().eq(0)[0].focus();
+            nyaBsOptionNode = findFocus(true);
+            if(nyaBsOptionNode) {
+              dropdownMenu.children().removeClass('active');
+              jqLite(nyaBsOptionNode).addClass('active');
+            }
+          } else if($element.hasClass('open')) {
+            nyaBsOptionNode = findFocus(true);
+            if(nyaBsOptionNode) {
+              setFocus(nyaBsOptionNode);
+            }
+          }
+        });
+
+        // actions box
+        if ($attrs.actionsBox === 'true' && isMultiple) {
+          actionsBox.find('button').eq(0).on('click', function () {
+            setAllOptions(true);
+          });
+          actionsBox.find('button').eq(1).on('click', function () {
+            setAllOptions(false);
+          });
+        }
+
+
+        // live search
+        if($attrs.liveSearch === 'true') {
+          searchBox.children().on('input', function(){
+
+            var searchKeyword = searchBox.children().val(),
+              found = 0,
+              options = dropdownMenu.children(),
+              length = options.length,
+              index,
+              option,
+              nyaBsOptionNode;
+
+            if(searchKeyword) {
+              for(index = 0; index < length; index++) {
+                option = options.eq(index);
+                if(option.hasClass('nya-bs-option')) {
+                  if(!hasKeyword(option.find('a'), searchKeyword)) {
+                    option.addClass('not-match');
+                  } else {
+                    option.removeClass('not-match');
+                    found++;
+                  }
+                }
+              }
+
+              if(found === 0) {
+                noSearchResult.addClass('show');
+              } else {
+                noSearchResult.removeClass('show');
+              }
+            } else {
+              for(index = 0; index < length; index++) {
+                option = options.eq(index);
+                if(option.hasClass('nya-bs-option')) {
+                  option.removeClass('not-match');
+                }
+              }
+              noSearchResult.removeClass('show');
+            }
+
+            nyaBsOptionNode = findFocus(true);
+
+            if(nyaBsOptionNode) {
+              options.removeClass('active');
+              jqLite(nyaBsOptionNode).addClass('active');
+            }
+
+          });
+        }
+
+
+        // model --> view
+
+        ngCtrl.$render = function() {
+          var modelValue = ngCtrl.$modelValue,
+            index,
+            bsOptionElements = dropdownMenu.children(),
+            length = bsOptionElements.length,
+            value;
+          if(typeof modelValue === 'undefined') {
+            // if modelValue is undefined. uncheck all option
+            for(index = 0; index < length; index++) {
+              if(bsOptionElements.eq(index).hasClass('nya-bs-option')) {
+                bsOptionElements.eq(index).removeClass('selected');
+              }
+            }
+          } else {
+            for(index = 0; index < length; index++) {
+              if(bsOptionElements.eq(index).hasClass('nya-bs-option')) {
+
+                value = getOptionValue(bsOptionElements.eq(index));
+                if(isMultiple) {
+                  if(contains(modelValue, value)) {
+                    bsOptionElements.eq(index).addClass('selected');
+                  } else {
+                    bsOptionElements.eq(index).removeClass('selected');
+                  }
+                } else {
+                  if(deepEquals(modelValue, value)) {
+                    bsOptionElements.eq(index).addClass('selected');
+                  } else {
+                    bsOptionElements.eq(index).removeClass('selected');
+                  }
+                }
+
+              }
+            }
+          }
+          //console.log(nyaBsSelectCtrl.id + ' render end');
+          updateButtonContent();
+        };
+
+        // simple keyboard support
+        $element.on('keydown', function(event){
+          var keyCode = event.keyCode;
+
+          if(keyCode !== 27 && keyCode !== 13 && keyCode !== 38 && keyCode !== 40) {
+            // we only handle special keys. don't waste time to traverse the dom tree.
+            return;
+          }
+
+          // prevent a click event to be fired.
+          event.preventDefault();
+          if(isDisabled) {
+            event.stopPropagation();
+            return;
+          }
+          var toggleButton = filterTarget(event.target, $element[0], dropdownToggle[0]),
+            menuContainer,
+            searchBoxContainer,
+            liElement,
+            nyaBsOptionNode;
+
+          if($attrs.liveSearch === 'true') {
+            searchBoxContainer = filterTarget(event.target, $element[0], searchBox[0]);
+          } else {
+            menuContainer = filterTarget(event.target, $element[0], dropdownContainer[0])
+          }
+
+          if(toggleButton) {
+            
+
+            // press enter to active dropdown
+            if((keyCode === 13 || keyCode === 38 || keyCode === 40) && !$element.hasClass('open')) {
+
+              event.stopPropagation();
+
+              $element.addClass('open');
+
+              // calculate menu size
+              if(typeof liHeight === 'undefined') {
+                calcMenuSize();
+              }
+
+              // if live search enabled. give focus to search box.
+              if($attrs.liveSearch === 'true') {
+                searchBox.children().eq(0)[0].focus();
+                // find the focusable node but we will use active
+                nyaBsOptionNode = findFocus(true);
+                if(nyaBsOptionNode) {
+                  // remove previous active state
+                  dropdownMenu.children().removeClass('active');
+                  // set active to first focusable element
+                  jqLite(nyaBsOptionNode).addClass('active');
+                }
+              } else {
+                // otherwise, give focus to first menu item.
+                nyaBsOptionNode = findFocus(true);
+                if(nyaBsOptionNode) {
+                  setFocus(nyaBsOptionNode);
+                }
+              }
+            }
+
+            // press enter or escape to de-active dropdown
+            //if((keyCode === 13 || keyCode === 27) && $element.hasClass('open')) {
+            //  $element.removeClass('open');
+            //  event.stopPropagation();
+            //}
+          } else if(menuContainer) {
+
+            if(keyCode === 27) {
+              // escape pressed
+              dropdownToggle[0].focus();
+              if($element.hasClass('open')) {
+                $element.triggerHandler('blur');
+              }
+              $element.removeClass('open');
+              event.stopPropagation();
+
+            } else if(keyCode === 38) {
+              event.stopPropagation();
+              // up arrow key
+              nyaBsOptionNode = findNextFocus(event.target.parentNode, 'previousSibling');
+              if(nyaBsOptionNode) {
+                setFocus(nyaBsOptionNode);
+              } else {
+                nyaBsOptionNode = findFocus(false);
+                if(nyaBsOptionNode) {
+                  setFocus(nyaBsOptionNode);
+                }
+              }
+            } else if(keyCode === 40) {
+              event.stopPropagation();
+              // down arrow key
+              nyaBsOptionNode = findNextFocus(event.target.parentNode, 'nextSibling');
+              if(nyaBsOptionNode) {
+                setFocus(nyaBsOptionNode);
+              } else {
+                nyaBsOptionNode = findFocus(true);
+                if(nyaBsOptionNode) {
+                  setFocus(nyaBsOptionNode);
+                }
+              }
+            } else if(keyCode === 13) {
+              event.stopPropagation();
+              // enter pressed
+              liElement = jqLite(event.target.parentNode);
+              if(liElement.hasClass('nya-bs-option')) {
+                selectOption(liElement);
+                if(!isMultiple) {
+                  dropdownToggle[0].focus();
+                }
+              }
+            }
+          } else if(searchBoxContainer) {
+            if(keyCode === 27) {
+              dropdownToggle[0].focus();
+              $element.removeClass('open');
+              event.stopPropagation();
+            } else if(keyCode === 38) {
+              // up
+              event.stopPropagation();
+
+              liElement = findActive();
+              if(liElement) {
+                nyaBsOptionNode = findNextFocus(liElement[0], 'previousSibling');
+                if(nyaBsOptionNode) {
+                  liElement.removeClass('active');
+                  jqLite(nyaBsOptionNode).addClass('active');
+                } else {
+                  nyaBsOptionNode = findFocus(false);
+                  if(nyaBsOptionNode) {
+                    liElement.removeClass('active');
+                    jqLite(nyaBsOptionNode).addClass('active');
+                  }
+                }
+              }
+
+            } else if(keyCode === 40) {
+              // down
+              event.stopPropagation();
+
+              liElement = findActive();
+              if(liElement) {
+                nyaBsOptionNode = findNextFocus(liElement[0], 'nextSibling');
+                if(nyaBsOptionNode) {
+                  liElement.removeClass('active');
+                  jqLite(nyaBsOptionNode).addClass('active');
+                } else {
+                  nyaBsOptionNode = findFocus(true);
+                  if(nyaBsOptionNode) {
+                    liElement.removeClass('active');
+                    jqLite(nyaBsOptionNode).addClass('active');
+                  }
+                }
+              }
+            } else if(keyCode === 13) {
+              // select an option.
+              liElement = findActive();
+              if(liElement) {
+                selectOption(liElement);
+                if(!isMultiple) {
+                  dropdownToggle[0].focus();
+                }
+              }
+            }
+          }
+        });
+
+        function findActive() {
+          var list = dropdownMenu.children(),
+            i, liElement,
+            length = list.length;
+          for(i = 0; i < length; i++) {
+            liElement = list.eq(i);
+            if(liElement.hasClass('active') && liElement.hasClass('nya-bs-option') && !liElement.hasClass('not-match')) {
+              return liElement;
+            }
+          }
+          return null;
+        }
+
+        /**
+         * setFocus on a nya-bs-option element. it actually set focus on its child anchor element.
+         * @param elem a nya-bs-option element.
+         */
+        function setFocus(elem) {
+          var childList = elem.childNodes,
+            length = childList.length,
+            child;
+          for(var i = 0; i < length; i++) {
+            child = childList[i];
+            if(child.nodeType === 1 && child.tagName.toLowerCase() === 'a') {
+              child.focus();
+              break;
+            }
+          }
+        }
+
+        function findFocus(fromFirst) {
+          var firstLiElement;
+          if(fromFirst) {
+            firstLiElement = dropdownMenu.children().eq(0);
+          } else {
+            firstLiElement = dropdownMenu.children().eq(dropdownMenu.children().length - 1);
+          }
+
+          // focus on selected element
+          for(var i = 0; i < dropdownMenu.children().length; i++) {
+            var childElement = dropdownMenu.children().eq(i);
+            if (!childElement.hasClass('not-match') && childElement.hasClass('selected')) {
+              return dropdownMenu.children().eq(i)[0];
+            }
+          }
+
+          if(firstLiElement.hasClass('nya-bs-option') && !firstLiElement.hasClass('disabled') && !firstLiElement.hasClass('not-match')) {
+            return firstLiElement[0];
+          } else {
+            if(fromFirst) {
+              return findNextFocus(firstLiElement[0], 'nextSibling');
+            } else {
+              return findNextFocus(firstLiElement[0], 'previousSibling');
+            }
+          }
+        }
+
+        /**
+         * find next focusable element on direction
+         * @param from the element traversed from
+         * @param direction can be 'nextSibling' or 'previousSibling'
+         * @returns the element if found, otherwise return null.
+         */
+        function findNextFocus(from, direction) {
+          if(from && !hasClass(from, 'nya-bs-option')) {
+            return;
+          }
+          var next = from;
+          while ((next = sibling(next, direction)) && next.nodeType) {
+            if(hasClass(next,'nya-bs-option') && !hasClass(next, 'disabled') && !hasClass(next, 'not-match')) {
+              return next
+            }
+          }
+          return null;
+        }
+
+        /**
+         *
+         */
+        function setAllOptions(selectAll) {
+          if (!isMultiple || isDisabled)
+            return;
+
+          var liElements,
+            wv,
+            viewValue;
+
+          liElements = dropdownMenu[0].querySelectorAll('.nya-bs-option');
+          if (liElements.length > 0) {
+            wv = ngCtrl.$viewValue;
+
+            // make a deep copy enforce ngModelController to call its $render method.
+            // See: https://github.com/angular/angular.js/issues/1751
+            viewValue = Array.isArray(wv) ? deepCopy(wv) : [];
+
+            for (var i = 0; i < liElements.length; i++) {
+              var nyaBsOption = jqLite(liElements[i]);
+              if (nyaBsOption.hasClass('disabled'))
+                continue;
+
+              var value, index;
+
+              // if user specify the value attribute. we should use the value attribute
+              // otherwise, use the valueIdentifier specified field in target scope
+              value = getOptionValue(nyaBsOption);
+
+              if (typeof value !== 'undefined') {
+                index = indexOf(viewValue, value);
+                if (selectAll && index == -1) {
+                  // check element
+                  viewValue.push(value);
+                  nyaBsOption.addClass('selected');
+                } else if (!selectAll && index != -1) {
+                  // uncheck element
+                  viewValue.splice(index, 1);
+                  nyaBsOption.removeClass('selected');
+                }
+              }
+            }
+
+            // update view value regardless
+            ngCtrl.$setViewValue(viewValue);
+            $scope.$digest();
+
+            updateButtonContent();
+          }
+        }
+
+        /**
+         * select an option represented by nyaBsOption argument. Get the option's value and update model.
+         * if isMultiple = true, doesn't close dropdown menu. otherwise close the menu.
+         * @param nyaBsOption the jqLite wrapped `nya-bs-option` element.
+         */
+        function selectOption(nyaBsOption) {
+          var value,
+            viewValue,
+            wv = ngCtrl.$viewValue,
+            index;
+          // if user specify the value attribute. we should use the value attribute
+          // otherwise, use the valueIdentifier specified field in target scope
+
+          value = getOptionValue(nyaBsOption);
+
+          if(typeof value !== 'undefined') {
+            if(isMultiple) {
+              // make a deep copy enforce ngModelController to call its $render method.
+              // See: https://github.com/angular/angular.js/issues/1751
+              viewValue = Array.isArray(wv) ? deepCopy(wv) : [];
+              index = indexOf(viewValue, value);
+              if(index === -1) {
+                // check element
+                viewValue.push(value);
+                nyaBsOption.addClass('selected');
+
+              } else {
+                // uncheck element
+                viewValue.splice(index, 1);
+                nyaBsOption.removeClass('selected');
+
+              }
+
+            } else {
+              dropdownMenu.children().removeClass('selected');
+              viewValue = value;
+              nyaBsOption.addClass('selected');
+
+            }
+          }
+          // update view value regardless
+          ngCtrl.$setViewValue(viewValue);
+          $scope.$digest();
+
+          if(!isMultiple) {
+            // in single selection mode. close the dropdown menu
+            if($element.hasClass('open')) {
+              $element.triggerHandler('blur');
+            }
+            $element.removeClass('open');
+            dropdownToggle[0].focus();
+          }
+          updateButtonContent();
+        }
+
+        /**
+         * get a value of current nyaBsOption. according to different setting.
+         * - if `nya-bs-option` directive is used to populate options and a `value` attribute is specified. use expression of the attribute value.
+         * - if `nya-bs-option` directive is used to populate options and no other settings, use the valueIdentifier or keyIdentifier to retrieve value from scope of current nyaBsOption.
+         * - if `nya-bs-option` class is used on static options. use literal value of the `value` attribute.
+         * @param nyaBsOption a jqLite wrapped `nya-bs-option` element
+         */
+        function getOptionValue(nyaBsOption) {
+          var scopeOfOption;
+          if(valueExpFn) {
+            // here we use the scope bound by ourselves in the nya-bs-option.
+            scopeOfOption = nyaBsOption.data('isolateScope');
+            return valueExpFn(scopeOfOption);
+          } else {
+            if(nyaBsSelectCtrl.valueIdentifier || nyaBsSelectCtrl.keyIdentifier) {
+              scopeOfOption = nyaBsOption.data('isolateScope');
+              return scopeOfOption[nyaBsSelectCtrl.valueIdentifier] || scopeOfOption[nyaBsSelectCtrl.keyIdentifier];
+            } else {
+              return nyaBsOption.attr('data-value');
+            }
+          }
+
+        }
+
+        function getOptionText(nyaBsOption) {
+          var item = nyaBsOption.find('a');
+          if(item.children().length === 0 || item.children().eq(0).hasClass('check-mark')) {
+            // if the first child is check-mark or has no children, means the option text is text node
+            return item[0].firstChild.cloneNode(false);
+          } else {
+            // otherwise we clone the first element of the item
+            return item.children().eq(0)[0].cloneNode(true);
+          }
+        }
+
+        function updateButtonContent() {
+          var viewValue = ngCtrl.$viewValue;
+          $element.triggerHandler('change');
+
+          var filterOption = jqLite(dropdownToggle[0].querySelector('.filter-option'));
+          var specialTitle = jqLite(dropdownToggle[0].querySelector('.special-title'));
+          if(typeof viewValue === 'undefined') {
+            /**
+             * Select empty option when model is undefined.
+             */
+            dropdownToggle.addClass('show-special-title');
+            filterOption.empty();
+            return;
+          }
+          if(isMultiple && viewValue.length === 0) {
+            dropdownToggle.addClass('show-special-title');
+            filterOption.empty();
+          } else {
+            dropdownToggle.removeClass('show-special-title');
+            $timeout(function() {
+
+              var bsOptionElements = dropdownMenu.children(),
+                value,
+                nyaBsOption,
+                index,
+                length = bsOptionElements.length,
+                optionTitle,
+                selection = [],
+                match,
+                count;
+
+              if(isMultiple && $attrs.selectedTextFormat === 'count') {
+                count = 1;
+              } else if(isMultiple && $attrs.selectedTextFormat && (match = $attrs.selectedTextFormat.match(/\s*count\s*>\s*(\d+)\s*/))) {
+                count = parseInt(match[1], 10);
+              }
+
+              // data-selected-text-format="count" or data-selected-text-format="count>x"
+              if((typeof count !== 'undefined') && viewValue.length > count) {
+                filterOption.empty();
+                if(localizedText.numberItemSelectedTpl) {
+                  filterOption.append(jqLite(localizedText.numberItemSelectedTpl.replace('%d', viewValue.length)));
+                } else if(localizedText.numberItemSelected) {
+                  filterOption.append(document.createTextNode(localizedText.numberItemSelected.replace('%d', viewValue.length)));
+                } else {
+                  filterOption.append(document.createTextNode(viewValue.length + ' items selected'));
+                }
+                return;
+              }
+
+              // data-selected-text-format="values" or the number of selected items is less than count
+              for(index = 0; index < length; index++) {
+                nyaBsOption = bsOptionElements.eq(index);
+                if(nyaBsOption.hasClass('nya-bs-option')) {
+
+                  value = getOptionValue(nyaBsOption);
+
+                  if(isMultiple) {
+                    if(Array.isArray(viewValue) && contains(viewValue, value)) {
+                      // if option has an title attribute. use the title value as content show in button.
+                      // otherwise get very first child element.
+                      optionTitle = nyaBsOption.attr('title');
+                      if(optionTitle) {
+                        selection.push(document.createTextNode(optionTitle));
+                      } else {
+                        selection.push(getOptionText(nyaBsOption));
+                      }
+
+                    }
+                  } else {
+                    if(deepEquals(viewValue, value)) {
+                      optionTitle = nyaBsOption.attr('title');
+                      if(optionTitle) {
+                        selection.push(document.createTextNode(optionTitle));
+                      } else {
+                        selection.push(getOptionText(nyaBsOption));
+                      }
+                    }
+                  }
+
+                }
+              }
+
+              if(selection.length === 0) {
+                filterOption.empty();
+                dropdownToggle.addClass('show-special-title');
+              } else if(selection.length === 1) {
+                dropdownToggle.removeClass('show-special-title');
+                // either single or multiple selection will show the only selected content.
+                filterOption.empty();
+                filterOption.append(selection[0]);
+              } else {
+                dropdownToggle.removeClass('show-special-title');
+                filterOption.empty();
+                for(index = 0; index < selection.length; index++) {
+                  filterOption.append(selection[index]);
+                  if(index < selection.length -1) {
+                    filterOption.append(document.createTextNode(', '));
+                  }
+                }
+              }
+
+            });
+          }
+
+        }
+
+        // will called only once.
+        function calcMenuSize(){
+
+          var liElements = dropdownMenu.find('li'),
+            length = liElements.length,
+            liElement,
+            i;
+          for(i = 0; i < length; i++) {
+            liElement = liElements.eq(i);
+            if(liElement.hasClass('nya-bs-option') || liElement.attr('nya-bs-option')) {
+              liHeight = liElement[0].clientHeight;
+              break;
+            }
+          }
+
+          if(/\d+/.test($attrs.size)) {
+            var dropdownSize = parseInt($attrs.size, 10);
+            dropdownMenu.css('max-height', (dropdownSize * liHeight) + 'px');
+            dropdownMenu.css('overflow-y', 'auto');
+          }
+
+        }
+
+        $scope.$on('$destroy', function() {
+          dropdownMenu.off();
+          dropdownToggle.off();
+          if (searchBox.off) searchBox.off();
+          $document.off('click', outClick);
+          
+        });
+
+      };
+    }
+  };
+}]);
+
+nyaBsSelect.directive('nyaBsOption', ['$parse', function($parse){
+
+                        //00000011111111111111100000000022222222222222200000003333333333333330000000000000004444444444000000000000000000055555555550000000000000000000006666666666000000
+  var BS_OPTION_REGEX = /^\s*(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/;
+
+  return {
+    restrict: 'A',
+    transclude: 'element',
+    priority: 1000,
+    terminal: true,
+    require: ['^nyaBsSelect', '^ngModel'],
+    compile: function nyaBsOptionCompile (tElement, tAttrs) {
+
+      var expression = tAttrs.nyaBsOption;
+      var nyaBsOptionEndComment = document.createComment(' end nyaBsOption: ' + expression + ' ');
+      var match = expression.match(BS_OPTION_REGEX);
+
+      if(!match) {
+        throw new Error('invalid expression');
+      }
+
+      // we want to keep our expression comprehensible so we don't use 'select as label for value in collection' expression.
+      var valueExp = tAttrs.value,
+        valueExpGetter = valueExp ? $parse(valueExp) : null;
+
+      var valueIdentifier = match[3] || match[1],
+        keyIdentifier = match[2],
+        collectionExp = match[4],
+        groupByExpGetter = match[5] ? $parse(match[5]) : null,
+        trackByExp = match[6];
+
+      var trackByIdArrayFn,
+        trackByIdObjFn,
+        trackByIdExpFn,
+        trackByExpGetter;
+      var hashFnLocals = {$id: hashKey};
+      var groupByFn, locals = {};
+
+      if(trackByExp) {
+        trackByExpGetter = $parse(trackByExp);
+      } else {
+        trackByIdArrayFn = function(key, value) {
+          return hashKey(value);
+        };
+        trackByIdObjFn = function(key) {
+          return key;
+        };
+      }
+      return function nyaBsOptionLink($scope, $element, $attr, ctrls, $transclude) {
+
+        var nyaBsSelectCtrl = ctrls[0],
+          ngCtrl = ctrls[1],
+          valueExpFn,
+          deepWatched,
+          valueExpLocals = {};
+
+        if(trackByExpGetter) {
+          trackByIdExpFn = function(key, value, index) {
+            // assign key, value, and $index to the locals so that they can be used in hash functions
+            if (keyIdentifier) {
+              hashFnLocals[keyIdentifier] = key;
+            }
+            hashFnLocals[valueIdentifier] = value;
+            hashFnLocals.$index = index;
+            return trackByExpGetter($scope, hashFnLocals);
+          };
+        }
+
+        if(groupByExpGetter) {
+          groupByFn = function(key, value) {
+            if(keyIdentifier) {
+              locals[keyIdentifier] = key;
+            }
+            locals[valueIdentifier] = value;
+            return groupByExpGetter($scope, locals);
+          }
+        }
+
+        // set keyIdentifier and valueIdentifier property of nyaBsSelectCtrl
+        if(keyIdentifier) {
+          nyaBsSelectCtrl.keyIdentifier = keyIdentifier;
+        }
+        if(valueIdentifier) {
+          nyaBsSelectCtrl.valueIdentifier = valueIdentifier;
+        }
+
+        if(valueExpGetter) {
+          nyaBsSelectCtrl.valueExp = valueExp;
+          valueExpFn = function(key, value) {
+            if(keyIdentifier) {
+              valueExpLocals[keyIdentifier] = key;
+            }
+            valueExpLocals[valueIdentifier] = value;
+            return valueExpGetter($scope, valueExpLocals);
+          }
+
+        }
+
+
+        // Store a list of elements from previous run. This is a hash where key is the item from the
+        // iterator, and the value is objects with following properties.
+        //   - scope: bound scope
+        //   - element: previous element.
+        //   - index: position
+        //
+        // We are using no-proto object so that we don't need to guard against inherited props via
+        // hasOwnProperty.
+        var lastBlockMap = createMap();
+
+        // deepWatch will impact performance. use with caution.
+        if($attr.deepWatch === 'true') {
+          deepWatched = true;
+          $scope.$watch(collectionExp, nyaBsOptionAction, true);
+        } else {
+          deepWatched = false;
+          $scope.$watchCollection(collectionExp, nyaBsOptionAction);
+        }
+
+        function nyaBsOptionAction(collection) {
+          var index,
+
+            previousNode = $element[0],     // node that cloned nodes should be inserted after
+          // initialized to the comment node anchor
+
+            key, value,
+            trackById,
+            trackByIdFn,
+            collectionKeys,
+            collectionLength,
+          // Same as lastBlockMap but it has the current state. It will become the
+          // lastBlockMap on the next iteration.
+            nextBlockMap = createMap(),
+            nextBlockOrder,
+            block,
+            groupName,
+            nextNode,
+            group,
+            lastGroup,
+
+            removedClone, // removed clone node, should also remove isolateScope data as well
+
+            values = [],
+            valueObj; // the collection value
+
+          if(groupByFn) {
+            group = [];
+          }
+
+          if(isArrayLike(collection)) {
+            collectionKeys = collection;
+            trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
+          } else {
+            trackByIdFn = trackByIdExpFn || trackByIdObjFn;
+            // if object, extract keys, sort them and use to determine order of iteration over obj props
+            collectionKeys = [];
+            for (var itemKey in collection) {
+              if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) != '$') {
+                collectionKeys.push(itemKey);
+              }
+            }
+            collectionKeys.sort();
+          }
+          collectionLength = collectionKeys.length;
+          nextBlockOrder = new Array(collectionLength);
+
+          for(index = 0; index < collectionLength; index++) {
+            key = (collection === collectionKeys) ? index : collectionKeys[index];
+            value = collection[key];
+            trackById = trackByIdFn(key, value, index);
+
+            // copy the value with scope like structure to notify the select directive.
+            valueObj = {};
+            if(keyIdentifier) {
+              valueObj[keyIdentifier] = key;
+            }
+
+            valueObj[valueIdentifier] = value;
+            values.push(valueObj);
+
+            if(groupByFn) {
+              groupName = groupByFn(key, value);
+              if(group.indexOf(groupName) === -1 && groupName) {
+                group.push(groupName);
+              }
+            }
+
+            if(lastBlockMap[trackById]) {
+              // found previously seen block
+              block = lastBlockMap[trackById];
+              delete lastBlockMap[trackById];
+
+              // must update block here because some data we stored may change.
+              if(groupByFn) {
+                block.group = groupName;
+              }
+              block.key = key;
+              block.value = value;
+
+              nextBlockMap[trackById] = block;
+              nextBlockOrder[index] = block;
+            } else if(nextBlockMap[trackById]) {
+              //if collision detected. restore lastBlockMap and throw an error
+              nextBlockOrder.forEach(function(block) {
+                if(block && block.scope) {
+                  lastBlockMap[block.id] = block;
+                }
+              });
+              throw new Error("Duplicates in a select are not allowed. Use 'track by' expression to specify unique keys.");
+            } else {
+              // new never before seen block
+              nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined, key: key, value: value};
+              nextBlockMap[trackById] = true;
+              if(groupName) {
+                nextBlockOrder[index].group = groupName;
+              }
+            }
+          }
+
+          // only resort nextBlockOrder when group found
+          if(group && group.length > 0) {
+
+            nextBlockOrder = sortByGroup(nextBlockOrder, group, 'group');
+          }
+
+          // remove DOM nodes
+          for( var blockKey in lastBlockMap) {
+            block = lastBlockMap[blockKey];
+            removedClone = getBlockNodes(block.clone);
+            // remove the isolateScope data to detach scope from this clone
+            removedClone.removeData('isolateScope');
+            removedClone.remove();
+            block.scope.$destroy();
+          }
+
+          for(index = 0; index < collectionLength; index++) {
+            block = nextBlockOrder[index];
+            if(block.scope) {
+              // if we have already seen this object, then we need to reuse the
+              // associated scope/element
+
+              nextNode = previousNode;
+              if(getBlockStart(block) != nextNode) {
+                jqLite(previousNode).after(block.clone);
+              }
+              previousNode = getBlockEnd(block);
+
+              updateScope(block.scope, index, valueIdentifier, block.value, keyIdentifier, block.key, collectionLength, block.group);
+            } else {
+              $transclude(function nyaBsOptionTransclude(clone, scope) {
+                // in case of the debugInfoEnable is set to false, we have to bind the scope to the clone node.
+                setElementIsolateScope(clone, scope);
+
+                block.scope = scope;
+
+                var endNode = nyaBsOptionEndComment.cloneNode(false);
+                clone[clone.length++] = endNode;
+
+                jqLite(previousNode).after(clone);
+
+                // add nya-bs-option class
+                clone.addClass('nya-bs-option');
+
+                // for newly created item we need to ensure its selected status from the model value.
+                if(valueExpFn) {
+                  value = valueExpFn(block.key, block.value);
+                } else {
+                  value = block.value || block.key;
+                }
+
+                if(nyaBsSelectCtrl.isMultiple) {
+                  if(Array.isArray(ngCtrl.$modelValue) && contains(ngCtrl.$modelValue, value)) {
+                    clone.addClass('selected');
+                  }
+                } else {
+                  if(deepEquals(value, ngCtrl.$modelValue)) {
+                    clone.addClass('selected');
+                  }
+                }
+
+                previousNode = endNode;
+                // Note: We only need the first/last node of the cloned nodes.
+                // However, we need to keep the reference to the jqlite wrapper as it might be changed later
+                // by a directive with templateUrl when its template arrives.
+                block.clone = clone;
+                nextBlockMap[block.id] = block;
+                updateScope(block.scope, index, valueIdentifier, block.value, keyIdentifier, block.key, collectionLength, block.group);
+              });
+
+            }
+
+            // we need to mark the first item of a group
+            if(group) {
+              if(!lastGroup || lastGroup !== block.group) {
+                block.clone.addClass('first-in-group');
+              } else {
+                block.clone.removeClass('first-in-group');
+              }
+
+              lastGroup = block.group;
+
+              // add special class for indent
+              block.clone.addClass('group-item');
+            }
+          }
+
+          lastBlockMap = nextBlockMap;
+
+          nyaBsSelectCtrl.onCollectionChange(values, deepWatched);
+        }
+      };
+    }
+  }
+}]);
+
+
+})();

File diff suppressed because it is too large
+ 5 - 0
static/js/nya-bs-select.min.js


File diff suppressed because it is too large
+ 0 - 0
static/js/textAngular-rangy.min.js


+ 791 - 0
static/js/textAngular-sanitize.js

@@ -0,0 +1,791 @@
+/**
+ * @license AngularJS v1.3.10
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'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
+ * @kind 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 input may also contain SVG markup.
+ *   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="sanitizeExample" deps="angular-sanitize.js">
+   <file name="index.html">
+     <script>
+         angular.module('sanitizeExample', ['ngSanitize'])
+           .controller('ExampleController', ['$scope', '$sce', function($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="ExampleController">
+        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>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</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>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
+&lt;/div&gt;</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>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</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("&lt;p style=\"color:blue\"&gt;an html\n" +
+              "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
+              "snippet&lt;/p&gt;");
+     });
+
+     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 &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
+     });
+   </file>
+   </example>
+ */
+function $SanitizeProvider() {
+  this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+    return function(html) {
+      if (typeof arguments[1] != 'undefined') {
+        arguments[1].version = 'taSanitize';
+      }
+      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 =
+       /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
+  END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
+  ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
+  BEGIN_TAG_REGEXP = /^</,
+  BEGING_END_TAGE_REGEXP = /^<\//,
+  COMMENT_REGEXP = /<!--(.*?)-->/g,
+  SINGLE_COMMENT_REGEXP = /(^<!--.*?-->)/,
+  DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
+  CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
+  SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
+  // Match everything outside of normal chars and " (quote character)
+  NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g,
+  WHITE_SPACE_REGEXP = /^(\s+)/;
+
+
+// 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,input");
+
+// 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"));
+
+// SVG Elements
+// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
+var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," +
+        "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," +
+        "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," +
+        "stop,svg,switch,text,title,tspan,use");
+
+// Special Elements (can contain anything)
+var specialElements = makeMap("script,style");
+
+var validElements = angular.extend({},
+                                   voidElements,
+                                   blockElements,
+                                   inlineElements,
+                                   optionalEndTagElements,
+                                   svgElements);
+
+//Attributes that have href and hence need to be sanitized
+var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
+
+var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
+    'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
+    'id,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
+    'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
+    'valign,value,vspace,width');
+
+// SVG attributes (without "id" and "name" attributes)
+// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
+var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
+    'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' +
+    'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' +
+    'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' +
+    'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' +
+    'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' +
+    'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' +
+    'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' +
+    'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' +
+    'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' +
+    'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' +
+    'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' +
+    'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' +
+    'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' +
+    'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' +
+    'zoomAndPan');
+
+var validAttrs = angular.extend({},
+                                uriAttrs,
+                                svgAttrs,
+                                htmlAttrs);
+
+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) {
+  if (typeof html !== 'string') {
+    if (html === null || typeof html === 'undefined') {
+      html = '';
+    } else {
+      html = '' + html;
+    }
+  }
+  var index, chars, match, stack = [], last = html, text;
+  stack.last = function() { return stack[ stack.length - 1 ]; };
+
+  while (html) {
+    text = '';
+    chars = true;
+
+    // Make sure we're not in a script or style element
+    if (!stack.last() || !specialElements[ stack.last() ]) {
+
+      // White space
+      if (WHITE_SPACE_REGEXP.test(html)) {
+        match = html.match(WHITE_SPACE_REGEXP);
+
+        if (match) {
+          var mat = match[0];
+          if (handler.whitespace) handler.whitespace(match[0]);
+          html = html.replace(match[0], '');
+          chars = false;
+        }
+      //Comment
+      } else if (SINGLE_COMMENT_REGEXP.test(html)) {
+        match = html.match(SINGLE_COMMENT_REGEXP);
+
+        if (match) {
+          if (handler.comment) handler.comment(match[1]);
+          html = html.replace(match[0], '');
+          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) {
+          // We only have a valid start-tag if there is a '>'.
+          if (match[4]) {
+            html = html.substring(match[0].length);
+            match[0].replace(START_TAG_REGEXP, parseStartTag);
+          }
+          chars = false;
+        } else {
+          // no ending tag found --- this piece should be encoded as an entity.
+          text += '<';
+          html = html.substring(1);
+        }
+      }
+
+      if (chars) {
+        index = html.indexOf("<");
+
+        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,"&lt;");
+    // 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, '&amp;').
+    replace(SURROGATE_PAIR_REGEXP, function(value) {
+      var hi = value.charCodeAt(0);
+      var low = value.charCodeAt(1);
+      return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
+    }).
+    replace(NON_ALPHANUMERIC_REGEXP, function(value) {
+      // unsafe chars are: \u0000-\u001f \u007f-\u009f \u00ad \u0600-\u0604 \u070f \u17b4 \u17b5 \u200c-\u200f \u2028-\u202f \u2060-\u206f \ufeff \ufff0-\uffff from jslint.com/lint.html
+      // decimal values are: 0-31, 127-159, 173, 1536-1540, 1807, 6068, 6069, 8204-8207, 8232-8239, 8288-8303, 65279, 65520-65535
+      var c = value.charCodeAt(0);
+      // if unsafe character encode
+      if(c <= 159 ||
+        c == 173 ||
+        (c >= 1536 && c <= 1540) ||
+        c == 1807 ||
+        c == 6068 ||
+        c == 6069 ||
+        (c >= 8204 && c <= 8207) ||
+        (c >= 8232 && c <= 8239) ||
+        (c >= 8288 && c <= 8303) ||
+        c == 65279 ||
+        (c >= 65520 && c <= 65535)) return '&#' + c + ';';
+      return value; // avoids multilingual issues
+    }).
+    replace(/</g, '&lt;').
+    replace(/>/g, '&gt;');
+}
+
+var trim = (function() {
+  // native trim is way faster: http://jsperf.com/angular-trim-test
+  // but IE doesn't have it... :-(
+  // TODO: we should move this into IE/ES5 polyfill
+  if (!String.prototype.trim) {
+    return function(value) {
+      return angular.isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value;
+    };
+  }
+  return function(value) {
+    return angular.isString(value) ? value.trim() : value;
+  };
+})();
+
+// Custom logic for accepting certain style options only - textAngular
+// Currently allows only the color, background-color, text-align, float, width and height attributes
+// all other attributes should be easily done through classes.
+function validStyles(styleAttr){
+	var result = '';
+	var styleArray = styleAttr.split(';');
+	angular.forEach(styleArray, function(value){
+		var v = value.split(':');
+		if(v.length == 2){
+			var key = trim(angular.lowercase(v[0]));
+			var value = trim(angular.lowercase(v[1]));
+			if(
+				(key === 'color' || key === 'background-color') && (
+					value.match(/^rgb\([0-9%,\. ]*\)$/i)
+					|| value.match(/^rgba\([0-9%,\. ]*\)$/i)
+					|| value.match(/^hsl\([0-9%,\. ]*\)$/i)
+					|| value.match(/^hsla\([0-9%,\. ]*\)$/i)
+					|| value.match(/^#[0-9a-f]{3,6}$/i)
+					|| value.match(/^[a-z]*$/i)
+				)
+			||
+				key === 'text-align' && (
+					value === 'left'
+					|| value === 'right'
+					|| value === 'center'
+					|| value === 'justify'
+				)
+			||
+                key === 'text-decoration' && (
+                    value === 'underline'
+                    || value === 'line-through'
+                )
+            || key === 'font-weight' && (
+                    value === 'bold'
+                )
+            ||
+				key === 'float' && (
+					value === 'left'
+					|| value === 'right'
+					|| value === 'none'
+				)
+			||
+				(key === 'width' || key === 'height') && (
+					value.match(/[0-9\.]*(px|em|rem|%)/)
+				)
+			|| // Reference #520
+				(key === 'direction' && value.match(/^ltr|rtl|initial|inherit$/))
+			) result += key + ': ' + value + ';';
+		}
+	});
+	return result;
+}
+
+// this function is used to manually allow specific attributes on specific tags with certain prerequisites
+function validCustomTag(tag, attrs, lkey, value){
+	// catch the div placeholder for the iframe replacement
+    if (tag === 'img' && attrs['ta-insert-video']){
+        if(lkey === 'ta-insert-video' || lkey === 'allowfullscreen' || lkey === 'frameborder' || (lkey === 'contenteditable' && value === 'false')) return true;
+    }
+    return false;
+}
+
+/**
+ * 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 ((lkey === 'style' && (value = validStyles(value)) !== '') || validCustomTag(tag, attrs, lkey, value) || validAttrs[lkey] === true &&
+            (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
+            out(' ');
+            out(key);
+            out('="');
+            out(encodeEntities(value));
+            out('"');
+          }
+        });
+        out(unary ? '/>' : '>');
+      }
+    },
+    comment: function (com) {
+      out(com);
+    },
+    whitespace: function (ws) {
+      out(encodeEntities(ws));
+    },
+    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);
+
+/* global sanitizeText: false */
+
+/**
+ * @ngdoc filter
+ * @name linky
+ * @kind function
+ *
+ * @description
+ * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
+ * plain email address links.
+ *
+ * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
+ *
+ * @param {string} text Input text.
+ * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
+ * @returns {string} Html-linkified text.
+ *
+ * @usage
+   <span ng-bind-html="linky_expression | linky"></span>
+ *
+ * @example
+   <example module="linkyExample" deps="angular-sanitize.js">
+     <file name="index.html">
+       <script>
+         angular.module('linkyExample', ['ngSanitize'])
+           .controller('ExampleController', ['$scope', function($scope) {
+             $scope.snippet =
+               'Pretty text with some links:\n'+
+               'http://angularjs.org/,\n'+
+               'mailto:us@somewhere.org,\n'+
+               'another@somewhere.org,\n'+
+               'and one more: ftp://127.0.0.1/.';
+             $scope.snippetWithTarget = 'http://angularjs.org/';
+           }]);
+       </script>
+       <div ng-controller="ExampleController">
+       Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
+       <table>
+         <tr>
+           <td>Filter</td>
+           <td>Source</td>
+           <td>Rendered</td>
+         </tr>
+         <tr id="linky-filter">
+           <td>linky filter</td>
+           <td>
+             <pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
+           </td>
+           <td>
+             <div ng-bind-html="snippet | linky"></div>
+           </td>
+         </tr>
+         <tr id="linky-target">
+          <td>linky target</td>
+          <td>
+            <pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
+          </td>
+          <td>
+            <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
+          </td>
+         </tr>
+         <tr id="escaped-html">
+           <td>no filter</td>
+           <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
+           <td><div ng-bind="snippet"></div></td>
+         </tr>
+       </table>
+     </file>
+     <file name="protractor.js" type="protractor">
+       it('should linkify the snippet with urls', function() {
+         expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+             toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
+                  'another@somewhere.org, and one more: ftp://127.0.0.1/.');
+         expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
+       });
+
+       it('should not linkify snippet without the linky filter', function() {
+         expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
+             toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
+                  'another@somewhere.org, and one more: ftp://127.0.0.1/.');
+         expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
+       });
+
+       it('should update', function() {
+         element(by.model('snippet')).clear();
+         element(by.model('snippet')).sendKeys('new http://link.');
+         expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+             toBe('new http://link.');
+         expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
+         expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
+             .toBe('new http://link.');
+       });
+
+       it('should work with the target property', function() {
+        expect(element(by.id('linky-target')).
+            element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
+            toBe('http://angularjs.org/');
+        expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
+       });
+     </file>
+   </example>
+ */
+angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
+  var LINKY_URL_REGEXP =
+        /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/,
+      MAILTO_REGEXP = /^mailto:/;
+
+  return function(text, target) {
+    if (!text) return text;
+    var match;
+    var raw = text;
+    var html = [];
+    var url;
+    var i;
+    while ((match = raw.match(LINKY_URL_REGEXP))) {
+      // We can not end in these as they are sometimes found at the end of the sentence
+      url = match[0];
+      // if we did not match ftp/http/www/mailto then assume mailto
+      if (!match[2] && !match[4]) {
+        url = (match[3] ? 'http://' : 'mailto:') + url;
+      }
+      i = match.index;
+      addText(raw.substr(0, i));
+      addLink(url, match[0].replace(MAILTO_REGEXP, ''));
+      raw = raw.substring(i + match[0].length);
+    }
+    addText(raw);
+    return $sanitize(html.join(''));
+
+    function addText(text) {
+      if (!text) {
+        return;
+      }
+      html.push(sanitizeText(text));
+    }
+
+    function addLink(url, text) {
+      html.push('<a ');
+      if (angular.isDefined(target)) {
+        html.push('target="',
+                  target,
+                  '" ');
+      }
+      html.push('href="',
+                url.replace(/"/g, '&quot;'),
+                '">');
+      addText(text);
+      html.push('</a>');
+    }
+  };
+}]);
+
+
+})(window, window.angular);

File diff suppressed because it is too large
+ 5 - 0
static/js/textAngular-sanitize.min.js


+ 3069 - 0
static/js/textAngular.js

@@ -0,0 +1,3069 @@
+/*
+@license textAngular
+Author : Austin Anderson
+License : 2013 MIT
+Version 1.5.1
+
+See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use.
+*/
+
+/*
+Commonjs package manager support (eg componentjs).
+*/
+
+
+"use strict";
+// IE version detection - http://stackoverflow.com/questions/4169160/javascript-ie-detection-why-not-use-simple-conditional-comments
+// We need this as IE sometimes plays funny tricks with the contenteditable.
+// ----------------------------------------------------------
+// If you're not in IE (or IE version is less than 5) then:
+// ie === undefined
+// If you're in IE (>=5) then you can determine which version:
+// ie === 7; // IE7
+// Thus, to detect IE:
+// if (ie) {}
+// And to detect the version:
+// ie === 6 // IE6
+// ie > 7 // IE8, IE9, IE10 ...
+// ie < 9 // Anything less than IE9
+// ----------------------------------------------------------
+/* istanbul ignore next: untestable browser check */
+var _browserDetect = {
+	ie: (function(){
+		var undef,
+			v = 3,
+			div = document.createElement('div'),
+			all = div.getElementsByTagName('i');
+
+		while (
+			div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
+			all[0]
+		);
+
+		return v > 4 ? v : undef;
+	}()),
+	webkit: /AppleWebKit\/([\d.]+)/i.test(navigator.userAgent)
+};
+
+// fix a webkit bug, see: https://gist.github.com/shimondoodkin/1081133
+// this is set true when a blur occurs as the blur of the ta-bind triggers before the click
+var globalContentEditableBlur = false;
+/* istanbul ignore next: Browser Un-Focus fix for webkit */
+if(_browserDetect.webkit) {
+	document.addEventListener("mousedown", function(_event){
+		var e = _event || window.event;
+		var curelement = e.target;
+		if(globalContentEditableBlur && curelement !== null){
+			var isEditable = false;
+			var tempEl = curelement;
+			while(tempEl !== null && tempEl.tagName.toLowerCase() !== 'html' && !isEditable){
+				isEditable = tempEl.contentEditable === 'true';
+				tempEl = tempEl.parentNode;
+			}
+			if(!isEditable){
+				document.getElementById('textAngular-editableFix-010203040506070809').setSelectionRange(0, 0); // set caret focus to an element that handles caret focus correctly.
+				curelement.focus(); // focus the wanted element.
+				if (curelement.select) {
+					curelement.select(); // use select to place cursor for input elements.
+				}
+			}
+		}
+		globalContentEditableBlur = false;
+	}, false); // add global click handler
+	angular.element(document).ready(function () {
+		angular.element(document.body).append(angular.element('<input id="textAngular-editableFix-010203040506070809" class="ta-hidden-input" aria-hidden="true" unselectable="on" tabIndex="-1">'));
+	});
+}
+
+// Gloabl to textAngular REGEXP vars for block and list elements.
+
+var BLOCKELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/i;
+var LISTELEMENTS = /^(ul|li|ol)$/i;
+var VALIDELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/i;
+
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Compatibility
+/* istanbul ignore next: trim shim for older browsers */
+if (!String.prototype.trim) {
+	String.prototype.trim = function () {
+		return this.replace(/^\s+|\s+$/g, '');
+	};
+}
+
+/*
+	Custom stylesheet for the placeholders rules.
+	Credit to: http://davidwalsh.name/add-rules-stylesheets
+*/
+var sheet, addCSSRule, removeCSSRule, _addCSSRule, _removeCSSRule, _getRuleIndex;
+/* istanbul ignore else: IE <8 test*/
+if(_browserDetect.ie > 8 || _browserDetect.ie === undefined){
+	var _sheets = document.styleSheets;
+	/* istanbul ignore next: preference for stylesheet loaded externally */
+	for(var i = 0; i < _sheets.length; i++){
+		if(_sheets[i].media.length === 0 || _sheets[i].media.mediaText.match(/(all|screen)/ig)){
+			if(_sheets[i].href){
+				if(_sheets[i].href.match(/textangular\.(min\.|)css/ig)){
+					sheet = _sheets[i];
+					break;
+				}
+			}
+		}
+	}
+	/* istanbul ignore next: preference for stylesheet loaded externally */
+	if(!sheet){
+		// this sheet is used for the placeholders later on.
+		sheet = (function() {
+			// Create the <style> tag
+			var style = document.createElement("style");
+			/* istanbul ignore else : WebKit hack :( */
+			if(_browserDetect.webkit) style.appendChild(document.createTextNode(""));
+
+			// Add the <style> element to the page, add as first so the styles can be overridden by custom stylesheets
+			document.getElementsByTagName('head')[0].appendChild(style);
+
+			return style.sheet;
+		})();
+	}
+
+	// use as: addCSSRule("header", "float: left");
+	addCSSRule = function(selector, rules) {
+		return _addCSSRule(sheet, selector, rules);
+	};
+	_addCSSRule = function(_sheet, selector, rules){
+		var insertIndex;
+		var insertedRule;
+		// This order is important as IE 11 has both cssRules and rules but they have different lengths - cssRules is correct, rules gives an error in IE 11
+		/* istanbul ignore next: browser catches */
+		if(_sheet.cssRules) insertIndex = Math.max(_sheet.cssRules.length - 1, 0);
+		else if(_sheet.rules) insertIndex = Math.max(_sheet.rules.length - 1, 0);
+
+		/* istanbul ignore else: untestable IE option */
+		if(_sheet.insertRule) {
+			_sheet.insertRule(selector + "{" + rules + "}", insertIndex);
+		}
+		else {
+			_sheet.addRule(selector, rules, insertIndex);
+		}
+		/* istanbul ignore next: browser catches */
+		if(sheet.rules) insertedRule = sheet.rules[insertIndex];
+		else if(sheet.cssRules) insertedRule = sheet.cssRules[insertIndex];
+		// return the inserted stylesheet rule
+		return insertedRule;
+	};
+
+	_getRuleIndex = function(rule, rules) {
+		var i, ruleIndex;
+		for (i=0; i < rules.length; i++) {
+			/* istanbul ignore else: check for correct rule */
+			if (rules[i].cssText === rule.cssText) {
+				ruleIndex = i;
+				break;
+			}
+		}
+		return ruleIndex;
+	};
+
+	removeCSSRule = function(rule){
+		_removeCSSRule(sheet, rule);
+	};
+	/* istanbul ignore next: tests are browser specific */
+	_removeCSSRule = function(sheet, rule){
+		var rules = sheet.cssRules || sheet.rules;
+		if(!rules || rules.length === 0) return;
+		var ruleIndex = _getRuleIndex(rule, rules);
+		if(sheet.removeRule){
+			sheet.removeRule(ruleIndex);
+		}else{
+			sheet.deleteRule(ruleIndex);
+		}
+	};
+}
+
+angular.module('textAngular.factories', [])
+.factory('taBrowserTag', [function(){
+	return function(tag){
+		/* istanbul ignore next: ie specific test */
+		if(!tag) return (_browserDetect.ie <= 8)? 'P' : 'p';
+		else if(tag === '') return (_browserDetect.ie === undefined)? 'div' : (_browserDetect.ie <= 8)? 'P' : 'p';
+		else return (_browserDetect.ie <= 8)? tag.toUpperCase() : tag;
+	};
+}]).factory('taApplyCustomRenderers', ['taCustomRenderers', 'taDOM', function(taCustomRenderers, taDOM){
+	return function(val){
+		var element = angular.element('<div></div>');
+		element[0].innerHTML = val;
+
+		angular.forEach(taCustomRenderers, function(renderer){
+			var elements = [];
+			// get elements based on what is defined. If both defined do secondary filter in the forEach after using selector string
+			if(renderer.selector && renderer.selector !== '')
+				elements = element.find(renderer.selector);
+			/* istanbul ignore else: shouldn't fire, if it does we're ignoring everything */
+			else if(renderer.customAttribute && renderer.customAttribute !== '')
+				elements = taDOM.getByAttribute(element, renderer.customAttribute);
+			// process elements if any found
+			angular.forEach(elements, function(_element){
+				_element = angular.element(_element);
+				if(renderer.selector && renderer.selector !== '' && renderer.customAttribute && renderer.customAttribute !== ''){
+					if(_element.attr(renderer.customAttribute) !== undefined) renderer.renderLogic(_element);
+				} else renderer.renderLogic(_element);
+			});
+		});
+
+		return element[0].innerHTML;
+	};
+}]).factory('taFixChrome', function(){
+	// get whaterever rubbish is inserted in chrome
+	// should be passed an html string, returns an html string
+	var taFixChrome = function(html){
+		if(!html || !angular.isString(html) || html.length <= 0) return html;
+		// grab all elements with a style attibute
+		var spanMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
+		var match, styleVal, newTag, finalHtml = '', lastIndex = 0;
+		while(match = spanMatch.exec(html)){
+			// one of the quoted values ' or "
+			/* istanbul ignore next: quotations match */
+			styleVal = match[3] || match[4];
+			// test for chrome inserted junk
+			if(styleVal && styleVal.match(/line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;/i)){
+				// replace original tag with new tag
+				styleVal = styleVal.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;/ig, '');
+				newTag = '<' + match[1].trim();
+				if(styleVal.trim().length > 0) newTag += ' style=' + match[2].substring(0,1) + styleVal + match[2].substring(0,1);
+				newTag += match[5].trim() + ">";
+				finalHtml += html.substring(lastIndex, match.index) + newTag;
+				lastIndex = match.index + match[0].length;
+			}
+		}
+		finalHtml += html.substring(lastIndex);
+		// only replace when something has changed, else we get focus problems on inserting lists
+		if(lastIndex > 0){
+			// replace all empty strings
+			return finalHtml.replace(/<span\s?>(.*?)<\/span>(<br(\/|)>|)/ig, '$1');
+		} else return html;
+	};
+	return taFixChrome;
+}).factory('taSanitize', ['$sanitize', function taSanitizeFactory($sanitize){
+
+	var convert_infos = [
+		{
+			property: 'font-weight',
+			values: [ 'bold' ],
+			tag: 'b'
+		},
+		{
+			property: 'font-style',
+			values: [ 'italic' ],
+			tag: 'i'
+		}
+	];
+	
+	var styleMatch = [];
+	for(var i = 0; i < convert_infos.length; i++){
+		var _partialStyle = '(' + convert_infos[i].property + ':\\s*(';
+		for(var j = 0; j < convert_infos[i].values.length; j++){
+			/* istanbul ignore next: not needed to be tested yet */
+			if(j > 0) _partialStyle += '|';
+			_partialStyle += convert_infos[i].values[j];
+		}
+		_partialStyle += ');)';
+		styleMatch.push(_partialStyle);
+	}
+	var styleRegexString = '(' + styleMatch.join('|') + ')';
+	
+	function wrapNested(html, wrapTag) {
+		var depth = 0;
+		var lastIndex = 0;
+		var match;
+		var tagRegex = /<[^>]*>/ig;
+		while(match = tagRegex.exec(html)){
+			lastIndex = match.index;
+			if(match[0].substr(1, 1) === '/'){
+				if(depth === 0) break;
+				else depth--;
+			}else depth++;
+		}
+		return wrapTag +
+			html.substring(0, lastIndex) +
+			// get the start tags reversed - this is safe as we construct the strings with no content except the tags
+			angular.element(wrapTag)[0].outerHTML.substring(wrapTag.length) +
+			html.substring(lastIndex);
+	}
+	
+	function transformLegacyStyles(html){
+		if(!html || !angular.isString(html) || html.length <= 0) return html;
+		var i;
+		var styleElementMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
+		var match, subMatch, styleVal, newTag, lastNewTag = '', newHtml, finalHtml = '', lastIndex = 0;
+		while(match = styleElementMatch.exec(html)){
+			// one of the quoted values ' or "
+			/* istanbul ignore next: quotations match */
+			styleVal = match[3] || match[4];
+			var styleRegex = new RegExp(styleRegexString, 'i');
+			// test for style values to change
+			if(angular.isString(styleVal) && styleRegex.test(styleVal)){
+				// remove build tag list
+				newTag = '';
+				// init regex here for exec
+				var styleRegexExec = new RegExp(styleRegexString, 'ig');
+				// find relevand tags and build a string of them
+				while(subMatch = styleRegexExec.exec(styleVal)){
+					for(i = 0; i < convert_infos.length; i++){
+						if(!!subMatch[(i*2) + 2]){
+							newTag += '<' + convert_infos[i].tag + '>';
+						}
+					}
+				}
+				// recursively find more legacy styles in html before this tag and after the previous match (if any)
+				newHtml = transformLegacyStyles(html.substring(lastIndex, match.index));
+				// build up html
+				if(lastNewTag.length > 0){
+					finalHtml += wrapNested(newHtml, lastNewTag);
+				}else finalHtml += newHtml;
+				// grab the style val without the transformed values
+				styleVal = styleVal.replace(new RegExp(styleRegexString, 'ig'), '');
+				// build the html tag
+				finalHtml += '<' + match[1].trim();
+				if(styleVal.length > 0) finalHtml += ' style="' + styleVal + '"';
+				finalHtml += match[5] + '>';
+				// update the start index to after this tag
+				lastIndex = match.index + match[0].length;
+				lastNewTag = newTag;
+			}
+		}
+		if(lastNewTag.length > 0){
+			finalHtml += wrapNested(html.substring(lastIndex), lastNewTag);
+		}
+		else finalHtml += html.substring(lastIndex);
+		return finalHtml;
+	}
+	
+	function transformLegacyAttributes(html){
+		if(!html || !angular.isString(html) || html.length <= 0) return html;
+		// replace all align='...' tags with text-align attributes
+		var attrElementMatch = /<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/ig;
+		var match, finalHtml = '', lastIndex = 0;
+		// match all attr tags
+		while(match = attrElementMatch.exec(html)){
+			// add all html before this tag
+			finalHtml += html.substring(lastIndex, match.index);
+			// record last index after this tag
+			lastIndex = match.index + match[0].length;
+			// construct tag without the align attribute
+			var newTag = '<' + match[1] + match[5];
+			// add the style attribute
+			if(/style=("([^"]+)"|'([^']+)')/ig.test(newTag)){
+				/* istanbul ignore next: quotations match */
+				newTag = newTag.replace(/style=("([^"]+)"|'([^']+)')/i, 'style="$2$3 text-align:' + (match[3] || match[4]) + ';"');
+			}else{
+				/* istanbul ignore next: quotations match */
+				newTag += ' style="text-align:' + (match[3] || match[4]) + ';"';
+			}
+			newTag += '>';
+			// add to html
+			finalHtml += newTag;
+		}
+		// return with remaining html
+		return finalHtml + html.substring(lastIndex);
+	}
+	
+	return function taSanitize(unsafe, oldsafe, ignore){
+		// unsafe html should NEVER built into a DOM object via angular.element. This allows XSS to be inserted and run.
+		if ( !ignore ) {
+			try {
+				unsafe = transformLegacyStyles(unsafe);
+			} catch (e) {
+			}
+		}
+
+		// unsafe and oldsafe should be valid HTML strings
+		// any exceptions (lets say, color for example) should be made here but with great care
+		// setup unsafe element for modification
+		unsafe = transformLegacyAttributes(unsafe);
+		
+		var safe;
+		try {
+			safe = $sanitize(unsafe);
+			// do this afterwards, then the $sanitizer should still throw for bad markup
+			if(ignore) safe = unsafe;
+		} catch (e){
+			safe = oldsafe || '';
+		}
+		
+		// Do processing for <pre> tags, removing tabs and return carriages outside of them
+		
+		var _preTags = safe.match(/(<pre[^>]*>.*?<\/pre[^>]*>)/ig);
+		var processedSafe = safe.replace(/(&#(9|10);)*/ig, '');
+		var re = /<pre[^>]*>.*?<\/pre[^>]*>/ig;
+		var index = 0;
+		var lastIndex = 0;
+		var origTag;
+		safe = '';
+		while((origTag = re.exec(processedSafe)) !== null && index < _preTags.length){
+			safe += processedSafe.substring(lastIndex, origTag.index) + _preTags[index];
+			lastIndex = origTag.index + origTag[0].length;
+			index++;
+		}
+		return safe + processedSafe.substring(lastIndex);
+	};
+}]).factory('taToolExecuteAction', ['$q', '$log', function($q, $log){
+	// this must be called on a toolScope or instance
+	return function(editor){
+		if(editor !== undefined) this.$editor = function(){ return editor; };
+		var deferred = $q.defer(),
+			promise = deferred.promise,
+			_editor = this.$editor();
+		// pass into the action the deferred function and also the function to reload the current selection if rangy available
+		var result;
+		try{
+			result = this.action(deferred, _editor.startAction());
+			// We set the .finally callback here to make sure it doesn't get executed before any other .then callback.
+			promise['finally'](function(){
+				_editor.endAction.call(_editor);
+			});
+		}catch(exc){
+			$log.error(exc);
+		}
+		if(result || result === undefined){
+			// if true or undefined is returned then the action has finished. Otherwise the deferred action will be resolved manually.
+			deferred.resolve();
+		}
+	};
+}]);
+angular.module('textAngular.DOM', ['textAngular.factories'])
+.factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', function(taSelection, taBrowserTag, $document){
+	var listToDefault = function(listElement, defaultWrap){
+		var $target, i;
+		// if all selected then we should remove the list
+		// grab all li elements and convert to taDefaultWrap tags
+		var children = listElement.find('li');
+		for(i = children.length - 1; i >= 0; i--){
+			$target = angular.element('<' + defaultWrap + '>' + children[i].innerHTML + '</' + defaultWrap + '>');
+			listElement.after($target);
+		}
+		listElement.remove();
+		taSelection.setSelectionToElementEnd($target[0]);
+	};
+	var selectLi = function(liElement){
+		if(/(<br(|\/)>)$/i.test(liElement.innerHTML.trim())) taSelection.setSelectionBeforeElement(angular.element(liElement).find("br")[0]);
+		else taSelection.setSelectionToElementEnd(liElement);
+	};
+	var listToList = function(listElement, newListTag){
+		var $target = angular.element('<' + newListTag + '>' + listElement[0].innerHTML + '</' + newListTag + '>');
+		listElement.after($target);
+		listElement.remove();
+		selectLi($target.find('li')[0]);
+	};
+	var childElementsToList = function(elements, listElement, newListTag){
+		var html = '';
+		for(var i = 0; i < elements.length; i++){
+			html += '<' + taBrowserTag('li') + '>' + elements[i].innerHTML + '</' + taBrowserTag('li') + '>';
+		}
+		var $target = angular.element('<' + newListTag + '>' + html + '</' + newListTag + '>');
+		listElement.after($target);
+		listElement.remove();
+		selectLi($target.find('li')[0]);
+	};
+	return function(taDefaultWrap, topNode){
+		taDefaultWrap = taBrowserTag(taDefaultWrap);
+		return function(command, showUI, options, defaultTagAttributes){
+			var i, $target, html, _nodes, next, optionsTagName, selectedElement;
+			var defaultWrapper = angular.element('<' + taDefaultWrap + '>');
+			try{
+				selectedElement = taSelection.getSelectionElement();
+			}catch(e){}
+			var $selected = angular.element(selectedElement);
+			if(selectedElement !== undefined){
+				var tagName = selectedElement.tagName.toLowerCase();
+				if(command.toLowerCase() === 'insertorderedlist' || command.toLowerCase() === 'insertunorderedlist'){
+					var selfTag = taBrowserTag((command.toLowerCase() === 'insertorderedlist')? 'ol' : 'ul');
+					if(tagName === selfTag){
+						// if all selected then we should remove the list
+						// grab all li elements and convert to taDefaultWrap tags
+						return listToDefault($selected, taDefaultWrap);
+					}else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() === selfTag && $selected.parent().children().length === 1){
+						// catch for the previous statement if only one li exists
+						return listToDefault($selected.parent(), taDefaultWrap);
+					}else if(tagName === 'li' && $selected.parent()[0].tagName.toLowerCase() !== selfTag && $selected.parent().children().length === 1){
+						// catch for the previous statement if only one li exists
+						return listToList($selected.parent(), selfTag);
+					}else if(tagName.match(BLOCKELEMENTS) && !$selected.hasClass('ta-bind')){
+						// if it's one of those block elements we have to change the contents
+						// if it's a ol/ul we are changing from one to the other
+						if(tagName === 'ol' || tagName === 'ul'){
+							return listToList($selected, selfTag);
+						}else{
+							var childBlockElements = false;
+							angular.forEach($selected.children(), function(elem){
+								if(elem.tagName.match(BLOCKELEMENTS)) {
+									childBlockElements = true;
+								}
+							});
+							if(childBlockElements){
+								return childElementsToList($selected.children(), $selected, selfTag);
+							}else{
+								return childElementsToList([angular.element('<div>' + selectedElement.innerHTML + '</div>')[0]], $selected, selfTag);
+							}
+						}
+					}else if(tagName.match(BLOCKELEMENTS)){
+						// if we get here then all the contents of the ta-bind are selected
+						_nodes = taSelection.getOnlySelectedElements();
+						if(_nodes.length === 0){
+							// here is if there is only text in ta-bind ie <div ta-bind>test content</div>
+							$target = angular.element('<' + selfTag + '><li>' + selectedElement.innerHTML + '</li></' + selfTag + '>');
+							$selected.html('');
+							$selected.append($target);
+						}else if(_nodes.length === 1 && (_nodes[0].tagName.toLowerCase() === 'ol' || _nodes[0].tagName.toLowerCase() === 'ul')){
+							if(_nodes[0].tagName.toLowerCase() === selfTag){
+								// remove
+								return listToDefault(angular.element(_nodes[0]), taDefaultWrap);
+							}else{
+								return listToList(angular.element(_nodes[0]), selfTag);
+							}
+						}else{
+							html = '';
+							var $nodes = [];
+							for(i = 0; i < _nodes.length; i++){
+								/* istanbul ignore else: catch for real-world can't make it occur in testing */
+								if(_nodes[i].nodeType !== 3){
+									var $n = angular.element(_nodes[i]);
+									/* istanbul ignore if: browser check only, phantomjs doesn't return children nodes but chrome at least does */
+									if(_nodes[i].tagName.toLowerCase() === 'li') continue;
+									else if(_nodes[i].tagName.toLowerCase() === 'ol' || _nodes[i].tagName.toLowerCase() === 'ul'){
+										html += $n[0].innerHTML; // if it's a list, add all it's children
+									}else if(_nodes[i].tagName.toLowerCase() === 'span' && (_nodes[i].childNodes[0].tagName.toLowerCase() === 'ol' || _nodes[i].childNodes[0].tagName.toLowerCase() === 'ul')){
+										html += $n[0].childNodes[0].innerHTML; // if it's a list, add all it's children
+									}else{
+										html += '<' + taBrowserTag('li') + '>' + $n[0].innerHTML + '</' + taBrowserTag('li') + '>';
+									}
+									$nodes.unshift($n);
+								}
+							}
+							$target = angular.element('<' + selfTag + '>' + html + '</' + selfTag + '>');
+							$nodes.pop().replaceWith($target);
+							angular.forEach($nodes, function($node){ $node.remove(); });
+						}
+						taSelection.setSelectionToElementEnd($target[0]);
+						return;
+					}
+				}else if(command.toLowerCase() === 'formatblock'){
+					optionsTagName = options.toLowerCase().replace(/[<>]/ig, '');
+					if(optionsTagName.trim() === 'default') {
+						optionsTagName = taDefaultWrap;
+						options = '<' + taDefaultWrap + '>';
+					}
+					if(tagName === 'li') $target = $selected.parent();
+					else $target = $selected;
+					// find the first blockElement
+					while(!$target[0].tagName || !$target[0].tagName.match(BLOCKELEMENTS) && !$target.parent().attr('contenteditable')){
+						$target = $target.parent();
+						/* istanbul ignore next */
+						tagName = ($target[0].tagName || '').toLowerCase();
+					}
+					if(tagName === optionsTagName){
+						// $target is wrap element
+						_nodes = $target.children();
+						var hasBlock = false;
+						for(i = 0; i < _nodes.length; i++){
+							hasBlock = hasBlock || _nodes[i].tagName.match(BLOCKELEMENTS);
+						}
+						if(hasBlock){
+							$target.after(_nodes);
+							next = $target.next();
+							$target.remove();
+							$target = next;
+						}else{
+							defaultWrapper.append($target[0].childNodes);
+							$target.after(defaultWrapper);
+							$target.remove();
+							$target = defaultWrapper;
+						}
+					}else if($target.parent()[0].tagName.toLowerCase() === optionsTagName && !$target.parent().hasClass('ta-bind')){
+						//unwrap logic for parent
+						var blockElement = $target.parent();
+						var contents = blockElement.contents();
+						for(i = 0; i < contents.length; i ++){
+							/* istanbul ignore next: can't test - some wierd thing with how phantomjs works */
+							if(blockElement.parent().hasClass('ta-bind') && contents[i].nodeType === 3){
+								defaultWrapper = angular.element('<' + taDefaultWrap + '>');
+								defaultWrapper[0].innerHTML = contents[i].outerHTML;
+								contents[i] = defaultWrapper[0];
+							}
+							blockElement.parent()[0].insertBefore(contents[i], blockElement[0]);
+						}
+						blockElement.remove();
+					}else if(tagName.match(LISTELEMENTS)){
+						// wrapping a list element
+						$target.wrap(options);
+					}else{
+						// default wrap behaviour
+						_nodes = taSelection.getOnlySelectedElements();
+						if(_nodes.length === 0) _nodes = [$target[0]];
+						// find the parent block element if any of the nodes are inline or text
+						for(i = 0; i < _nodes.length; i++){
+							if(_nodes[i].nodeType === 3 || !_nodes[i].tagName.match(BLOCKELEMENTS)){
+								while(_nodes[i].nodeType === 3 || !_nodes[i].tagName || !_nodes[i].tagName.match(BLOCKELEMENTS)){
+									_nodes[i] = _nodes[i].parentNode;
+								}
+							}
+						}
+						if(angular.element(_nodes[0]).hasClass('ta-bind')){
+							$target = angular.element(options);
+							$target[0].innerHTML = _nodes[0].innerHTML;
+							_nodes[0].innerHTML = $target[0].outerHTML;
+						}else if(optionsTagName === 'blockquote'){
+							// blockquotes wrap other block elements
+							html = '';
+							for(i = 0; i < _nodes.length; i++){
+								html += _nodes[i].outerHTML;
+							}
+							$target = angular.element(options);
+							$target[0].innerHTML = html;
+							_nodes[0].parentNode.insertBefore($target[0],_nodes[0]);
+							for(i = _nodes.length - 1; i >= 0; i--){
+								/* istanbul ignore else:  */
+								if(_nodes[i].parentNode) _nodes[i].parentNode.removeChild(_nodes[i]);
+							}
+						}
+						else {
+							// regular block elements replace other block elements
+							for(i = 0; i < _nodes.length; i++){
+								$target = angular.element(options);
+								$target[0].innerHTML = _nodes[i].innerHTML;
+								_nodes[i].parentNode.insertBefore($target[0],_nodes[i]);
+								_nodes[i].parentNode.removeChild(_nodes[i]);
+							}
+						}
+					}
+					taSelection.setSelectionToElementEnd($target[0]);
+					return;
+				}else if(command.toLowerCase() === 'createlink'){
+					var tagBegin = '<a href="' + options + '" target="' +
+							(defaultTagAttributes.a.target ? defaultTagAttributes.a.target : '') +
+							'">',
+						tagEnd = '</a>',
+						_selection = taSelection.getSelection();
+					if(_selection.collapsed){
+						// insert text at selection, then select then just let normal exec-command run
+						taSelection.insertHtml(tagBegin + options + tagEnd, topNode);
+					}else if(rangy.getSelection().getRangeAt(0).canSurroundContents()){
+						var node = angular.element(tagBegin + tagEnd)[0];
+						rangy.getSelection().getRangeAt(0).surroundContents(node);
+					}
+					return;
+				}else if(command.toLowerCase() === 'inserthtml'){
+					taSelection.insertHtml(options, topNode);
+					return;
+				}
+			}
+			try{
+				$document[0].execCommand(command, showUI, options);
+			}catch(e){}
+		};
+	};
+}]).service('taSelection', ['$document', 'taDOM',
+/* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */
+function($document, taDOM){
+	// need to dereference the document else the calls don't work correctly
+	var _document = $document[0];
+	var brException = function (element, offset) {
+		/* check if selection is a BR element at the beginning of a container. If so, get
+		* the parentNode instead.
+		* offset should be zero in this case. Otherwise, return the original
+		* element.
+		*/
+		if (element.tagName && element.tagName.match(/^br$/i) && offset === 0 && !element.previousSibling) {
+            return {
+                element: element.parentNode,
+                offset: 0
+            };
+		} else {
+			return {
+				element: element,
+				offset: offset
+			};
+		}
+	};
+	var api = {
+		getSelection: function(){
+			var range = rangy.getSelection().getRangeAt(0);
+			var container = range.commonAncestorContainer;
+			var selection = {
+				start: brException(range.startContainer, range.startOffset),
+				end: brException(range.endContainer, range.endOffset),
+				collapsed: range.collapsed
+			};
+			// Check if the container is a text node and return its parent if so
+			container = container.nodeType === 3 ? container.parentNode : container;
+			if (container.parentNode === selection.start.element ||
+				container.parentNode === selection.end.element) {
+				selection.container = container.parentNode;
+			} else {
+				selection.container = container;
+			}
+			return selection;
+		},
+		getOnlySelectedElements: function(){
+			var range = rangy.getSelection().getRangeAt(0);
+			var container = range.commonAncestorContainer;
+			// Check if the container is a text node and return its parent if so
+			container = container.nodeType === 3 ? container.parentNode : container;
+			return range.getNodes([1], function(node){
+				return node.parentNode === container;
+			});
+		},
+		// Some basic selection functions
+		getSelectionElement: function () {
+			return api.getSelection().container;
+		},
+		setSelection: function(el, start, end){
+			var range = rangy.createRange();
+			
+			range.setStart(el, start);
+			range.setEnd(el, end);
+			
+			rangy.getSelection().setSingleRange(range);
+		},
+		setSelectionBeforeElement: function (el){
+			var range = rangy.createRange();
+			
+			range.selectNode(el);
+			range.collapse(true);
+			
+			rangy.getSelection().setSingleRange(range);
+		},
+		setSelectionAfterElement: function (el){
+			var range = rangy.createRange();
+			
+			range.selectNode(el);
+			range.collapse(false);
+			
+			rangy.getSelection().setSingleRange(range);
+		},
+		setSelectionToElementStart: function (el){
+			var range = rangy.createRange();
+			
+			range.selectNodeContents(el);
+			range.collapse(true);
+			
+			rangy.getSelection().setSingleRange(range);
+		},
+		setSelectionToElementEnd: function (el){
+			var range = rangy.createRange();
+			
+			range.selectNodeContents(el);
+			range.collapse(false);
+			if(el.childNodes && el.childNodes[el.childNodes.length - 1] && el.childNodes[el.childNodes.length - 1].nodeName === 'br'){
+				range.startOffset = range.endOffset = range.startOffset - 1;
+			}
+			rangy.getSelection().setSingleRange(range);
+		},
+		// from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
+		// topNode is the contenteditable normally, all manipulation MUST be inside this.
+		insertHtml: function(html, topNode){
+			var parent, secondParent, _childI, nodes, i, lastNode, _tempFrag;
+			var element = angular.element("<div>" + html + "</div>");
+			var range = rangy.getSelection().getRangeAt(0);
+			var frag = _document.createDocumentFragment();
+			var children = element[0].childNodes;
+			var isInline = true;
+			
+			if(children.length > 0){
+				// NOTE!! We need to do the following:
+				// check for blockelements - if they exist then we have to split the current element in half (and all others up to the closest block element) and insert all children in-between.
+				// If there are no block elements, or there is a mixture we need to create textNodes for the non wrapped text (we don't want them spans messing up the picture).
+				nodes = [];
+				for(_childI = 0; _childI < children.length; _childI++){
+					if(!(
+						(children[_childI].nodeName.toLowerCase() === 'p' && children[_childI].innerHTML.trim() === '') || // empty p element
+						(children[_childI].nodeType === 3 && children[_childI].nodeValue.trim() === '') // empty text node
+					)){
+						isInline = isInline && !BLOCKELEMENTS.test(children[_childI].nodeName);
+						nodes.push(children[_childI]);
+					}
+				}
+				for(var _n = 0; _n < nodes.length; _n++) lastNode = frag.appendChild(nodes[_n]);
+				if(!isInline && range.collapsed && /^(|<br(|\/)>)$/i.test(range.startContainer.innerHTML)) range.selectNode(range.startContainer);
+			}else{
+				isInline = true;
+				// paste text of some sort
+				lastNode = frag = _document.createTextNode(html);
+			}
+			
+			// Other Edge case - selected data spans multiple blocks.
+			if(isInline){
+				range.deleteContents();
+			}else{ // not inline insert
+				if(range.collapsed && range.startContainer !== topNode){
+					if(range.startContainer.innerHTML && range.startContainer.innerHTML.match(/^<[^>]*>$/i)){
+						// this log is to catch when innerHTML is something like `<img ...>`
+						parent = range.startContainer;
+						if(range.startOffset === 1){
+							// before single tag
+							range.setStartAfter(parent);
+							range.setEndAfter(parent);
+						}else{
+							// after single tag
+							range.setStartBefore(parent);
+							range.setEndBefore(parent);
+						}
+					}else{
+						// split element into 2 and insert block element in middle
+						if(range.startContainer.nodeType === 3 && range.startContainer.parentNode !== topNode){ // if text node
+							parent = range.startContainer.parentNode;
+							secondParent = parent.cloneNode();
+							// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
+							taDOM.splitNodes(parent.childNodes, parent, secondParent, range.startContainer, range.startOffset);
+							
+							// Escape out of the inline tags like b
+							while(!VALIDELEMENTS.test(parent.nodeName)){
+								angular.element(parent).after(secondParent);
+								parent = parent.parentNode;
+								var _lastSecondParent = secondParent;
+								secondParent = parent.cloneNode();
+								// split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
+								taDOM.splitNodes(parent.childNodes, parent, secondParent, _lastSecondParent);
+							}
+						}else{
+							parent = range.startContainer;
+							secondParent = parent.cloneNode();
+							taDOM.splitNodes(parent.childNodes, parent, secondParent, undefined, undefined, range.startOffset);
+						}
+						
+						angular.element(parent).after(secondParent);
+						// put cursor to end of inserted content
+						range.setStartAfter(parent);
+						range.setEndAfter(parent);
+						
+						if(/^(|<br(|\/)>)$/i.test(parent.innerHTML.trim())){
+							range.setStartBefore(parent);
+							range.setEndBefore(parent);
+							angular.element(parent).remove();
+						}
+						if(/^(|<br(|\/)>)$/i.test(secondParent.innerHTML.trim())) angular.element(secondParent).remove();
+						if(parent.nodeName.toLowerCase() === 'li'){
+							_tempFrag = _document.createDocumentFragment();
+							for(i = 0; i < frag.childNodes.length; i++){
+								element = angular.element('<li>');
+								taDOM.transferChildNodes(frag.childNodes[i], element[0]);
+								taDOM.transferNodeAttributes(frag.childNodes[i], element[0]);
+								_tempFrag.appendChild(element[0]);
+							}
+							frag = _tempFrag;
+							if(lastNode){
+								lastNode = frag.childNodes[frag.childNodes.length - 1];
+								lastNode = lastNode.childNodes[lastNode.childNodes.length - 1];
+							}
+						}
+					}
+				}else{
+					range.deleteContents();
+				}
+			}
+			
+			range.insertNode(frag);
+			if(lastNode){
+				api.setSelectionToElementEnd(lastNode);
+			}
+		}
+	};
+	return api;
+}]).service('taDOM', function(){
+	var taDOM = {
+		// recursive function that returns an array of angular.elements that have the passed attribute set on them
+		getByAttribute: function(element, attribute){
+			var resultingElements = [];
+			var childNodes = element.children();
+			if(childNodes.length){
+				angular.forEach(childNodes, function(child){
+					resultingElements = resultingElements.concat(taDOM.getByAttribute(angular.element(child), attribute));
+				});
+			}
+			if(element.attr(attribute) !== undefined) resultingElements.push(element);
+			return resultingElements;
+		},
+		
+		transferChildNodes: function(source, target){
+			// clear out target
+			target.innerHTML = '';
+			while(source.childNodes.length > 0) target.appendChild(source.childNodes[0]);
+			return target;
+		},
+		
+		splitNodes: function(nodes, target1, target2, splitNode, subSplitIndex, splitIndex){
+			if(!splitNode && isNaN(splitIndex)) throw new Error('taDOM.splitNodes requires a splitNode or splitIndex');
+			var startNodes = document.createDocumentFragment();
+			var endNodes = document.createDocumentFragment();
+			var index = 0;
+			
+			while(nodes.length > 0 && (isNaN(splitIndex) || splitIndex !== index) && nodes[0] !== splitNode){
+				startNodes.appendChild(nodes[0]); // this removes from the nodes array (if proper childNodes object.
+				index++;
+			}
+			
+			if(!isNaN(subSplitIndex) && subSplitIndex >= 0 && nodes[0]){
+				startNodes.appendChild(document.createTextNode(nodes[0].nodeValue.substring(0, subSplitIndex)));
+				nodes[0].nodeValue = nodes[0].nodeValue.substring(subSplitIndex);
+			}
+			while(nodes.length > 0) endNodes.appendChild(nodes[0]);
+			
+			taDOM.transferChildNodes(startNodes, target1);
+			taDOM.transferChildNodes(endNodes, target2);
+		},
+		
+		transferNodeAttributes: function(source, target){
+			for(var i = 0; i < source.attributes.length; i++) target.setAttribute(source.attributes[i].name, source.attributes[i].value);
+			return target;
+		}
+	};
+	return taDOM;
+});
+angular.module('textAngular.validators', [])
+.directive('taMaxText', function(){
+	return {
+		restrict: 'A',
+		require: 'ngModel',
+		link: function(scope, elem, attrs, ctrl){
+			var max = parseInt(scope.$eval(attrs.taMaxText));
+			if (isNaN(max)){
+				throw('Max text must be an integer');
+			}
+			attrs.$observe('taMaxText', function(value){
+				max = parseInt(value);
+				if (isNaN(max)){
+					throw('Max text must be an integer');
+				}
+				if (ctrl.$dirty){
+					ctrl.$validate();
+				}
+			});
+			ctrl.$validators.taMaxText = function(viewValue){
+				var source = angular.element('<div/>');
+				source.html(viewValue);
+				return source.text().length <= max;
+			};
+		}
+	};
+}).directive('taMinText', function(){
+	return {
+		restrict: 'A',
+		require: 'ngModel',
+		link: function(scope, elem, attrs, ctrl){
+			var min = parseInt(scope.$eval(attrs.taMinText));
+			if (isNaN(min)){
+				throw('Min text must be an integer');
+			}
+			attrs.$observe('taMinText', function(value){
+				min = parseInt(value);
+				if (isNaN(min)){
+					throw('Min text must be an integer');
+				}
+				if (ctrl.$dirty){
+					ctrl.$validate();
+				}
+			});
+			ctrl.$validators.taMinText = function(viewValue){
+				var source = angular.element('<div/>');
+				source.html(viewValue);
+				return !source.text().length || source.text().length >= min;
+			};
+		}
+	};
+});
+angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'])
+.service('_taBlankTest', [function(){
+	var INLINETAGS_NONBLANK = /<(a|abbr|acronym|bdi|bdo|big|cite|code|del|dfn|img|ins|kbd|label|map|mark|q|ruby|rp|rt|s|samp|time|tt|var)[^>]*(>|$)/i;
+	return function(_defaultTest){
+		return function(_blankVal){
+			if(!_blankVal) return true;
+			// find first non-tag match - ie start of string or after tag that is not whitespace
+			var _firstMatch = /(^[^<]|>)[^<]/i.exec(_blankVal);
+			var _firstTagIndex;
+			if(!_firstMatch){
+				// find the end of the first tag removing all the
+				// Don't do a global replace as that would be waaayy too long, just replace the first 4 occurences should be enough
+				_blankVal = _blankVal.toString().replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '').replace(/="[^"]*"/i, '');
+				_firstTagIndex = _blankVal.indexOf('>');
+			}else{
+				_firstTagIndex = _firstMatch.index;
+			}
+			_blankVal = _blankVal.trim().substring(_firstTagIndex, _firstTagIndex + 100);
+			// check for no tags entry
+			if(/^[^<>]+$/i.test(_blankVal)) return false;
+			// this regex is to match any number of whitespace only between two tags
+			if (_blankVal.length === 0 || _blankVal === _defaultTest || /^>(\s|&nbsp;)*<\/[^>]+>$/ig.test(_blankVal)) return true;
+			// this regex tests if there is a tag followed by some optional whitespace and some text after that
+			else if (/>\s*[^\s<]/i.test(_blankVal) || INLINETAGS_NONBLANK.test(_blankVal)) return false;
+			else return true;
+		};
+	};
+}])
+.directive('taButton', [function(){
+	return {
+		link: function(scope, element, attrs){
+			element.attr('unselectable', 'on');
+			element.on('mousedown', function(e, eventData){
+				/* istanbul ignore else: this is for catching the jqLite testing*/
+				if(eventData) angular.extend(e, eventData);
+				// this prevents focusout from firing on the editor when clicking toolbar buttons
+				e.preventDefault();
+				return false;
+			});
+		}
+	};
+}])
+.directive('taBind', [
+		'taSanitize', '$timeout', '$document', 'taFixChrome', 'taBrowserTag',
+		'taSelection', 'taSelectableElements', 'taApplyCustomRenderers', 'taOptions',
+		'_taBlankTest', '$parse', 'taDOM', 'textAngularManager',
+		function(
+			taSanitize, $timeout, $document, taFixChrome, taBrowserTag,
+			taSelection, taSelectableElements, taApplyCustomRenderers, taOptions,
+			_taBlankTest, $parse, taDOM, textAngularManager){
+	// Uses for this are textarea or input with ng-model and ta-bind='text'
+	// OR any non-form element with contenteditable="contenteditable" ta-bind="html|text" ng-model
+	return {
+		priority: 2, // So we override validators correctly
+		require: ['ngModel','?ngModelOptions'],
+		link: function(scope, element, attrs, controller){
+			var ngModel = controller[0];
+			var ngModelOptions = controller[1] || {};
+			// the option to use taBind on an input or textarea is required as it will sanitize all input into it correctly.
+			var _isContentEditable = element.attr('contenteditable') !== undefined && element.attr('contenteditable');
+			var _isInputFriendly = _isContentEditable || element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input';
+			var _isReadonly = false;
+			var _focussed = false;
+			var _skipRender = false;
+			var _disableSanitizer = attrs.taUnsafeSanitizer || taOptions.disableSanitizer;
+			var _lastKey;
+			// see http://www.javascripter.net/faq/keycodes.htm for good information
+			// NOTE Mute On|Off 173 (Opera MSIE Safari Chrome) 181 (Firefox)
+			// BLOCKED_KEYS are special keys...
+			// Tab, pause/break, CapsLock, Esc, Page Up, End, Home,
+			// Left arrow, Up arrow, Right arrow, Down arrow, Insert, Delete,
+			// f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12
+			// NumLock, ScrollLock
+			var BLOCKED_KEYS = /^(9|19|20|27|33|34|35|36|37|38|39|40|45|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/i;
+			// UNDO_TRIGGER_KEYS - spaces, enter, delete, backspace, all punctuation
+			// Backspace, Enter, Space, Delete, (; :) (Firefox), (= +) (Firefox),
+			// Numpad +, Numpad -, (; :), (= +),
+			// (, <), (- _), (. >), (/ ?), (` ~), ([ {), (\ |), (] }), (' ")
+			// NOTE - Firefox: 173 = (- _) -- adding this to UNDO_TRIGGER_KEYS
+			var UNDO_TRIGGER_KEYS = /^(8|13|32|46|59|61|107|109|173|186|187|188|189|190|191|192|219|220|221|222)$/i;
+			var _pasteHandler;
+
+			// defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element
+			// non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct...
+			var _defaultVal, _defaultTest;
+
+			var _CTRL_KEY = 0x0001;
+			var _META_KEY = 0x0002;
+			var _ALT_KEY = 0x0004;
+			var _SHIFT_KEY = 0x0008;
+			// map events to special keys...
+			// mappings is an array of maps from events to specialKeys as declared in textAngularSetup
+			var _keyMappings = [
+				//		ctrl/command + z
+				{
+					specialKey: 'UndoKey',
+					forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
+					mustHaveModifiers: [_META_KEY + _CTRL_KEY],
+					keyCode: 90
+				},
+				//		ctrl/command + shift + z
+				{
+					specialKey: 'RedoKey',
+					forbiddenModifiers: _ALT_KEY,
+					mustHaveModifiers: [_META_KEY + _CTRL_KEY, _SHIFT_KEY],
+					keyCode: 90
+				},
+				//		ctrl/command + y
+				{
+					specialKey: 'RedoKey',
+					forbiddenModifiers: _ALT_KEY + _SHIFT_KEY,
+					mustHaveModifiers: [_META_KEY + _CTRL_KEY],
+					keyCode: 89
+				},
+				//		TabKey
+				{
+					specialKey: 'TabKey',
+					forbiddenModifiers: _META_KEY + _SHIFT_KEY + _ALT_KEY + _CTRL_KEY,
+					mustHaveModifiers: [],
+					keyCode: 9
+				},
+				//		shift + TabKey
+				{
+					specialKey: 'ShiftTabKey',
+					forbiddenModifiers: _META_KEY + _ALT_KEY + _CTRL_KEY,
+					mustHaveModifiers: [_SHIFT_KEY],
+					keyCode: 9
+				}
+			];
+			function _mapKeys(event) {
+				var specialKey;
+				_keyMappings.forEach(function (map){
+					if (map.keyCode === event.keyCode) {
+						var netModifiers = (event.metaKey ? _META_KEY: 0) +
+							(event.ctrlKey ? _CTRL_KEY: 0) +
+							(event.shiftKey ? _SHIFT_KEY: 0) +
+							(event.altKey ? _ALT_KEY: 0);
+						if (map.forbiddenModifiers & netModifiers) return;
+						if (map.mustHaveModifiers.every(function (modifier) { return netModifiers & modifier; })){
+							specialKey = map.specialKey;
+						}
+					}
+				});
+				return specialKey;
+			}
+
+			// set the default to be a paragraph value
+			if(attrs.taDefaultWrap === undefined) attrs.taDefaultWrap = 'p';
+			/* istanbul ignore next: ie specific test */
+			if(attrs.taDefaultWrap === ''){
+				_defaultVal = '';
+				_defaultTest = (_browserDetect.ie === undefined)? '<div><br></div>' : (_browserDetect.ie >= 11)? '<p><br></p>' : (_browserDetect.ie <= 8)? '<P>&nbsp;</P>' : '<p>&nbsp;</p>';
+			}else{
+				_defaultVal = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
+					'<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
+					(_browserDetect.ie <= 8)?
+						'<' + attrs.taDefaultWrap.toUpperCase() + '></' + attrs.taDefaultWrap.toUpperCase() + '>' :
+						'<' + attrs.taDefaultWrap + '></' + attrs.taDefaultWrap + '>';
+				_defaultTest = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
+					'<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
+					(_browserDetect.ie <= 8)?
+						'<' + attrs.taDefaultWrap.toUpperCase() + '>&nbsp;</' + attrs.taDefaultWrap.toUpperCase() + '>' :
+						'<' + attrs.taDefaultWrap + '>&nbsp;</' + attrs.taDefaultWrap + '>';
+			}
+
+			/* istanbul ignore else */
+			if(!ngModelOptions.$options) ngModelOptions.$options = {}; // ng-model-options support
+
+			var _blankTest = _taBlankTest(_defaultTest);
+
+			var _ensureContentWrapped = function(value) {
+				if (_blankTest(value)) return value;
+				var domTest = angular.element("<div>" + value + "</div>");
+				//console.log('domTest.children().length():', domTest.children().length);
+				if (domTest.children().length === 0) {
+					value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
+				} else {
+					var _children = domTest[0].childNodes;
+					var i;
+					var _foundBlockElement = false;
+					for (i = 0; i < _children.length; i++) {
+						if (_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
+					}
+					if (!_foundBlockElement) {
+						value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
+					}
+					else{
+						value = "";
+						for(i = 0; i < _children.length; i++){
+							var node = _children[i];
+							var nodeName = node.nodeName.toLowerCase();
+							//console.log(nodeName);
+							if(nodeName === '#comment') {
+								value += '<!--' + node.nodeValue + '-->';
+							} else if(nodeName === '#text') {
+								// determine if this is all whitespace, if so, we will leave it as it is.
+								// otherwise, we will wrap it as it is
+								var text = node.textContent;
+								if (!text.trim()) {
+									// just whitespace
+									value += text;
+								} else {
+									// not pure white space so wrap in <p>...</p> or whatever attrs.taDefaultWrap is set to.
+									value += "<" + attrs.taDefaultWrap + ">" + text + "</" + attrs.taDefaultWrap + ">";
+								}
+							} else if(!nodeName.match(BLOCKELEMENTS)){
+								/* istanbul ignore  next: Doesn't seem to trigger on tests */
+								var _subVal = (node.outerHTML || node.nodeValue);
+								/* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */
+								if(_subVal.trim() !== '')
+									value += "<" + attrs.taDefaultWrap + ">" + _subVal + "</" + attrs.taDefaultWrap + ">";
+								else value += _subVal;
+							} else {
+								value += node.outerHTML;
+							}
+						}
+					}
+				}
+				//console.log(value);
+				return value;
+			};
+
+			if(attrs.taPaste) _pasteHandler = $parse(attrs.taPaste);
+
+			element.addClass('ta-bind');
+
+			var _undoKeyupTimeout;
+
+			scope['$undoManager' + (attrs.id || '')] = ngModel.$undoManager = {
+				_stack: [],
+				_index: 0,
+				_max: 1000,
+				push: function(value){
+					if((typeof value === "undefined" || value === null) ||
+						((typeof this.current() !== "undefined" && this.current() !== null) && value === this.current())) return value;
+					if(this._index < this._stack.length - 1){
+						this._stack = this._stack.slice(0,this._index+1);
+					}
+					this._stack.push(value);
+					if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
+					if(this._stack.length > this._max) this._stack.shift();
+					this._index = this._stack.length - 1;
+					return value;
+				},
+				undo: function(){
+					return this.setToIndex(this._index-1);
+				},
+				redo: function(){
+					return this.setToIndex(this._index+1);
+				},
+				setToIndex: function(index){
+					if(index < 0 || index > this._stack.length - 1){
+						return undefined;
+					}
+					this._index = index;
+					return this.current();
+				},
+				current: function(){
+					return this._stack[this._index];
+				}
+			};
+
+			var _redoUndoTimeout;
+			var _undo = scope['$undoTaBind' + (attrs.id || '')] = function(){
+				/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
+				if(!_isReadonly && _isContentEditable){
+					var content = ngModel.$undoManager.undo();
+					if(typeof content !== "undefined" && content !== null){
+						_setInnerHTML(content);
+						_setViewValue(content, false);
+						if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
+						_redoUndoTimeout = $timeout(function(){
+							element[0].focus();
+							taSelection.setSelectionToElementEnd(element[0]);
+						}, 1);
+					}
+				}
+			};
+
+			var _redo = scope['$redoTaBind' + (attrs.id || '')] = function(){
+				/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
+				if(!_isReadonly && _isContentEditable){
+					var content = ngModel.$undoManager.redo();
+					if(typeof content !== "undefined" && content !== null){
+						_setInnerHTML(content);
+						_setViewValue(content, false);
+						/* istanbul ignore next */
+						if(_redoUndoTimeout) $timeout.cancel(_redoUndoTimeout);
+						_redoUndoTimeout = $timeout(function(){
+							element[0].focus();
+							taSelection.setSelectionToElementEnd(element[0]);
+						}, 1);
+					}
+				}
+			};
+
+			// in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code.
+			var _compileHtml = function(){
+				if(_isContentEditable) return element[0].innerHTML;
+				if(_isInputFriendly) return element.val();
+				throw ('textAngular Error: attempting to update non-editable taBind');
+			};
+
+			var _setViewValue = function(_val, triggerUndo, skipRender){
+				_skipRender = skipRender || false;
+				if(typeof triggerUndo === "undefined" || triggerUndo === null) triggerUndo = true && _isContentEditable; // if not contentEditable then the native undo/redo is fine
+				if(typeof _val === "undefined" || _val === null) _val = _compileHtml();
+				if(_blankTest(_val)){
+					// this avoids us from tripping the ng-pristine flag if we click in and out with out typing
+					if(ngModel.$viewValue !== '') ngModel.$setViewValue('');
+					if(triggerUndo && ngModel.$undoManager.current() !== '') ngModel.$undoManager.push('');
+				}else{
+					_reApplyOnSelectorHandlers();
+					if(ngModel.$viewValue !== _val){
+						ngModel.$setViewValue(_val);
+						if(triggerUndo) ngModel.$undoManager.push(_val);
+					}
+				}
+				ngModel.$render();
+			};
+
+			//used for updating when inserting wrapped elements
+			scope['updateTaBind' + (attrs.id || '')] = function(){
+				if(!_isReadonly) _setViewValue(undefined, undefined, true);
+			};
+
+			// catch DOM XSS via taSanitize
+			// Sanitizing both ways is identical
+			var _sanitize = function(unsafe){
+				return (ngModel.$oldViewValue = taSanitize(taFixChrome(unsafe), ngModel.$oldViewValue, _disableSanitizer));
+			};
+
+			// trigger the validation calls
+			if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) {
+				return !_blankTest(modelValue || viewValue);
+			};
+			// parsers trigger from the above keyup function or any other time that the viewValue is updated and parses it for storage in the ngModel
+			ngModel.$parsers.push(_sanitize);
+			ngModel.$parsers.unshift(_ensureContentWrapped);
+			// because textAngular is bi-directional (which is awesome) we need to also sanitize values going in from the server
+			ngModel.$formatters.push(_sanitize);
+			ngModel.$formatters.unshift(_ensureContentWrapped);
+			ngModel.$formatters.unshift(function(value){
+				return ngModel.$undoManager.push(value || '');
+			});
+
+			//this code is used to update the models when data is entered/deleted
+			if(_isInputFriendly){
+				scope.events = {};
+				if(!_isContentEditable){
+					// if a textarea or input just add in change and blur handlers, everything else is done by angulars input directive
+					element.on('change blur', scope.events.change = scope.events.blur = function(){
+						if(!_isReadonly) ngModel.$setViewValue(_compileHtml());
+					});
+
+					element.on('keydown', scope.events.keydown = function(event, eventData){
+						/* istanbul ignore else: this is for catching the jqLite testing*/
+						if(eventData) angular.extend(event, eventData);
+						// Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea
+						/* istanbul ignore else: otherwise normal functionality */
+						if(event.keyCode === 9){ // tab was pressed
+							// get caret position/selection
+							var start = this.selectionStart;
+							var end = this.selectionEnd;
+
+							var value = element.val();
+							if(event.shiftKey){
+								// find \t
+								var _linebreak = value.lastIndexOf('\n', start), _tab = value.lastIndexOf('\t', start);
+								if(_tab !== -1 && _tab >= _linebreak){
+									// set textarea value to: text before caret + tab + text after caret
+									element.val(value.substring(0, _tab) + value.substring(_tab + 1));
+
+									// put caret at right position again (add one for the tab)
+									this.selectionStart = this.selectionEnd = start - 1;
+								}
+							}else{
+								// set textarea value to: text before caret + tab + text after caret
+								element.val(value.substring(0, start) + "\t" + value.substring(end));
+
+								// put caret at right position again (add one for the tab)
+								this.selectionStart = this.selectionEnd = start + 1;
+							}
+							// prevent the focus lose
+							event.preventDefault();
+						}
+					});
+
+					var _repeat = function(string, n){
+						var result = '';
+						for(var _n = 0; _n < n; _n++) result += string;
+						return result;
+					};
+
+					// add a forEach function that will work on a NodeList, etc..
+					var forEach = function (array, callback, scope) {
+						for (var i= 0; i<array.length; i++) {
+							callback.call(scope, i, array[i]);
+						}
+					};
+
+					// handle <ul> or <ol> nodes
+					var recursiveListFormat = function(listNode, tablevel){
+						var _html = '';
+						var _subnodes = listNode.childNodes;
+						tablevel++;
+						// tab out and add the <ul> or <ol> html piece
+						_html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, 4);
+						forEach(_subnodes, function (index, node) {
+							/* istanbul ignore next: browser catch */
+							var nodeName = node.nodeName.toLowerCase();
+							if (nodeName === '#comment') {
+								_html += '<!--' + node.nodeValue + '-->';
+								return;
+							}
+							if (nodeName === '#text') {
+								_html += node.textContent;
+								return;
+							}
+							/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
+							if(!node.outerHTML) {
+								// no html to add
+								return;
+							}
+							if(nodeName === 'ul' || nodeName === 'ol') {
+								_html += '\n' + recursiveListFormat(node, tablevel);
+							}
+							else {
+								// no reformatting within this subnode, so just do the tabing...
+								_html += '\n' + _repeat('\t', tablevel) + node.outerHTML;
+							}
+						});
+						// now add on the </ol> or </ul> piece
+						_html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
+						return _html;
+					};
+					// handle formating of something like:
+					// <ol><!--First comment-->
+					//  <li>Test Line 1<!--comment test list 1--></li>
+					//    <ul><!--comment ul-->
+					//      <li>Nested Line 1</li>
+					//        <!--comment between nested lines--><li>Nested Line 2</li>
+					//    </ul>
+					//  <li>Test Line 3</li>
+					// </ol>
+					ngModel.$formatters.unshift(function(htmlValue){
+						// tabulate the HTML so it looks nicer
+						//
+						// first get a list of the nodes...
+						// we do this by using the element parser...
+						//
+						// doing this -- which is simpiler -- breaks our tests...
+						//var _nodes=angular.element(htmlValue);
+						var _nodes = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
+						if(_nodes.length > 0){
+							// do the reformatting of the layout...
+							htmlValue = '';
+							forEach(_nodes, function (index, node) {
+								var nodeName = node.nodeName.toLowerCase();
+								if (nodeName === '#comment') {
+									htmlValue += '<!--' + node.nodeValue + '-->';
+									return;
+								}
+								if (nodeName === '#text') {
+									htmlValue += node.textContent;
+									return;
+								}
+								/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
+								if(!node.outerHTML)
+								{
+									// nothing to format!
+									return;
+								}
+								if(htmlValue.length > 0) {
+									// we aready have some content, so drop to a new line
+									htmlValue += '\n';
+								}
+								if(nodeName === 'ul' || nodeName === 'ol') {
+									// okay a set of list stuff we want to reformat in a nested way
+									htmlValue += '' + recursiveListFormat(node, 0);
+								}
+								else {
+									// just use the original without any additional formating
+									htmlValue += '' + node.outerHTML;
+								}
+							});
+						}
+						return htmlValue;
+					});
+				}else{
+					// all the code specific to contenteditable divs
+					var _processingPaste = false;
+					/* istanbul ignore next: phantom js cannot test this for some reason */
+					var processpaste = function(text) {
+                        var _isOneNote = text.match(/content=["']*OneNote.File/i);
+						/* istanbul ignore else: don't care if nothing pasted */
+                        //console.log(text);
+						if(text && text.trim().length){
+							// test paste from word/microsoft product
+							if(text.match(/class=["']*Mso(Normal|List)/i) || text.match(/content=["']*Word.Document/i) || text.match(/content=["']*OneNote.File/i)){
+								var textFragment = text.match(/<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/i);
+								if(!textFragment) textFragment = text;
+								else textFragment = textFragment[1];
+								textFragment = textFragment.replace(/<o:p>[\s\S]*?<\/o:p>/ig, '').replace(/class=(["']|)MsoNormal(["']|)/ig, '');
+								var dom = angular.element("<div>" + textFragment + "</div>");
+								var targetDom = angular.element("<div></div>");
+								var _list = {
+									element: null,
+									lastIndent: [],
+									lastLi: null,
+									isUl: false
+								};
+								_list.lastIndent.peek = function(){
+									var n = this.length;
+									if (n>0) return this[n-1];
+								};
+								var _resetList = function(isUl){
+									_list.isUl = isUl;
+									_list.element = angular.element(isUl ? "<ul>" : "<ol>");
+									_list.lastIndent = [];
+									_list.lastIndent.peek = function(){
+										var n = this.length;
+										if (n>0) return this[n-1];
+									};
+									_list.lastLevelMatch = null;
+								};
+								for(var i = 0; i <= dom[0].childNodes.length; i++){
+									if(!dom[0].childNodes[i] || dom[0].childNodes[i].nodeName === "#text"){
+										continue;
+									} else {
+										var tagName = dom[0].childNodes[i].tagName.toLowerCase();
+										if(tagName !== "p" && tagName !== "h1" && tagName !== "h2" && tagName !== "h3" && tagName !== "h4" && tagName !== "h5" && tagName !== "h6"){
+											continue;
+										}
+									}
+									var el = angular.element(dom[0].childNodes[i]);
+									var _listMatch = (el.attr('class') || '').match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);
+
+									if(_listMatch){
+										if(el[0].childNodes.length < 2 || el[0].childNodes[1].childNodes.length < 1){
+											continue;
+										}
+										var isUl = _listMatch[1].toLowerCase() === "bullet" || (_listMatch[1].toLowerCase() !== "number" && !(/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].innerHTML) || /^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(el[0].childNodes[1].childNodes[0].innerHTML)));
+										var _indentMatch = (el.attr('style') || '').match(/margin-left:([\-\.0-9]*)/i);
+										var indent = parseFloat((_indentMatch)?_indentMatch[1]:0);
+										var _levelMatch = (el.attr('style') || '').match(/mso-list:l([0-9]+) level([0-9]+) lfo[0-9+]($|;)/i);
+										// prefers the mso-list syntax
+
+										if(_levelMatch && _levelMatch[2]) indent = parseInt(_levelMatch[2]);
+
+										if ((_levelMatch && (!_list.lastLevelMatch || _levelMatch[1] !== _list.lastLevelMatch[1])) || !_listMatch[3] || _listMatch[3].toLowerCase() === "first" || (_list.lastIndent.peek() === null) || (_list.isUl !== isUl && _list.lastIndent.peek() === indent)) {
+											_resetList(isUl);
+											targetDom.append(_list.element);
+										} else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() < indent){
+											_list.element = angular.element(isUl ? "<ul>" : "<ol>");
+											_list.lastLi.append(_list.element);
+										} else if (_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
+											while(_list.lastIndent.peek() != null && _list.lastIndent.peek() > indent){
+												if(_list.element.parent()[0].tagName.toLowerCase() === 'li'){
+													_list.element = _list.element.parent();
+													continue;
+												}else if(/[uo]l/i.test(_list.element.parent()[0].tagName.toLowerCase())){
+													_list.element = _list.element.parent();
+												}else{ // else it's it should be a sibling
+													break;
+												}
+												_list.lastIndent.pop();
+											}
+											_list.isUl = _list.element[0].tagName.toLowerCase() === "ul";
+											if (isUl !== _list.isUl) {
+												_resetList(isUl);
+												targetDom.append(_list.element);
+											}
+										}
+
+										_list.lastLevelMatch = _levelMatch;
+										if(indent !== _list.lastIndent.peek()) _list.lastIndent.push(indent);
+										_list.lastLi = angular.element("<li>");
+										_list.element.append(_list.lastLi);
+										_list.lastLi.html(el.html().replace(/<!(--|)\[if !supportLists\](--|)>[\s\S]*?<!(--|)\[endif\](--|)>/ig, ''));
+										el.remove();
+									}else{
+										_resetList(false);
+										targetDom.append(el);
+									}
+								}
+								var _unwrapElement = function(node){
+									node = angular.element(node);
+									for(var _n = node[0].childNodes.length - 1; _n >= 0; _n--) node.after(node[0].childNodes[_n]);
+									node.remove();
+								};
+
+								angular.forEach(targetDom.find('span'), function(node){
+									node.removeAttribute('lang');
+									if(node.attributes.length <= 0) _unwrapElement(node);
+								});
+								angular.forEach(targetDom.find('font'), _unwrapElement);
+
+                                text = targetDom.html();
+                                if(_isOneNote){
+                                    text = targetDom.html() || dom.html();
+                                }
+							}else{
+								// remove unnecessary chrome insert
+								text = text.replace(/<(|\/)meta[^>]*?>/ig, '');
+								if(text.match(/<[^>]*?(ta-bind)[^>]*?>/)){
+									// entire text-angular or ta-bind has been pasted, REMOVE AT ONCE!!
+									if(text.match(/<[^>]*?(text-angular)[^>]*?>/)){
+										var _el = angular.element("<div>" + text + "</div>");
+										_el.find('textarea').remove();
+										var binds = taDOM.getByAttribute(_el, 'ta-bind');
+										for(var _b = 0; _b < binds.length; _b++){
+											var _target = binds[_b][0].parentNode.parentNode;
+											for(var _c = 0; _c < binds[_b][0].childNodes.length; _c++){
+												_target.parentNode.insertBefore(binds[_b][0].childNodes[_c], _target);
+											}
+											_target.parentNode.removeChild(_target);
+										}
+										text = _el.html().replace('<br class="Apple-interchange-newline">', '');
+									}
+								}else if(text.match(/^<span/)){
+									// in case of pasting only a span - chrome paste, remove them. THis is just some wierd formatting
+									// if we remove the '<span class="Apple-converted-space"> </span>' here we destroy the spacing
+									// on paste from even ourselves!
+									if (!text.match(/<span class=(\"Apple-converted-space\"|\'Apple-converted-space\')>.<\/span>/ig)) {
+										text = text.replace(/<(|\/)span[^>]*?>/ig, '');
+									}
+								}
+								// Webkit on Apple tags
+								text = text.replace(/<br class="Apple-interchange-newline"[^>]*?>/ig, '').replace(/<span class="Apple-converted-space">( |&nbsp;)<\/span>/ig, '&nbsp;');
+							}
+
+							if (/<li(\s.*)?>/i.test(text) && /(<ul(\s.*)?>|<ol(\s.*)?>).*<li(\s.*)?>/i.test(text) === false) {
+								// insert missing parent of li element
+								text = text.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i, '<ul>$&</ul>');
+							}
+
+							// parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste
+							text = text.replace(/^[ |\u00A0]+/gm, function (match) {
+								var result = '';
+								for (var i = 0; i < match.length; i++) {
+									result += '&nbsp;';
+								}
+								return result;
+							}).replace(/\n|\r\n|\r/g, '<br />').replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
+
+							if(_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text;
+
+							text = taSanitize(text, '', _disableSanitizer);
+
+							taSelection.insertHtml(text, element[0]);
+							$timeout(function(){
+								ngModel.$setViewValue(_compileHtml());
+								_processingPaste = false;
+								element.removeClass('processing-paste');
+							}, 0);
+						}else{
+							_processingPaste = false;
+							element.removeClass('processing-paste');
+						}
+					};
+
+					element.on('paste', scope.events.paste = function(e, eventData){
+						/* istanbul ignore else: this is for catching the jqLite testing*/
+						if(eventData) angular.extend(e, eventData);
+						if(_isReadonly || _processingPaste){
+							e.stopPropagation();
+							e.preventDefault();
+							return false;
+						}
+
+						// Code adapted from http://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser/6804718#6804718
+						_processingPaste = true;
+						element.addClass('processing-paste');
+						var pastedContent;
+						var clipboardData = (e.originalEvent || e).clipboardData;
+						if (clipboardData && clipboardData.getData && clipboardData.types.length > 0) {// Webkit - get data from clipboard, put into editdiv, cleanup, then cancel event
+							var _types = "";
+							for(var _t = 0; _t < clipboardData.types.length; _t++){
+								_types += " " + clipboardData.types[_t];
+							}
+							/* istanbul ignore next: browser tests */
+							if (/text\/html/i.test(_types)) {
+								pastedContent = clipboardData.getData('text/html');
+							} else if (/text\/plain/i.test(_types)) {
+								pastedContent = clipboardData.getData('text/plain');
+							}
+
+							processpaste(pastedContent);
+							e.stopPropagation();
+							e.preventDefault();
+							return false;
+						} else {// Everything else - empty editdiv and allow browser to paste content into it, then cleanup
+							var _savedSelection = rangy.saveSelection(),
+								_tempDiv = angular.element('<div class="ta-hidden-input" contenteditable="true"></div>');
+							$document.find('body').append(_tempDiv);
+							_tempDiv[0].focus();
+							$timeout(function(){
+								// restore selection
+								rangy.restoreSelection(_savedSelection);
+								processpaste(_tempDiv[0].innerHTML);
+								element[0].focus();
+								_tempDiv.remove();
+							}, 0);
+						}
+					});
+					element.on('cut', scope.events.cut = function(e){
+						// timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display
+						if(!_isReadonly) $timeout(function(){
+							ngModel.$setViewValue(_compileHtml());
+						}, 0);
+						else e.preventDefault();
+					});
+
+					element.on('keydown', scope.events.keydown = function(event, eventData){
+						/* istanbul ignore else: this is for catching the jqLite testing*/
+						if(eventData) angular.extend(event, eventData);
+						event.specialKey = _mapKeys(event);
+						var userSpecialKey;
+						/* istanbul ignore next: difficult to test */
+						taOptions.keyMappings.forEach(function (mapping) {
+							if (event.specialKey === mapping.commandKeyCode) {
+								// taOptions has remapped this binding... so
+								// we disable our own
+								event.specialKey = undefined;
+							}
+							if (mapping.testForKey(event)) {
+								userSpecialKey = mapping.commandKeyCode;
+							}
+							if ((mapping.commandKeyCode === 'UndoKey') || (mapping.commandKeyCode === 'RedoKey')) {
+								// this is necessary to fully stop the propagation.
+								if (!mapping.enablePropagation) {
+									event.preventDefault();
+								}
+							}
+						});
+						/* istanbul ignore next: difficult to test */
+						if (typeof userSpecialKey !== 'undefined') {
+							event.specialKey = userSpecialKey;
+						}
+						/* istanbul ignore next: difficult to test as can't seem to select */
+						if ((typeof event.specialKey !== 'undefined') && (
+								event.specialKey !== 'UndoKey' || event.specialKey !== 'RedoKey'
+							)) {
+							event.preventDefault();
+							textAngularManager.sendKeyCommand(scope, event);
+						}
+						/* istanbul ignore else: readonly check */
+						if(!_isReadonly){
+							if (event.specialKey==='UndoKey') {
+								_undo();
+								event.preventDefault();
+							}
+							if (event.specialKey==='RedoKey') {
+								_redo();
+								event.preventDefault();
+							}
+							/* istanbul ignore next: difficult to test as can't seem to select */
+							if(event.keyCode === 13 && !event.shiftKey){
+								var contains = function(a, obj) {
+									for (var i = 0; i < a.length; i++) {
+										if (a[i] === obj) {
+											return true;
+										}
+									}
+									return false;
+								};
+								var $selection;
+								var selection = taSelection.getSelectionElement();
+								if(!selection.tagName.match(VALIDELEMENTS)) return;
+								var _new = angular.element(_defaultVal);
+								// if we are in the last element of a blockquote, or ul or ol and the element is blank
+								// we need to pull the element outside of the said type
+								var moveOutsideElements = ['blockquote', 'ul', 'ol'];
+								if (contains(moveOutsideElements, selection.parentNode.tagName.toLowerCase())) {
+									if (/^<br(|\/)>$/i.test(selection.innerHTML.trim()) && !selection.nextSibling) {
+										// if last element is blank, pull element outside.
+										$selection = angular.element(selection);
+										var _parent = $selection.parent();
+										_parent.after(_new);
+										$selection.remove();
+										if (_parent.children().length === 0) _parent.remove();
+										taSelection.setSelectionToElementStart(_new[0]);
+										event.preventDefault();
+									}
+									if (/^<[^>]+><br(|\/)><\/[^>]+>$/i.test(selection.innerHTML.trim())) {
+										$selection = angular.element(selection);
+										$selection.after(_new);
+										$selection.remove();
+										taSelection.setSelectionToElementStart(_new[0]);
+										event.preventDefault();
+									}
+								}
+							}
+						}
+					});
+					var _keyupTimeout;
+					element.on('keyup', scope.events.keyup = function(event, eventData){
+						/* istanbul ignore else: this is for catching the jqLite testing*/
+						if(eventData) angular.extend(event, eventData);
+						/* istanbul ignore next: FF specific bug fix */
+						if (event.keyCode === 9) {
+							var _selection = taSelection.getSelection();
+							if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
+							return;
+						}
+						if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
+						if(!_isReadonly && !BLOCKED_KEYS.test(event.keyCode)){
+							// if enter - insert new taDefaultWrap, if shift+enter insert <br/>
+							if(_defaultVal !== '' && event.keyCode === 13){
+								if(!event.shiftKey){
+									// new paragraph, br should be caught correctly
+									var selection = taSelection.getSelectionElement();
+									while(!selection.tagName.match(VALIDELEMENTS) && selection !== element[0]){
+										selection = selection.parentNode;
+									}
+
+									if(selection.tagName.toLowerCase() !== attrs.taDefaultWrap && selection.tagName.toLowerCase() !== 'li' && (selection.innerHTML.trim() === '' || selection.innerHTML.trim() === '<br>')){
+										var _new = angular.element(_defaultVal);
+										angular.element(selection).replaceWith(_new);
+										taSelection.setSelectionToElementStart(_new[0]);
+									}
+								}
+							}
+							var val = _compileHtml();
+							if(_defaultVal !== '' && val.trim() === ''){
+								_setInnerHTML(_defaultVal);
+								taSelection.setSelectionToElementStart(element.children()[0]);
+							}else if(val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== ''){
+								/* we no longer do this, since there can be comments here and white space
+								var _savedSelection = rangy.saveSelection();
+								val = _compileHtml();
+								val = "<" + attrs.taDefaultWrap + ">" + val + "</" + attrs.taDefaultWrap + ">";
+								_setInnerHTML(val);
+								rangy.restoreSelection(_savedSelection);
+								*/
+							}
+							var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
+							if(_keyupTimeout) $timeout.cancel(_keyupTimeout);
+							_keyupTimeout = $timeout(function() {
+								_setViewValue(val, triggerUndo, true);
+							}, ngModelOptions.$options.debounce || 400);
+							if(!triggerUndo) _undoKeyupTimeout = $timeout(function(){ ngModel.$undoManager.push(val); }, 250);
+							_lastKey = event.keyCode;
+						}
+					});
+
+					element.on('blur', scope.events.blur = function(){
+						_focussed = false;
+						/* istanbul ignore else: if readonly don't update model */
+						if(!_isReadonly){
+							_setViewValue(undefined, undefined, true);
+						}else{
+							_skipRender = true; // don't redo the whole thing, just check the placeholder logic
+							ngModel.$render();
+						}
+					});
+
+					// Placeholders not supported on ie 8 and below
+					if(attrs.placeholder && (_browserDetect.ie > 8 || _browserDetect.ie === undefined)){
+						var rule;
+						if(attrs.id) rule = addCSSRule('#' + attrs.id + '.placeholder-text:before', 'content: "' + attrs.placeholder + '"');
+						else throw('textAngular Error: An unique ID is required for placeholders to work');
+
+						scope.$on('$destroy', function(){
+							removeCSSRule(rule);
+						});
+					}
+
+					element.on('focus', scope.events.focus = function(){
+						_focussed = true;
+						element.removeClass('placeholder-text');
+						_reApplyOnSelectorHandlers();
+					});
+
+					element.on('mouseup', scope.events.mouseup = function(){
+						var _selection = taSelection.getSelection();
+						if(_selection.start.element === element[0] && element.children().length) taSelection.setSelectionToElementStart(element.children()[0]);
+					});
+
+					// prevent propagation on mousedown in editor, see #206
+					element.on('mousedown', scope.events.mousedown = function(event, eventData){
+						/* istanbul ignore else: this is for catching the jqLite testing*/
+						if(eventData) angular.extend(event, eventData);
+						event.stopPropagation();
+					});
+				}
+			}
+
+			var selectorClickHandler = function(event){
+				// emit the element-select event, pass the element
+				scope.$emit('ta-element-select', this);
+				event.preventDefault();
+				return false;
+			};
+			var fileDropHandler = function(event, eventData){
+				/* istanbul ignore else: this is for catching the jqLite testing*/
+				if(eventData) angular.extend(event, eventData);
+				// emit the drop event, pass the element, preventing should be done elsewhere
+				if(!dropFired && !_isReadonly){
+					dropFired = true;
+					var dataTransfer;
+					if(event.originalEvent) dataTransfer = event.originalEvent.dataTransfer;
+					else dataTransfer = event.dataTransfer;
+					scope.$emit('ta-drop-event', this, event, dataTransfer);
+					$timeout(function(){
+						dropFired = false;
+						_setViewValue(undefined, undefined, true);
+					}, 100);
+				}
+			};
+
+			//used for updating when inserting wrapped elements
+			var _reApplyOnSelectorHandlers = scope['reApplyOnSelectorHandlers' + (attrs.id || '')] = function(){
+				/* istanbul ignore else */
+				if(!_isReadonly) angular.forEach(taSelectableElements, function(selector){
+						// check we don't apply the handler twice
+						element.find(selector)
+							.off('click', selectorClickHandler)
+							.on('click', selectorClickHandler);
+					});
+			};
+
+			var _setInnerHTML = function(newval){
+				element[0].innerHTML = newval;
+			};
+			var _renderTimeout;
+			var _renderInProgress = false;
+			// changes to the model variable from outside the html/text inputs
+			ngModel.$render = function(){
+				/* istanbul ignore if: Catches rogue renders, hard to replicate in tests */
+				if(_renderInProgress) return;
+				else _renderInProgress = true;
+				// catch model being null or undefined
+				var val = ngModel.$viewValue || '';
+				// if the editor isn't focused it needs to be updated, otherwise it's receiving user input
+				if(!_skipRender){
+					/* istanbul ignore else: in other cases we don't care */
+					if(_isContentEditable && _focussed){
+						// update while focussed
+						element.removeClass('placeholder-text');
+						if(_renderTimeout) $timeout.cancel(_renderTimeout);
+						_renderTimeout = $timeout(function(){
+							/* istanbul ignore if: Can't be bothered testing this... */
+							if(!_focussed){
+								element[0].focus();
+								taSelection.setSelectionToElementEnd(element.children()[element.children().length - 1]);
+							}
+							_renderTimeout = undefined;
+						}, 1);
+					}
+					if(_isContentEditable){
+						// WYSIWYG Mode
+						if(attrs.placeholder){
+							if(val === ''){
+								// blank
+								_setInnerHTML(_defaultVal);
+							}else{
+								// not-blank
+								_setInnerHTML(val);
+							}
+						}else{
+							_setInnerHTML((val === '') ? _defaultVal : val);
+						}
+						// if in WYSIWYG and readOnly we kill the use of links by clicking
+						if(!_isReadonly){
+							_reApplyOnSelectorHandlers();
+							element.on('drop', fileDropHandler);
+						}else{
+							element.off('drop', fileDropHandler);
+						}
+					}else if(element[0].tagName.toLowerCase() !== 'textarea' && element[0].tagName.toLowerCase() !== 'input'){
+						// make sure the end user can SEE the html code as a display. This is a read-only display element
+						_setInnerHTML(taApplyCustomRenderers(val));
+					}else{
+						// only for input and textarea inputs
+						element.val(val);
+					}
+				}
+				if(_isContentEditable && attrs.placeholder){
+					if(val === ''){
+						if(_focussed) element.removeClass('placeholder-text');
+						else element.addClass('placeholder-text');
+					}else{
+						element.removeClass('placeholder-text');
+					}
+				}
+				_renderInProgress = _skipRender = false;
+			};
+
+			if(attrs.taReadonly){
+				//set initial value
+				_isReadonly = scope.$eval(attrs.taReadonly);
+				if(_isReadonly){
+					element.addClass('ta-readonly');
+					// we changed to readOnly mode (taReadonly='true')
+					if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
+						element.attr('disabled', 'disabled');
+					}
+					if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
+						element.removeAttr('contenteditable');
+					}
+				}else{
+					element.removeClass('ta-readonly');
+					// we changed to NOT readOnly mode (taReadonly='false')
+					if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
+						element.removeAttr('disabled');
+					}else if(_isContentEditable){
+						element.attr('contenteditable', 'true');
+					}
+				}
+				// taReadonly only has an effect if the taBind element is an input or textarea or has contenteditable='true' on it.
+				// Otherwise it is readonly by default
+				scope.$watch(attrs.taReadonly, function(newVal, oldVal){
+					if(oldVal === newVal) return;
+					if(newVal){
+						element.addClass('ta-readonly');
+						// we changed to readOnly mode (taReadonly='true')
+						if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
+							element.attr('disabled', 'disabled');
+						}
+						if(element.attr('contenteditable') !== undefined && element.attr('contenteditable')){
+							element.removeAttr('contenteditable');
+						}
+						// turn ON selector click handlers
+						angular.forEach(taSelectableElements, function(selector){
+							element.find(selector).on('click', selectorClickHandler);
+						});
+						element.off('drop', fileDropHandler);
+					}else{
+						element.removeClass('ta-readonly');
+						// we changed to NOT readOnly mode (taReadonly='false')
+						if(element[0].tagName.toLowerCase() === 'textarea' || element[0].tagName.toLowerCase() === 'input'){
+							element.removeAttr('disabled');
+						}else if(_isContentEditable){
+							element.attr('contenteditable', 'true');
+						}
+						// remove the selector click handlers
+						angular.forEach(taSelectableElements, function(selector){
+							element.find(selector).off('click', selectorClickHandler);
+						});
+						element.on('drop', fileDropHandler);
+					}
+					_isReadonly = newVal;
+				});
+			}
+
+			// Initialise the selectableElements
+			// if in WYSIWYG and readOnly we kill the use of links by clicking
+			if(_isContentEditable && !_isReadonly){
+				angular.forEach(taSelectableElements, function(selector){
+					element.find(selector).on('click', selectorClickHandler);
+				});
+				element.on('drop', fileDropHandler);
+				element.on('blur', function(){
+					/* istanbul ignore next: webkit fix */
+					if(_browserDetect.webkit) { // detect webkit
+						globalContentEditableBlur = true;
+					}
+				});
+			}
+		}
+	};
+}]);
+
+// this global var is used to prevent multiple fires of the drop event. Needs to be global to the textAngular file.
+var dropFired = false;
+var textAngular = angular.module("textAngular", ['ngSanitize', 'textAngularSetup', 'textAngular.factories', 'textAngular.DOM', 'textAngular.validators', 'textAngular.taBind']); //This makes ngSanitize required
+
+textAngular.config([function(){
+	// clear taTools variable. Just catches testing and any other time that this config may run multiple times...
+	angular.forEach(taTools, function(value, key){ delete taTools[key];	});
+}]);
+
+textAngular.directive("textAngular", [
+	'$compile', '$timeout', 'taOptions', 'taSelection', 'taExecCommand',
+	'textAngularManager', '$document', '$animate', '$log', '$q', '$parse',
+	function($compile, $timeout, taOptions, taSelection, taExecCommand,
+		textAngularManager, $document, $animate, $log, $q, $parse){
+		return {
+			require: '?ngModel',
+			scope: {},
+			restrict: "EA",
+			priority: 2, // So we override validators correctly
+			link: function(scope, element, attrs, ngModel){
+				// all these vars should not be accessable outside this directive
+				var _keydown, _keyup, _keypress, _mouseup, _focusin, _focusout,
+					_originalContents, _toolbars,
+					_serial = (attrs.serial) ? attrs.serial : Math.floor(Math.random() * 10000000000000000),
+					_taExecCommand, _resizeMouseDown, _updateSelectedStylesTimeout;
+
+				scope._name = (attrs.name) ? attrs.name : 'textAngularEditor' + _serial;
+
+				var oneEvent = function(_element, event, action){
+					$timeout(function(){
+						// shim the .one till fixed
+						var _func = function(){
+							_element.off(event, _func);
+							action.apply(this, arguments);
+						};
+						_element.on(event, _func);
+					}, 100);
+				};
+				_taExecCommand = taExecCommand(attrs.taDefaultWrap);
+				// get the settings from the defaults and add our specific functions that need to be on the scope
+				angular.extend(scope, angular.copy(taOptions), {
+					// wraps the selection in the provided tag / execCommand function. Should only be called in WYSIWYG mode.
+					wrapSelection: function(command, opt, isSelectableElementTool){
+						if(command.toLowerCase() === "undo"){
+							scope['$undoTaBindtaTextElement' + _serial]();
+						}else if(command.toLowerCase() === "redo"){
+							scope['$redoTaBindtaTextElement' + _serial]();
+						}else{
+							// catch errors like FF erroring when you try to force an undo with nothing done
+							_taExecCommand(command, false, opt, scope.defaultTagAttributes);
+							if(isSelectableElementTool){
+								// re-apply the selectable tool events
+								scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
+							}
+							// refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box.
+							// You still have focus on the text/html input it just doesn't show up
+							scope.displayElements.text[0].focus();
+						}
+					},
+					showHtml: scope.$eval(attrs.taShowHtml) || false
+				});
+				// setup the options from the optional attributes
+				if(attrs.taFocussedClass)			scope.classes.focussed = attrs.taFocussedClass;
+				if(attrs.taTextEditorClass)			scope.classes.textEditor = attrs.taTextEditorClass;
+				if(attrs.taHtmlEditorClass)			scope.classes.htmlEditor = attrs.taHtmlEditorClass;
+				if(attrs.taDefaultTagAttributes){
+					try	{
+						//	TODO: This should use angular.merge to enhance functionality once angular 1.4 is required
+						angular.extend(scope.defaultTagAttributes, angular.fromJson(attrs.taDefaultTagAttributes));
+					} catch (error) {
+						$log.error(error);
+					}
+				}
+				// optional setup functions
+				if(attrs.taTextEditorSetup)			scope.setup.textEditorSetup = scope.$parent.$eval(attrs.taTextEditorSetup);
+				if(attrs.taHtmlEditorSetup)			scope.setup.htmlEditorSetup = scope.$parent.$eval(attrs.taHtmlEditorSetup);
+				// optional fileDropHandler function
+				if(attrs.taFileDrop)				scope.fileDropHandler = scope.$parent.$eval(attrs.taFileDrop);
+				else								scope.fileDropHandler = scope.defaultFileDropHandler;
+
+				_originalContents = element[0].innerHTML;
+				// clear the original content
+				element[0].innerHTML = '';
+
+				// Setup the HTML elements as variable references for use later
+				scope.displayElements = {
+					// we still need the hidden input even with a textarea as the textarea may have invalid/old input in it,
+					// wheras the input will ALLWAYS have the correct value.
+					forminput: angular.element("<input type='hidden' tabindex='-1' style='display: none;'>"),
+					html: angular.element("<textarea></textarea>"),
+					text: angular.element("<div></div>"),
+					// other toolbased elements
+					scrollWindow: angular.element("<div class='ta-scroll-window'></div>"),
+					popover: angular.element('<div class="popover fade bottom" style="max-width: none; width: 305px;"></div>'),
+					popoverArrow: angular.element('<div class="arrow"></div>'),
+					popoverContainer: angular.element('<div class="popover-content"></div>'),
+					resize: {
+						overlay: angular.element('<div class="ta-resizer-handle-overlay"></div>'),
+						background: angular.element('<div class="ta-resizer-handle-background"></div>'),
+						anchors: [
+							angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tl"></div>'),
+							angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tr"></div>'),
+							angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-bl"></div>'),
+							angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-br"></div>')
+						],
+						info: angular.element('<div class="ta-resizer-handle-info"></div>')
+					}
+				};
+
+				// Setup the popover
+				scope.displayElements.popover.append(scope.displayElements.popoverArrow);
+				scope.displayElements.popover.append(scope.displayElements.popoverContainer);
+				scope.displayElements.scrollWindow.append(scope.displayElements.popover);
+
+				scope.displayElements.popover.on('mousedown', function(e, eventData){
+					/* istanbul ignore else: this is for catching the jqLite testing*/
+					if(eventData) angular.extend(e, eventData);
+					// this prevents focusout from firing on the editor when clicking anything in the popover
+					e.preventDefault();
+					return false;
+				});
+
+				// define the popover show and hide functions
+				scope.showPopover = function(_el){
+					scope.displayElements.popover.css('display', 'block');
+					scope.reflowPopover(_el);
+					$animate.addClass(scope.displayElements.popover, 'in');
+					oneEvent($document.find('body'), 'click keyup', function(){scope.hidePopover();});
+				};
+				scope.reflowPopover = function(_el){
+					/* istanbul ignore if: catches only if near bottom of editor */
+					if(scope.displayElements.text[0].offsetHeight - 51 > _el[0].offsetTop){
+						scope.displayElements.popover.css('top', _el[0].offsetTop + _el[0].offsetHeight + scope.displayElements.scrollWindow[0].scrollTop + 'px');
+						scope.displayElements.popover.removeClass('top').addClass('bottom');
+					}else{
+						scope.displayElements.popover.css('top', _el[0].offsetTop - 54 + scope.displayElements.scrollWindow[0].scrollTop + 'px');
+						scope.displayElements.popover.removeClass('bottom').addClass('top');
+					}
+					var _maxLeft = scope.displayElements.text[0].offsetWidth - scope.displayElements.popover[0].offsetWidth;
+					var _targetLeft = _el[0].offsetLeft + (_el[0].offsetWidth / 2.0) - (scope.displayElements.popover[0].offsetWidth / 2.0);
+					scope.displayElements.popover.css('left', Math.max(0, Math.min(_maxLeft, _targetLeft)) + 'px');
+					scope.displayElements.popoverArrow.css('margin-left', (Math.min(_targetLeft, (Math.max(0, _targetLeft - _maxLeft))) - 11) + 'px');
+				};
+				scope.hidePopover = function(){
+					scope.displayElements.popover.css('display', '');
+					scope.displayElements.popoverContainer.attr('style', '');
+					scope.displayElements.popoverContainer.attr('class', 'popover-content');
+					scope.displayElements.popover.removeClass('in');
+				};
+
+				// setup the resize overlay
+				scope.displayElements.resize.overlay.append(scope.displayElements.resize.background);
+				angular.forEach(scope.displayElements.resize.anchors, function(anchor){ scope.displayElements.resize.overlay.append(anchor);});
+				scope.displayElements.resize.overlay.append(scope.displayElements.resize.info);
+				scope.displayElements.scrollWindow.append(scope.displayElements.resize.overlay);
+
+				// define the show and hide events
+				scope.reflowResizeOverlay = function(_el){
+					_el = angular.element(_el)[0];
+					scope.displayElements.resize.overlay.css({
+						'display': 'block',
+						'left': _el.offsetLeft - 5 + 'px',
+						'top': _el.offsetTop - 5 + 'px',
+						'width': _el.offsetWidth + 10 + 'px',
+						'height': _el.offsetHeight + 10 + 'px'
+					});
+					scope.displayElements.resize.info.text(_el.offsetWidth + ' x ' + _el.offsetHeight);
+				};
+				/* istanbul ignore next: pretty sure phantomjs won't test this */
+				scope.showResizeOverlay = function(_el){
+					var _body = $document.find('body');
+					_resizeMouseDown = function(event){
+						var startPosition = {
+							width: parseInt(_el.attr('width')),
+							height: parseInt(_el.attr('height')),
+							x: event.clientX,
+							y: event.clientY
+						};
+						if(startPosition.width === undefined || isNaN(startPosition.width)) startPosition.width = _el[0].offsetWidth;
+						if(startPosition.height === undefined || isNaN(startPosition.height)) startPosition.height = _el[0].offsetHeight;
+						scope.hidePopover();
+						var ratio = startPosition.height / startPosition.width;
+						var mousemove = function(event){
+							// calculate new size
+							var pos = {
+								x: Math.max(0, startPosition.width + (event.clientX - startPosition.x)),
+								y: Math.max(0, startPosition.height + (event.clientY - startPosition.y))
+							};
+
+							// DEFAULT: the aspect ratio is not locked unless the Shift key is pressed.
+							//
+							// attribute: ta-resize-force-aspect-ratio -- locks resize into maintaing the aspect ratio
+							var bForceAspectRatio = (attrs.taResizeForceAspectRatio !== undefined);
+							// attribute: ta-resize-maintain-aspect-ratio=true causes the space ratio to remain locked
+							// unless the Shift key is pressed
+							var bFlipKeyBinding = attrs.taResizeMaintainAspectRatio;
+							var bKeepRatio =  bForceAspectRatio || (bFlipKeyBinding && !event.shiftKey);
+							if(bKeepRatio) {
+								var newRatio = pos.y / pos.x;
+								pos.x = ratio > newRatio ? pos.x : pos.y / ratio;
+								pos.y = ratio > newRatio ? pos.x * ratio : pos.y;
+							}
+							var el = angular.element(_el);
+							function roundedMaxVal(val) {
+								return Math.round(Math.max(0, val));
+							}
+							el.css('height', roundedMaxVal(pos.y) + 'px');
+							el.css('width', roundedMaxVal(pos.x) + 'px');
+
+							// reflow the popover tooltip
+							scope.reflowResizeOverlay(_el);
+						};
+						_body.on('mousemove', mousemove);
+						oneEvent(_body, 'mouseup', function(event){
+							event.preventDefault();
+							event.stopPropagation();
+							_body.off('mousemove', mousemove);
+							// at this point, we need to force the model to update! since the css has changed!
+							// this fixes bug: #862 - we now hide the popover -- as this seems more consitent.
+							// there are still issues under firefox, the window does not repaint. -- not sure
+							// how best to resolve this, but clicking anywhere works.
+							scope.$apply(function (){
+								scope.hidePopover();
+								scope.updateTaBindtaTextElement();
+							}, 100);
+						});
+						event.stopPropagation();
+						event.preventDefault();
+					};
+
+					scope.displayElements.resize.anchors[3].off('mousedown');
+					scope.displayElements.resize.anchors[3].on('mousedown', _resizeMouseDown);
+
+					scope.reflowResizeOverlay(_el);
+					oneEvent(_body, 'click', function(){scope.hideResizeOverlay();});
+				};
+				/* istanbul ignore next: pretty sure phantomjs won't test this */
+				scope.hideResizeOverlay = function(){
+					scope.displayElements.resize.anchors[3].off('mousedown', _resizeMouseDown);
+					scope.displayElements.resize.overlay.css('display', '');
+				};
+
+				// allow for insertion of custom directives on the textarea and div
+				scope.setup.htmlEditorSetup(scope.displayElements.html);
+				scope.setup.textEditorSetup(scope.displayElements.text);
+				scope.displayElements.html.attr({
+					'id': 'taHtmlElement' + _serial,
+					'ng-show': 'showHtml',
+					'ta-bind': 'ta-bind',
+					'ng-model': 'html',
+					'ng-model-options': element.attr('ng-model-options')
+				});
+				scope.displayElements.text.attr({
+					'id': 'taTextElement' + _serial,
+					'contentEditable': 'true',
+					'ta-bind': 'ta-bind',
+					'ng-model': 'html',
+					'ng-model-options': element.attr('ng-model-options')
+				});
+				scope.displayElements.scrollWindow.attr({'ng-hide': 'showHtml'});
+				if(attrs.taDefaultWrap) scope.displayElements.text.attr('ta-default-wrap', attrs.taDefaultWrap);
+
+				if(attrs.taUnsafeSanitizer){
+					scope.displayElements.text.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
+					scope.displayElements.html.attr('ta-unsafe-sanitizer', attrs.taUnsafeSanitizer);
+				}
+
+				// add the main elements to the origional element
+				scope.displayElements.scrollWindow.append(scope.displayElements.text);
+				element.append(scope.displayElements.scrollWindow);
+				element.append(scope.displayElements.html);
+
+				scope.displayElements.forminput.attr('name', scope._name);
+				element.append(scope.displayElements.forminput);
+
+				if(attrs.tabindex){
+					element.removeAttr('tabindex');
+					scope.displayElements.text.attr('tabindex', attrs.tabindex);
+					scope.displayElements.html.attr('tabindex', attrs.tabindex);
+				}
+
+				if (attrs.placeholder) {
+					scope.displayElements.text.attr('placeholder', attrs.placeholder);
+					scope.displayElements.html.attr('placeholder', attrs.placeholder);
+				}
+
+				if(attrs.taDisabled){
+					scope.displayElements.text.attr('ta-readonly', 'disabled');
+					scope.displayElements.html.attr('ta-readonly', 'disabled');
+					scope.disabled = scope.$parent.$eval(attrs.taDisabled);
+					scope.$parent.$watch(attrs.taDisabled, function(newVal){
+						scope.disabled = newVal;
+						if(scope.disabled){
+							element.addClass(scope.classes.disabled);
+						}else{
+							element.removeClass(scope.classes.disabled);
+						}
+					});
+				}
+
+				if(attrs.taPaste){
+					scope._pasteHandler = function(_html){
+						return $parse(attrs.taPaste)(scope.$parent, {$html: _html});
+					};
+					scope.displayElements.text.attr('ta-paste', '_pasteHandler($html)');
+				}
+
+				// compile the scope with the text and html elements only - if we do this with the main element it causes a compile loop
+				$compile(scope.displayElements.scrollWindow)(scope);
+				$compile(scope.displayElements.html)(scope);
+
+				scope.updateTaBindtaTextElement = scope['updateTaBindtaTextElement' + _serial];
+				scope.updateTaBindtaHtmlElement = scope['updateTaBindtaHtmlElement' + _serial];
+
+				// add the classes manually last
+				element.addClass("ta-root");
+				scope.displayElements.scrollWindow.addClass("ta-text ta-editor " + scope.classes.textEditor);
+				scope.displayElements.html.addClass("ta-html ta-editor " + scope.classes.htmlEditor);
+
+				// used in the toolbar actions
+				scope._actionRunning = false;
+				var _savedSelection = false;
+				scope.startAction = function(){
+					scope._actionRunning = true;
+					// if rangy library is loaded return a function to reload the current selection
+					_savedSelection = rangy.saveSelection();
+					return function(){
+						if(_savedSelection) rangy.restoreSelection(_savedSelection);
+					};
+				};
+				scope.endAction = function(){
+					scope._actionRunning = false;
+					if(_savedSelection){
+						if(scope.showHtml){
+							scope.displayElements.html[0].focus();
+						}else{
+							scope.displayElements.text[0].focus();
+						}
+						// rangy.restoreSelection(_savedSelection);
+						rangy.removeMarkers(_savedSelection);
+					}
+					_savedSelection = false;
+					scope.updateSelectedStyles();
+					// only update if in text or WYSIWYG mode
+					if(!scope.showHtml) scope['updateTaBindtaTextElement' + _serial]();
+				};
+
+				// note that focusout > focusin is called everytime we click a button - except bad support: http://www.quirksmode.org/dom/events/blurfocus.html
+				// cascades to displayElements.text and displayElements.html automatically.
+				_focusin = function(){
+					scope.focussed = true;
+					element.addClass(scope.classes.focussed);
+					_toolbars.focus();
+					element.triggerHandler('focus');
+				};
+				scope.displayElements.html.on('focus', _focusin);
+				scope.displayElements.text.on('focus', _focusin);
+				_focusout = function(e){
+					// if we are NOT runnig an action and have NOT focussed again on the text etc then fire the blur events
+					if(!scope._actionRunning && $document[0].activeElement !== scope.displayElements.html[0] && $document[0].activeElement !== scope.displayElements.text[0]){
+						element.removeClass(scope.classes.focussed);
+						_toolbars.unfocus();
+						// to prevent multiple apply error defer to next seems to work.
+						$timeout(function(){
+							scope._bUpdateSelectedStyles = false;
+							element.triggerHandler('blur');
+							scope.focussed = false;
+						}, 0);
+					}
+					e.preventDefault();
+					return false;
+				};
+				scope.displayElements.html.on('blur', _focusout);
+				scope.displayElements.text.on('blur', _focusout);
+
+				scope.displayElements.text.on('paste', function(event){
+					element.triggerHandler('paste', event);
+				});
+
+				// Setup the default toolbar tools, this way allows the user to add new tools like plugins.
+				// This is on the editor for future proofing if we find a better way to do this.
+				scope.queryFormatBlockState = function(command){
+					// $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
+					return !scope.showHtml && command.toLowerCase() === $document[0].queryCommandValue('formatBlock').toLowerCase();
+				};
+				scope.queryCommandState = function(command){
+					// $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
+					return (!scope.showHtml) ? $document[0].queryCommandState(command) : '';
+				};
+				scope.switchView = function(){
+					scope.showHtml = !scope.showHtml;
+					$animate.enabled(false, scope.displayElements.html);
+					$animate.enabled(false, scope.displayElements.text);
+					//Show the HTML view
+					if(scope.showHtml){
+						//defer until the element is visible
+						$timeout(function(){
+							$animate.enabled(true, scope.displayElements.html);
+							$animate.enabled(true, scope.displayElements.text);
+							// [0] dereferences the DOM object from the angular.element
+							return scope.displayElements.html[0].focus();
+						}, 100);
+					}else{
+						//Show the WYSIWYG view
+						//defer until the element is visible
+						$timeout(function(){
+							$animate.enabled(true, scope.displayElements.html);
+							$animate.enabled(true, scope.displayElements.text);
+							// [0] dereferences the DOM object from the angular.element
+							return scope.displayElements.text[0].focus();
+						}, 100);
+					}
+				};
+
+				// changes to the model variable from outside the html/text inputs
+				// if no ngModel, then the only input is from inside text-angular
+				if(attrs.ngModel){
+					var _firstRun = true;
+					ngModel.$render = function(){
+						if(_firstRun){
+							// we need this firstRun to set the originalContents otherwise it gets overrided by the setting of ngModel to undefined from NaN
+							_firstRun = false;
+							// if view value is null or undefined initially and there was original content, set to the original content
+							var _initialValue = scope.$parent.$eval(attrs.ngModel);
+							if((_initialValue === undefined || _initialValue === null) && (_originalContents && _originalContents !== '')){
+								// on passing through to taBind it will be sanitised
+								ngModel.$setViewValue(_originalContents);
+							}
+						}
+						scope.displayElements.forminput.val(ngModel.$viewValue);
+						// if the editors aren't focused they need to be updated, otherwise they are doing the updating
+						scope.html = ngModel.$viewValue || '';
+					};
+					// trigger the validation calls
+					if(element.attr('required')) ngModel.$validators.required = function(modelValue, viewValue) {
+						var value = modelValue || viewValue;
+						return !(!value || value.trim() === '');
+					};
+				}else{
+					// if no ngModel then update from the contents of the origional html.
+					scope.displayElements.forminput.val(_originalContents);
+					scope.html = _originalContents;
+				}
+
+				// changes from taBind back up to here
+				scope.$watch('html', function(newValue, oldValue){
+					if(newValue !== oldValue){
+						if(attrs.ngModel && ngModel.$viewValue !== newValue) ngModel.$setViewValue(newValue);
+						scope.displayElements.forminput.val(newValue);
+					}
+				});
+
+				if(attrs.taTargetToolbars) _toolbars = textAngularManager.registerEditor(scope._name, scope, attrs.taTargetToolbars.split(','));
+				else{
+					var _toolbar = angular.element('<div text-angular-toolbar name="textAngularToolbar' + _serial + '">');
+					// passthrough init of toolbar options
+					if(attrs.taToolbar)						_toolbar.attr('ta-toolbar', attrs.taToolbar);
+					if(attrs.taToolbarClass)				_toolbar.attr('ta-toolbar-class', attrs.taToolbarClass);
+					if(attrs.taToolbarGroupClass)			_toolbar.attr('ta-toolbar-group-class', attrs.taToolbarGroupClass);
+					if(attrs.taToolbarButtonClass)			_toolbar.attr('ta-toolbar-button-class', attrs.taToolbarButtonClass);
+					if(attrs.taToolbarActiveButtonClass)	_toolbar.attr('ta-toolbar-active-button-class', attrs.taToolbarActiveButtonClass);
+					if(attrs.taFocussedClass)				_toolbar.attr('ta-focussed-class', attrs.taFocussedClass);
+
+					element.prepend(_toolbar);
+					$compile(_toolbar)(scope.$parent);
+					_toolbars = textAngularManager.registerEditor(scope._name, scope, ['textAngularToolbar' + _serial]);
+				}
+
+				scope.$on('$destroy', function(){
+					textAngularManager.unregisterEditor(scope._name);
+					angular.element(window).off('blur');
+				});
+
+				// catch element select event and pass to toolbar tools
+				scope.$on('ta-element-select', function(event, element){
+					if(_toolbars.triggerElementSelect(event, element)){
+						scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
+					}
+				});
+
+				scope.$on('ta-drop-event', function(event, element, dropEvent, dataTransfer){
+					scope.displayElements.text[0].focus();
+					if(dataTransfer && dataTransfer.files && dataTransfer.files.length > 0){
+						angular.forEach(dataTransfer.files, function(file){
+							// taking advantage of boolean execution, if the fileDropHandler returns true, nothing else after it is executed
+							// If it is false then execute the defaultFileDropHandler if the fileDropHandler is NOT the default one
+							// Once one of these has been executed wrap the result as a promise, if undefined or variable update the taBind, else we should wait for the promise
+							try{
+								$q.when(scope.fileDropHandler(file, scope.wrapSelection) ||
+									(scope.fileDropHandler !== scope.defaultFileDropHandler &&
+									$q.when(scope.defaultFileDropHandler(file, scope.wrapSelection)))).then(function(){
+										scope['updateTaBindtaTextElement' + _serial]();
+									});
+							}catch(error){
+								$log.error(error);
+							}
+						});
+						dropEvent.preventDefault();
+						dropEvent.stopPropagation();
+					/* istanbul ignore else, the updates if moved text */
+					}else{
+						$timeout(function(){
+							scope['updateTaBindtaTextElement' + _serial]();
+						}, 0);
+					}
+				});
+
+				// the following is for applying the active states to the tools that support it
+				scope._bUpdateSelectedStyles = false;
+				/* istanbul ignore next: browser window/tab leave check */
+				angular.element(window).on('blur', function(){
+					scope._bUpdateSelectedStyles = false;
+					scope.focussed = false;
+				});
+				// loop through all the tools polling their activeState function if it exists
+				scope.updateSelectedStyles = function(){
+					var _selection;
+					/* istanbul ignore next: This check is to ensure multiple timeouts don't exist */
+					if(_updateSelectedStylesTimeout) $timeout.cancel(_updateSelectedStylesTimeout);
+					// test if the common element ISN'T the root ta-text node
+					if((_selection = taSelection.getSelectionElement()) !== undefined && _selection.parentNode !== scope.displayElements.text[0]){
+						_toolbars.updateSelectedStyles(angular.element(_selection));
+					}else _toolbars.updateSelectedStyles();
+					// used to update the active state when a key is held down, ie the left arrow
+					/* istanbul ignore else: browser only check */
+					if(scope._bUpdateSelectedStyles) _updateSelectedStylesTimeout = $timeout(scope.updateSelectedStyles, 200);
+				};
+				// start updating on keydown
+				_keydown = function(){
+					/* istanbul ignore next: ie catch */
+					if(!scope.focussed){
+						scope._bUpdateSelectedStyles = false;
+						return;
+					}
+					/* istanbul ignore else: don't run if already running */
+					if(!scope._bUpdateSelectedStyles){
+						scope._bUpdateSelectedStyles = true;
+						scope.$apply(function(){
+							scope.updateSelectedStyles();
+						});
+					}
+				};
+				scope.displayElements.html.on('keydown', _keydown);
+				scope.displayElements.text.on('keydown', _keydown);
+				// stop updating on key up and update the display/model
+				_keyup = function(){
+					scope._bUpdateSelectedStyles = false;
+				};
+				scope.displayElements.html.on('keyup', _keyup);
+				scope.displayElements.text.on('keyup', _keyup);
+				// stop updating on key up and update the display/model
+				_keypress = function(event, eventData){
+					/* istanbul ignore else: this is for catching the jqLite testing*/
+					if(eventData) angular.extend(event, eventData);
+					scope.$apply(function(){
+						if(_toolbars.sendKeyCommand(event)){
+							/* istanbul ignore else: don't run if already running */
+							if(!scope._bUpdateSelectedStyles){
+								scope.updateSelectedStyles();
+							}
+							event.preventDefault();
+							return false;
+						}
+					});
+				};
+				scope.displayElements.html.on('keypress', _keypress);
+				scope.displayElements.text.on('keypress', _keypress);
+				// update the toolbar active states when we click somewhere in the text/html boxed
+				_mouseup = function(){
+					// ensure only one execution of updateSelectedStyles()
+					scope._bUpdateSelectedStyles = false;
+					scope.$apply(function(){
+						scope.updateSelectedStyles();
+					});
+				};
+				scope.displayElements.html.on('mouseup', _mouseup);
+				scope.displayElements.text.on('mouseup', _mouseup);
+			}
+		};
+	}
+]);
+textAngular.service('textAngularManager', ['taToolExecuteAction', 'taTools', 'taRegisterTool', function(taToolExecuteAction, taTools, taRegisterTool){
+	// this service is used to manage all textAngular editors and toolbars.
+	// All publicly published functions that modify/need to access the toolbar or editor scopes should be in here
+	// these contain references to all the editors and toolbars that have been initialised in this app
+	var toolbars = {}, editors = {};
+	// when we focus into a toolbar, we need to set the TOOLBAR's $parent to be the toolbars it's linked to.
+	// We also need to set the tools to be updated to be the toolbars...
+	return {
+		// register an editor and the toolbars that it is affected by
+		registerEditor: function(name, scope, targetToolbars){
+			// targetToolbars are optional, we don't require a toolbar to function
+			if(!name || name === '') throw('textAngular Error: An editor requires a name');
+			if(!scope) throw('textAngular Error: An editor requires a scope');
+			if(editors[name]) throw('textAngular Error: An Editor with name "' + name + '" already exists');
+			// _toolbars is an ARRAY of toolbar scopes
+			var _toolbars = [];
+			angular.forEach(targetToolbars, function(_name){
+				if(toolbars[_name]) _toolbars.push(toolbars[_name]);
+				// if it doesn't exist it may not have been compiled yet and it will be added later
+			});
+			editors[name] = {
+				scope: scope,
+				toolbars: targetToolbars,
+				_registerToolbar: function(toolbarScope){
+					// add to the list late
+					if(this.toolbars.indexOf(toolbarScope.name) >= 0) _toolbars.push(toolbarScope);
+				},
+				// this is a suite of functions the editor should use to update all it's linked toolbars
+				editorFunctions: {
+					disable: function(){
+						// disable all linked toolbars
+						angular.forEach(_toolbars, function(toolbarScope){ toolbarScope.disabled = true; });
+					},
+					enable: function(){
+						// enable all linked toolbars
+						angular.forEach(_toolbars, function(toolbarScope){ toolbarScope.disabled = false; });
+					},
+					focus: function(){
+						// this should be called when the editor is focussed
+						angular.forEach(_toolbars, function(toolbarScope){
+							toolbarScope._parent = scope;
+							toolbarScope.disabled = false;
+							toolbarScope.focussed = true;
+							scope.focussed = true;
+						});
+					},
+					unfocus: function(){
+						// this should be called when the editor becomes unfocussed
+						angular.forEach(_toolbars, function(toolbarScope){
+							toolbarScope.disabled = true;
+							toolbarScope.focussed = false;
+						});
+						scope.focussed = false;
+					},
+					updateSelectedStyles: function(selectedElement){
+						// update the active state of all buttons on liked toolbars
+						angular.forEach(_toolbars, function(toolbarScope){
+							angular.forEach(toolbarScope.tools, function(toolScope){
+								if(toolScope.activeState){
+									toolbarScope._parent = scope;
+									toolScope.active = toolScope.activeState(selectedElement);
+								}
+							});
+						});
+					},
+					sendKeyCommand: function(event){
+						// we return true if we applied an action, false otherwise
+						var result = false;
+						if(event.ctrlKey || event.metaKey || event.specialKey) angular.forEach(taTools, function(tool, name){
+							if(tool.commandKeyCode && (tool.commandKeyCode === event.which || tool.commandKeyCode === event.specialKey)){
+								for(var _t = 0; _t < _toolbars.length; _t++){
+									if(_toolbars[_t].tools[name] !== undefined){
+										taToolExecuteAction.call(_toolbars[_t].tools[name], scope);
+										result = true;
+										break;
+									}
+								}
+							}
+						});
+						return result;
+					},
+					triggerElementSelect: function(event, element){
+						// search through the taTools to see if a match for the tag is made.
+						// if there is, see if the tool is on a registered toolbar and not disabled.
+						// NOTE: This can trigger on MULTIPLE tools simultaneously.
+						var elementHasAttrs = function(_element, attrs){
+							var result = true;
+							for(var i = 0; i < attrs.length; i++) result = result && _element.attr(attrs[i]);
+							return result;
+						};
+						var workerTools = [];
+						var unfilteredTools = {};
+						var result = false;
+						element = angular.element(element);
+						// get all valid tools by element name, keep track if one matches the
+						var onlyWithAttrsFilter = false;
+						angular.forEach(taTools, function(tool, name){
+							if(
+								tool.onElementSelect &&
+								tool.onElementSelect.element &&
+								tool.onElementSelect.element.toLowerCase() === element[0].tagName.toLowerCase() &&
+								(!tool.onElementSelect.filter || tool.onElementSelect.filter(element))
+							){
+								// this should only end up true if the element matches the only attributes
+								onlyWithAttrsFilter = onlyWithAttrsFilter ||
+									(angular.isArray(tool.onElementSelect.onlyWithAttrs) && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs));
+								if(!tool.onElementSelect.onlyWithAttrs || elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) unfilteredTools[name] = tool;
+							}
+						});
+						// if we matched attributes to filter on, then filter, else continue
+						if(onlyWithAttrsFilter){
+							angular.forEach(unfilteredTools, function(tool, name){
+								if(tool.onElementSelect.onlyWithAttrs && elementHasAttrs(element, tool.onElementSelect.onlyWithAttrs)) workerTools.push({'name': name, 'tool': tool});
+							});
+							// sort most specific (most attrs to find) first
+							workerTools.sort(function(a,b){
+								return b.tool.onElementSelect.onlyWithAttrs.length - a.tool.onElementSelect.onlyWithAttrs.length;
+							});
+						}else{
+							angular.forEach(unfilteredTools, function(tool, name){
+								workerTools.push({'name': name, 'tool': tool});
+							});
+						}
+						// Run the actions on the first visible filtered tool only
+						if(workerTools.length > 0){
+							for(var _i = 0; _i < workerTools.length; _i++){
+								var tool = workerTools[_i].tool;
+								var name = workerTools[_i].name;
+								for(var _t = 0; _t < _toolbars.length; _t++){
+									if(_toolbars[_t].tools[name] !== undefined){
+										tool.onElementSelect.action.call(_toolbars[_t].tools[name], event, element, scope);
+										result = true;
+										break;
+									}
+								}
+								if(result) break;
+							}
+						}
+						return result;
+					}
+				}
+			};
+			return editors[name].editorFunctions;
+		},
+		// retrieve editor by name, largely used by testing suites only
+		retrieveEditor: function(name){
+			return editors[name];
+		},
+		unregisterEditor: function(name){
+			delete editors[name];
+		},
+		// registers a toolbar such that it can be linked to editors
+		registerToolbar: function(scope){
+			if(!scope) throw('textAngular Error: A toolbar requires a scope');
+			if(!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
+			if(toolbars[scope.name]) throw('textAngular Error: A toolbar with name "' + scope.name + '" already exists');
+			toolbars[scope.name] = scope;
+			angular.forEach(editors, function(_editor){
+				_editor._registerToolbar(scope);
+			});
+		},
+		// retrieve toolbar by name, largely used by testing suites only
+		retrieveToolbar: function(name){
+			return toolbars[name];
+		},
+		// retrieve toolbars by editor name, largely used by testing suites only
+		retrieveToolbarsViaEditor: function(name){
+			var result = [], _this = this;
+			angular.forEach(this.retrieveEditor(name).toolbars, function(name){
+				result.push(_this.retrieveToolbar(name));
+			});
+			return result;
+		},
+		unregisterToolbar: function(name){
+			delete toolbars[name];
+		},
+		// functions for updating the toolbar buttons display
+		updateToolsDisplay: function(newTaTools){
+			// pass a partial struct of the taTools, this allows us to update the tools on the fly, will not change the defaults.
+			var _this = this;
+			angular.forEach(newTaTools, function(_newTool, key){
+				_this.updateToolDisplay(key, _newTool);
+			});
+		},
+		// this function resets all toolbars to their default tool definitions
+		resetToolsDisplay: function(){
+			var _this = this;
+			angular.forEach(taTools, function(_newTool, key){
+				_this.resetToolDisplay(key);
+			});
+		},
+		// update a tool on all toolbars
+		updateToolDisplay: function(toolKey, _newTool){
+			var _this = this;
+			angular.forEach(toolbars, function(toolbarScope, toolbarKey){
+				_this.updateToolbarToolDisplay(toolbarKey, toolKey, _newTool);
+			});
+		},
+		// resets a tool to the default/starting state on all toolbars
+		resetToolDisplay: function(toolKey){
+			var _this = this;
+			angular.forEach(toolbars, function(toolbarScope, toolbarKey){
+				_this.resetToolbarToolDisplay(toolbarKey, toolKey);
+			});
+		},
+		// update a tool on a specific toolbar
+		updateToolbarToolDisplay: function(toolbarKey, toolKey, _newTool){
+			if(toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, _newTool);
+			else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
+		},
+		// reset a tool on a specific toolbar to it's default starting value
+		resetToolbarToolDisplay: function(toolbarKey, toolKey){
+			if(toolbars[toolbarKey]) toolbars[toolbarKey].updateToolDisplay(toolKey, taTools[toolKey], true);
+			else throw('textAngular Error: No Toolbar with name "' + toolbarKey + '" exists');
+		},
+		// removes a tool from all toolbars and it's definition
+		removeTool: function(toolKey){
+			delete taTools[toolKey];
+			angular.forEach(toolbars, function(toolbarScope){
+				delete toolbarScope.tools[toolKey];
+				for(var i = 0; i < toolbarScope.toolbar.length; i++){
+					var toolbarIndex;
+					for(var j = 0; j < toolbarScope.toolbar[i].length; j++){
+						if(toolbarScope.toolbar[i][j] === toolKey){
+							toolbarIndex = {
+								group: i,
+								index: j
+							};
+							break;
+						}
+						if(toolbarIndex !== undefined) break;
+					}
+					if(toolbarIndex !== undefined){
+						toolbarScope.toolbar[toolbarIndex.group].slice(toolbarIndex.index, 1);
+						toolbarScope._$element.children().eq(toolbarIndex.group).children().eq(toolbarIndex.index).remove();
+					}
+				}
+			});
+		},
+		// toolkey, toolDefinition are required. If group is not specified will pick the last group, if index isnt defined will append to group
+		addTool: function(toolKey, toolDefinition, group, index){
+			taRegisterTool(toolKey, toolDefinition);
+			angular.forEach(toolbars, function(toolbarScope){
+				toolbarScope.addTool(toolKey, toolDefinition, group, index);
+			});
+		},
+		// adds a Tool but only to one toolbar not all
+		addToolToToolbar: function(toolKey, toolDefinition, toolbarKey, group, index){
+			taRegisterTool(toolKey, toolDefinition);
+			toolbars[toolbarKey].addTool(toolKey, toolDefinition, group, index);
+		},
+		// this is used when externally the html of an editor has been changed and textAngular needs to be notified to update the model.
+		// this will call a $digest if not already happening
+		refreshEditor: function(name){
+			if(editors[name]){
+				editors[name].scope.updateTaBindtaTextElement();
+				/* istanbul ignore else: phase catch */
+				if(!editors[name].scope.$$phase) editors[name].scope.$digest();
+			}else throw('textAngular Error: No Editor with name "' + name + '" exists');
+		},
+		// this is used by taBind to send a key command in response to a special key event
+		sendKeyCommand: function(scope, event){
+			var _editor = editors[scope._name];
+			/* istanbul ignore else: if nothing to do, do nothing */
+			if (_editor && _editor.editorFunctions.sendKeyCommand(event)) {
+				/* istanbul ignore else: don't run if already running */
+				if(!scope._bUpdateSelectedStyles){
+					scope.updateSelectedStyles();
+				}
+				event.preventDefault();
+				return false;
+			}
+		}
+	};
+}]);
+textAngular.directive('textAngularToolbar', [
+	'$compile', 'textAngularManager', 'taOptions', 'taTools', 'taToolExecuteAction', '$window',
+	function($compile, textAngularManager, taOptions, taTools, taToolExecuteAction, $window){
+		return {
+			scope: {
+				name: '@' // a name IS required
+			},
+			restrict: "EA",
+			link: function(scope, element, attrs){
+				if(!scope.name || scope.name === '') throw('textAngular Error: A toolbar requires a name');
+				angular.extend(scope, angular.copy(taOptions));
+				if(attrs.taToolbar)						scope.toolbar = scope.$parent.$eval(attrs.taToolbar);
+				if(attrs.taToolbarClass)				scope.classes.toolbar = attrs.taToolbarClass;
+				if(attrs.taToolbarGroupClass)			scope.classes.toolbarGroup = attrs.taToolbarGroupClass;
+				if(attrs.taToolbarButtonClass)			scope.classes.toolbarButton = attrs.taToolbarButtonClass;
+				if(attrs.taToolbarActiveButtonClass)	scope.classes.toolbarButtonActive = attrs.taToolbarActiveButtonClass;
+				if(attrs.taFocussedClass)				scope.classes.focussed = attrs.taFocussedClass;
+
+				scope.disabled = true;
+				scope.focussed = false;
+				scope._$element = element;
+				element[0].innerHTML = '';
+				element.addClass("ta-toolbar " + scope.classes.toolbar);
+
+				scope.$watch('focussed', function(){
+					if(scope.focussed) element.addClass(scope.classes.focussed);
+					else element.removeClass(scope.classes.focussed);
+				});
+
+				var setupToolElement = function(toolDefinition, toolScope){
+					var toolElement;
+					if(toolDefinition && toolDefinition.display){
+						toolElement = angular.element(toolDefinition.display);
+					}
+					else toolElement = angular.element("<button type='button'>");
+
+					if(toolDefinition && toolDefinition["class"]) toolElement.addClass(toolDefinition["class"]);
+					else toolElement.addClass(scope.classes.toolbarButton);
+
+					toolElement.attr('name', toolScope.name);
+					// important to not take focus from the main text/html entry
+					toolElement.attr('ta-button', 'ta-button');
+					toolElement.attr('ng-disabled', 'isDisabled()');
+					toolElement.attr('tabindex', '-1');
+					toolElement.attr('ng-click', 'executeAction()');
+					toolElement.attr('ng-class', 'displayActiveToolClass(active)');
+
+					if (toolDefinition && toolDefinition.tooltiptext) {
+						toolElement.attr('title', toolDefinition.tooltiptext);
+					}
+					if(toolDefinition && !toolDefinition.display && !toolScope._display){
+						// first clear out the current contents if any
+						toolElement[0].innerHTML = '';
+						// add the buttonText
+						if(toolDefinition.buttontext) toolElement[0].innerHTML = toolDefinition.buttontext;
+						// add the icon to the front of the button if there is content
+						if(toolDefinition.iconclass){
+							var icon = angular.element('<i>'), content = toolElement[0].innerHTML;
+							icon.addClass(toolDefinition.iconclass);
+							toolElement[0].innerHTML = '';
+							toolElement.append(icon);
+							if(content && content !== '') toolElement.append('&nbsp;' + content);
+						}
+					}
+
+					toolScope._lastToolDefinition = angular.copy(toolDefinition);
+
+					return $compile(toolElement)(toolScope);
+				};
+
+				// Keep a reference for updating the active states later
+				scope.tools = {};
+				// create the tools in the toolbar
+				// default functions and values to prevent errors in testing and on init
+				scope._parent = {
+					disabled: true,
+					showHtml: false,
+					queryFormatBlockState: function(){ return false; },
+					queryCommandState: function(){ return false; }
+				};
+				var defaultChildScope = {
+					$window: $window,
+					$editor: function(){
+						// dynamically gets the editor as it is set
+						return scope._parent;
+					},
+					isDisabled: function(){
+						// to set your own disabled logic set a function or boolean on the tool called 'disabled'
+						return ( // this bracket is important as without it it just returns the first bracket and ignores the rest
+							// when the button's disabled function/value evaluates to true
+							(typeof this.$eval('disabled') !== 'function' && this.$eval('disabled')) || this.$eval('disabled()') ||
+							// all buttons except the HTML Switch button should be disabled in the showHtml (RAW html) mode
+							(this.name !== 'html' && this.$editor().showHtml) ||
+							// if the toolbar is disabled
+							this.$parent.disabled ||
+							// if the current editor is disabled
+							this.$editor().disabled
+						);
+					},
+					displayActiveToolClass: function(active){
+						return (active)? scope.classes.toolbarButtonActive : '';
+					},
+					executeAction: taToolExecuteAction
+				};
+
+				angular.forEach(scope.toolbar, function(group){
+					// setup the toolbar group
+					var groupElement = angular.element("<div>");
+					groupElement.addClass(scope.classes.toolbarGroup);
+					angular.forEach(group, function(tool){
+						// init and add the tools to the group
+						// a tool name (key name from taTools struct)
+						//creates a child scope of the main angularText scope and then extends the childScope with the functions of this particular tool
+						// reference to the scope and element kept
+						scope.tools[tool] = angular.extend(scope.$new(true), taTools[tool], defaultChildScope, {name: tool});
+						scope.tools[tool].$element = setupToolElement(taTools[tool], scope.tools[tool]);
+						// append the tool compiled with the childScope to the group element
+						groupElement.append(scope.tools[tool].$element);
+					});
+					// append the group to the toolbar
+					element.append(groupElement);
+				});
+
+				// update a tool
+				// if a value is set to null, remove from the display
+				// when forceNew is set to true it will ignore all previous settings, used to reset to taTools definition
+				// to reset to defaults pass in taTools[key] as _newTool and forceNew as true, ie `updateToolDisplay(key, taTools[key], true);`
+				scope.updateToolDisplay = function(key, _newTool, forceNew){
+					var toolInstance = scope.tools[key];
+					if(toolInstance){
+						// get the last toolDefinition, then override with the new definition
+						if(toolInstance._lastToolDefinition && !forceNew) _newTool = angular.extend({}, toolInstance._lastToolDefinition, _newTool);
+						if(_newTool.buttontext === null && _newTool.iconclass === null && _newTool.display === null)
+							throw('textAngular Error: Tool Definition for updating "' + key + '" does not have a valid display/iconclass/buttontext value');
+
+						// if tool is defined on this toolbar, update/redo the tool
+						if(_newTool.buttontext === null){
+							delete _newTool.buttontext;
+						}
+						if(_newTool.iconclass === null){
+							delete _newTool.iconclass;
+						}
+						if(_newTool.display === null){
+							delete _newTool.display;
+						}
+
+						var toolElement = setupToolElement(_newTool, toolInstance);
+						toolInstance.$element.replaceWith(toolElement);
+						toolInstance.$element = toolElement;
+					}
+				};
+
+				// we assume here that all values passed are valid and correct
+				scope.addTool = function(key, _newTool, groupIndex, index){
+					scope.tools[key] = angular.extend(scope.$new(true), taTools[key], defaultChildScope, {name: key});
+					scope.tools[key].$element = setupToolElement(taTools[key], scope.tools[key]);
+					var group;
+					if(groupIndex === undefined) groupIndex = scope.toolbar.length - 1;
+					group = angular.element(element.children()[groupIndex]);
+
+					if(index === undefined){
+						group.append(scope.tools[key].$element);
+						scope.toolbar[groupIndex][scope.toolbar[groupIndex].length - 1] = key;
+					}else{
+						group.children().eq(index).after(scope.tools[key].$element);
+						scope.toolbar[groupIndex][index] = key;
+					}
+				};
+
+				textAngularManager.registerToolbar(scope);
+
+				scope.$on('$destroy', function(){
+					textAngularManager.unregisterToolbar(scope.name);
+				});
+			}
+		};
+	}
+]);

File diff suppressed because it is too large
+ 0 - 0
static/js/textAngular.min.js


+ 507 - 0
static/js/toaster.js

@@ -0,0 +1,507 @@
+/* global angular */
+(function(window, document) {
+    'use strict';
+
+    /*
+     * AngularJS Toaster
+     * Version: 2.0.0
+     *
+     * Copyright 2013-2016 Jiri Kavulak.
+     * All Rights Reserved.
+     * Use, reproduction, distribution, and modification of this code is subject to the terms and
+     * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
+     *
+     * Author: Jiri Kavulak
+     * Related to project of John Papa, Hans Fjällemark and Nguyễn Thiện Hùng (thienhung1989)
+     */
+
+    angular.module('toaster', []).constant(
+        'toasterConfig', {
+            'limit': 0,                   // limits max number of toasts
+            'tap-to-dismiss': true,
+            'close-button': false,
+            'close-html': '<button class="toast-close-button" type="button">&times;</button>',
+            'newest-on-top': true,
+            'time-out': 5000,
+            'icon-classes': {
+                error: 'toast-error',
+                info: 'toast-info',
+                wait: 'toast-wait',
+                success: 'toast-success',
+                warning: 'toast-warning'
+            },
+            'body-output-type': '', // Options: '', 'trustedHtml', 'template', 'templateWithData', 'directive'
+            'body-template': 'toasterBodyTmpl.html',
+            'icon-class': 'toast-info',
+            'position-class': 'toast-top-right', // Options (see CSS):
+            // 'toast-top-full-width', 'toast-bottom-full-width', 'toast-center',
+            // 'toast-top-left', 'toast-top-center', 'toast-top-right',
+            // 'toast-bottom-left', 'toast-bottom-center', 'toast-bottom-right',
+            'title-class': 'toast-title',
+            'message-class': 'toast-message',
+            'prevent-duplicates': false,
+            'mouseover-timer-stop': true // stop timeout on mouseover and restart timer on mouseout
+        }
+    ).service(
+        'toaster', [
+            '$rootScope', 'toasterConfig', function($rootScope, toasterConfig) {
+                // http://stackoverflow.com/questions/26501688/a-typescript-guid-class
+                var Guid = (function() {
+                    var Guid = {};
+                    Guid.newGuid = function() {
+                        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+                            var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+                            return v.toString(16);
+                        });
+                    };
+                    return Guid;
+                }());
+
+                this.pop = function(type, title, body, timeout, bodyOutputType, clickHandler, toasterId, showCloseButton, toastId, onHideCallback) {
+                    if (angular.isObject(type)) {
+                        var params = type; // Enable named parameters as pop argument
+                        this.toast = {
+                            type: params.type,
+                            title: params.title,
+                            body: params.body,
+                            timeout: params.timeout,
+                            bodyOutputType: params.bodyOutputType,
+                            clickHandler: params.clickHandler,
+                            showCloseButton: params.showCloseButton,
+                            closeHtml: params.closeHtml,
+                            toastId: params.toastId,
+                            onShowCallback: params.onShowCallback,
+                            onHideCallback: params.onHideCallback,
+                            directiveData: params.directiveData
+                        };
+                        toasterId = params.toasterId;
+                    } else {
+                        this.toast = {
+                            type: type,
+                            title: title,
+                            body: body,
+                            timeout: timeout,
+                            bodyOutputType: bodyOutputType,
+                            clickHandler: clickHandler,
+                            showCloseButton: showCloseButton,
+                            toastId: toastId,
+                            onHideCallback: onHideCallback
+                        };
+                    }
+
+                    if (!this.toast.toastId || !this.toast.toastId.length) {
+                        this.toast.toastId = Guid.newGuid();
+                    }
+
+                    $rootScope.$emit('toaster-newToast', toasterId, this.toast.toastId);
+                    
+                    return {
+                        toasterId: toasterId,
+                        toastId: this.toast.toastId
+                    };
+                };
+
+                this.clear = function(toasterId, toastId) {
+                    if (angular.isObject(toasterId)) {
+                        $rootScope.$emit('toaster-clearToasts', toasterId.toasterId, toasterId.toastId);
+                    } else {
+                        $rootScope.$emit('toaster-clearToasts', toasterId, toastId);
+                    }
+                };
+
+                // Create one method per icon class, to allow to call toaster.info() and similar
+                for (var type in toasterConfig['icon-classes']) {
+                    this[type] = createTypeMethod(type);
+                }
+
+                function createTypeMethod(toasterType) {
+                    return function(title, body, timeout, bodyOutputType, clickHandler, toasterId, showCloseButton, toastId, onHideCallback) {
+                        if (angular.isString(title)) {
+                            return this.pop(
+                                toasterType,
+                                title,
+                                body,
+                                timeout,
+                                bodyOutputType,
+                                clickHandler,
+                                toasterId,
+                                showCloseButton,
+                                toastId,
+                                onHideCallback);
+                        } else { // 'title' is actually an object with options
+                            return this.pop(angular.extend(title, { type: toasterType }));
+                        }
+                    };
+                }
+            }]
+        ).factory(
+        'toasterEventRegistry', [
+            '$rootScope', function($rootScope) {
+                var deregisterNewToast = null, deregisterClearToasts = null, newToastEventSubscribers = [], clearToastsEventSubscribers = [], toasterFactory;
+
+                toasterFactory = {
+                    setup: function() {
+                        if (!deregisterNewToast) {
+                            deregisterNewToast = $rootScope.$on(
+                                'toaster-newToast', function(event, toasterId, toastId) {
+                                    for (var i = 0, len = newToastEventSubscribers.length; i < len; i++) {
+                                        newToastEventSubscribers[i](event, toasterId, toastId);
+                                    }
+                                });
+                        }
+
+                        if (!deregisterClearToasts) {
+                            deregisterClearToasts = $rootScope.$on(
+                                'toaster-clearToasts', function(event, toasterId, toastId) {
+                                    for (var i = 0, len = clearToastsEventSubscribers.length; i < len; i++) {
+                                        clearToastsEventSubscribers[i](event, toasterId, toastId);
+                                    }
+                                });
+                        }
+                    },
+
+                    subscribeToNewToastEvent: function(onNewToast) {
+                        newToastEventSubscribers.push(onNewToast);
+                    },
+                    subscribeToClearToastsEvent: function(onClearToasts) {
+                        clearToastsEventSubscribers.push(onClearToasts);
+                    },
+                    unsubscribeToNewToastEvent: function(onNewToast) {
+                        var index = newToastEventSubscribers.indexOf(onNewToast);
+                        if (index >= 0) {
+                            newToastEventSubscribers.splice(index, 1);
+                        }
+
+                        if (newToastEventSubscribers.length === 0) {
+                            deregisterNewToast();
+                            deregisterNewToast = null;
+                        }
+                    },
+                    unsubscribeToClearToastsEvent: function(onClearToasts) {
+                        var index = clearToastsEventSubscribers.indexOf(onClearToasts);
+                        if (index >= 0) {
+                            clearToastsEventSubscribers.splice(index, 1);
+                        }
+
+                        if (clearToastsEventSubscribers.length === 0) {
+                            deregisterClearToasts();
+                            deregisterClearToasts = null;
+                        }
+                    }
+                };
+                return {
+                    setup: toasterFactory.setup,
+                    subscribeToNewToastEvent: toasterFactory.subscribeToNewToastEvent,
+                    subscribeToClearToastsEvent: toasterFactory.subscribeToClearToastsEvent,
+                    unsubscribeToNewToastEvent: toasterFactory.unsubscribeToNewToastEvent,
+                    unsubscribeToClearToastsEvent: toasterFactory.unsubscribeToClearToastsEvent
+                };
+            }]
+        )
+        .directive('directiveTemplate', ['$compile', '$injector', function($compile, $injector) {
+            return {
+                restrict: 'A',
+                scope: {
+                    directiveName: '@directiveName',
+                    directiveData: '@directiveData'
+                },
+                replace: true,
+                link: function(scope, elm, attrs) {
+                    scope.$watch('directiveName', function(directiveName) {
+                        if (angular.isUndefined(directiveName) || directiveName.length <= 0)
+                            throw new Error('A valid directive name must be provided via the toast body argument when using bodyOutputType: directive');
+
+                        var directive;
+
+                        try {
+                            directive = $injector.get(attrs.$normalize(directiveName) + 'Directive');
+                        } catch (e) {
+                            throw new Error(directiveName + ' could not be found. ' +
+                                'The name should appear as it exists in the markup, not camelCased as it would appear in the directive declaration,' +
+                                ' e.g. directive-name not directiveName.');
+                        }
+
+
+                        var directiveDetails = directive[0];
+
+                        if (directiveDetails.scope !== true && directiveDetails.scope) {
+                            throw new Error('Cannot use a directive with an isolated scope. ' +
+                                'The scope must be either true or falsy (e.g. false/null/undefined). ' +
+                                'Occurred for directive ' + directiveName + '.');
+                        }
+
+                        if (directiveDetails.restrict.indexOf('A') < 0) {
+                            throw new Error('Directives must be usable as attributes. ' +
+                                'Add "A" to the restrict option (or remove the option entirely). Occurred for directive ' +
+                                directiveName + '.');
+                        }
+
+                        if (scope.directiveData)
+                            scope.directiveData = angular.fromJson(scope.directiveData);
+
+                        var template = $compile('<div ' + directiveName + '></div>')(scope);
+
+                        elm.append(template);
+                    });
+                }
+            };
+        }])
+        .directive(
+        'toasterContainer', [
+            '$parse', '$rootScope', '$interval', '$sce', 'toasterConfig', 'toaster', 'toasterEventRegistry',
+            function($parse, $rootScope, $interval, $sce, toasterConfig, toaster, toasterEventRegistry) {
+                return {
+                    replace: true,
+                    restrict: 'EA',
+                    scope: true, // creates an internal scope for this directive (one per directive instance)
+                    link: function(scope, elm, attrs) {
+                        var mergedConfig;
+
+                        // Merges configuration set in directive with default one
+                        mergedConfig = angular.extend({}, toasterConfig, scope.$eval(attrs.toasterOptions));
+
+                        scope.config = {
+                            toasterId: mergedConfig['toaster-id'],
+                            position: mergedConfig['position-class'],
+                            title: mergedConfig['title-class'],
+                            message: mergedConfig['message-class'],
+                            tap: mergedConfig['tap-to-dismiss'],
+                            closeButton: mergedConfig['close-button'],
+                            closeHtml: mergedConfig['close-html'],
+                            animation: mergedConfig['animation-class'],
+                            mouseoverTimer: mergedConfig['mouseover-timer-stop']
+                        };
+
+                        scope.$on(
+                            "$destroy", function() {
+                                toasterEventRegistry.unsubscribeToNewToastEvent(scope._onNewToast);
+                                toasterEventRegistry.unsubscribeToClearToastsEvent(scope._onClearToasts);
+                            }
+                        );
+
+                        function setTimeout(toast, time) {
+                            toast.timeoutPromise = $interval(
+                                function() {
+                                    scope.removeToast(toast.toastId);
+                                }, time, 1
+                            );
+                        }
+
+                        scope.configureTimer = function(toast) {
+                            var timeout = angular.isNumber(toast.timeout) ? toast.timeout : mergedConfig['time-out'];
+                            if (typeof timeout === "object") timeout = timeout[toast.type];
+                            if (timeout > 0) {
+                                setTimeout(toast, timeout);
+                            }
+                        };
+
+                        function addToast(toast, toastId) {
+                            toast.type = mergedConfig['icon-classes'][toast.type];
+                            if (!toast.type) {
+                                toast.type = mergedConfig['icon-class'];
+                            }
+
+                            if (mergedConfig['prevent-duplicates'] === true && scope.toasters.length) {
+                                if (scope.toasters[scope.toasters.length - 1].body === toast.body) {
+                                    return;
+                                } else {
+                                    var i, len, dupFound = false;
+                                    for (i = 0, len = scope.toasters.length; i < len; i++) {
+                                        if (scope.toasters[i].toastId === toastId) {
+                                            dupFound = true;
+                                            break;
+                                        }
+                                    }
+                                    
+                                    if (dupFound) return;
+                                }
+                            }
+
+
+                            // set the showCloseButton property on the toast so that
+                            // each template can bind directly to the property to show/hide
+                            // the close button
+                            var closeButton = mergedConfig['close-button'];
+
+                            // if toast.showCloseButton is a boolean value,
+                            // it was specifically overriden in the pop arguments
+                            if (typeof toast.showCloseButton === "boolean") {
+
+                            } else if (typeof closeButton === "boolean") {
+                                toast.showCloseButton = closeButton;
+                            } else if (typeof closeButton === "object") {
+                                var closeButtonForType = closeButton[toast.type];
+
+                                if (typeof closeButtonForType !== "undefined" && closeButtonForType !== null) {
+                                    toast.showCloseButton = closeButtonForType;
+                                }
+                            } else {
+                                // if an option was not set, default to false.
+                                toast.showCloseButton = false;
+                            }
+
+                            if (toast.showCloseButton) {
+                                toast.closeHtml = $sce.trustAsHtml(toast.closeHtml || scope.config.closeHtml);
+                            }
+
+                            // Set the toast.bodyOutputType to the default if it isn't set
+                            toast.bodyOutputType = toast.bodyOutputType || mergedConfig['body-output-type'];
+                            switch (toast.bodyOutputType) {
+                                case 'trustedHtml':
+                                    toast.html = $sce.trustAsHtml(toast.body);
+                                    break;
+                                case 'template':
+                                    toast.bodyTemplate = toast.body || mergedConfig['body-template'];
+                                    break;
+                                case 'templateWithData':
+                                    var fcGet = $parse(toast.body || mergedConfig['body-template']);
+                                    var templateWithData = fcGet(scope);
+                                    toast.bodyTemplate = templateWithData.template;
+                                    toast.data = templateWithData.data;
+                                    break;
+                                case 'directive':
+                                    toast.html = toast.body;
+                                    break;
+                            }
+
+                            scope.configureTimer(toast);
+
+                            if (mergedConfig['newest-on-top'] === true) {
+                                scope.toasters.unshift(toast);
+                                if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
+                                    scope.toasters.pop();
+                                }
+                            } else {
+                                scope.toasters.push(toast);
+                                if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) {
+                                    scope.toasters.shift();
+                                }
+                            }
+
+                            if (angular.isFunction(toast.onShowCallback)) {
+                                toast.onShowCallback();
+                            }
+                        }
+
+                        scope.removeToast = function(toastId) {
+                            var i, len;
+                            for (i = 0, len = scope.toasters.length; i < len; i++) {
+                                if (scope.toasters[i].toastId === toastId) {
+                                    removeToast(i);
+                                    break;
+                                }
+                            }
+                        };
+
+                        function removeToast(toastIndex) {
+                            var toast = scope.toasters[toastIndex];
+
+                            // toast is always defined since the index always has a match
+                            if (toast.timeoutPromise) {
+                                $interval.cancel(toast.timeoutPromise);
+                            }
+                            scope.toasters.splice(toastIndex, 1);
+
+                            if (angular.isFunction(toast.onHideCallback)) {
+                                toast.onHideCallback();
+                            }
+                        }
+
+                        function removeAllToasts(toastId) {
+                            for (var i = scope.toasters.length - 1; i >= 0; i--) {
+                                if (isUndefinedOrNull(toastId)) {
+                                    removeToast(i);
+                                } else {
+                                    if (scope.toasters[i].toastId == toastId) {
+                                        removeToast(i);
+                                    }
+                                }
+                            }
+                        }
+
+                        scope.toasters = [];
+
+                        function isUndefinedOrNull(val) {
+                            return angular.isUndefined(val) || val === null;
+                        }
+
+                        scope._onNewToast = function(event, toasterId, toastId) {
+                            // Compatibility: if toaster has no toasterId defined, and if call to display
+                            // hasn't either, then the request is for us
+
+                            if ((isUndefinedOrNull(scope.config.toasterId) && isUndefinedOrNull(toasterId)) || (!isUndefinedOrNull(scope.config.toasterId) && !isUndefinedOrNull(toasterId) && scope.config.toasterId == toasterId)) {
+                                addToast(toaster.toast, toastId);
+                            }
+                        };
+                        scope._onClearToasts = function(event, toasterId, toastId) {
+                            // Compatibility: if toaster has no toasterId defined, and if call to display
+                            // hasn't either, then the request is for us
+                            if (toasterId == '*' || (isUndefinedOrNull(scope.config.toasterId) && isUndefinedOrNull(toasterId)) || (!isUndefinedOrNull(scope.config.toasterId) && !isUndefinedOrNull(toasterId) && scope.config.toasterId == toasterId)) {
+                                removeAllToasts(toastId);
+                            }
+                        };
+
+                        toasterEventRegistry.setup();
+
+                        toasterEventRegistry.subscribeToNewToastEvent(scope._onNewToast);
+                        toasterEventRegistry.subscribeToClearToastsEvent(scope._onClearToasts);
+                    },
+                    controller: [
+                        '$scope', '$element', '$attrs', function($scope, $element, $attrs) {
+                            // Called on mouseover
+                            $scope.stopTimer = function(toast) {
+                                if ($scope.config.mouseoverTimer === true) {
+                                    if (toast.timeoutPromise) {
+                                        $interval.cancel(toast.timeoutPromise);
+                                        toast.timeoutPromise = null;
+                                    }
+                                }
+                            };
+
+                            // Called on mouseout
+                            $scope.restartTimer = function(toast) {
+                                if ($scope.config.mouseoverTimer === true) {
+                                    if (!toast.timeoutPromise) {
+                                        $scope.configureTimer(toast);
+                                    }
+                                } else if (toast.timeoutPromise === null) {
+                                    $scope.removeToast(toast.toastId);
+                                }
+                            };
+
+                            $scope.click = function(toast, isCloseButton) {
+                                if ($scope.config.tap === true || (toast.showCloseButton === true && isCloseButton === true)) {
+                                    var removeToast = true;
+                                    if (toast.clickHandler) {
+                                        if (angular.isFunction(toast.clickHandler)) {
+                                            removeToast = toast.clickHandler(toast, isCloseButton);
+                                        } else if (angular.isFunction($scope.$parent.$eval(toast.clickHandler))) {
+                                            removeToast = $scope.$parent.$eval(toast.clickHandler)(toast, isCloseButton);
+                                        } else {
+                                            console.log("TOAST-NOTE: Your click handler is not inside a parent scope of toaster-container.");
+                                        }
+                                    }
+                                    if (removeToast) {
+                                        $scope.removeToast(toast.toastId);
+                                    }
+                                }
+                            };
+                        }],
+                    template:
+                    '<div id="toast-container" ng-class="[config.position, config.animation]">' +
+                    '<div ng-repeat="toaster in toasters" class="toast" ng-class="toaster.type" ng-click="click(toaster)" ng-mouseover="stopTimer(toaster)" ng-mouseout="restartTimer(toaster)">' +
+                    '<div ng-if="toaster.showCloseButton" ng-click="click(toaster, true)" ng-bind-html="toaster.closeHtml"></div>' +
+                    '<div ng-class="config.title">{{toaster.title}}</div>' +
+                    '<div ng-class="config.message" ng-switch on="toaster.bodyOutputType">' +
+                    '<div ng-switch-when="trustedHtml" ng-bind-html="toaster.html"></div>' +
+                    '<div ng-switch-when="template"><div ng-include="toaster.bodyTemplate"></div></div>' +
+                    '<div ng-switch-when="templateWithData"><div ng-include="toaster.bodyTemplate"></div></div>' +
+                    '<div ng-switch-when="directive"><div directive-template directive-name="{{toaster.html}}" directive-data="{{toaster.directiveData}}"></div></div>' +
+                    '<div ng-switch-default >{{toaster.body}}</div>' +
+                    '</div>' +
+                    '</div>' +
+                    '</div>'
+                };
+            }]
+        );
+})(window, document);

File diff suppressed because it is too large
+ 12 - 0
static/js/toaster.min.js


Some files were not shown because too many files changed in this diff