Attachment API, DynamicFKAuthorization - Support for custom fields and contacts.
[civicrm-core.git] / js / angular-crm-ui.js
index f38e20801126bfe212a5e3e038b9eaea851e621d..d9f590678aefecb2c1b60a95790bce78b6f54d7e 100644 (file)
@@ -3,10 +3,6 @@
 
   var uidCount = 0;
 
-  var partialUrl = function (relPath) {
-    return CRM.resourceUrls['civicrm'] + '/partials/crmUi/' + relPath;
-  };
-
   angular.module('crmUi', [])
 
     // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
       };
     })
 
-    // example: <input crm-ui-date="myobj.datefield" />
-    // example: <input crm-ui-date="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
+    // Display a date widget.
+    // example: <input crm-ui-date ng-model="myobj.datefield" />
+    // example: <input crm-ui-date ng-model="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
     .directive('crmUiDate', function ($parse, $timeout) {
       return {
         restrict: 'AE',
+        require: 'ngModel',
         scope: {
-          crmUiDate: '@', // expression, model binding
           crmUiDateFormat: '@' // expression, date format (default: "yy-mm-dd")
         },
-        link: function (scope, element, attrs) {
+        link: function (scope, element, attrs, ngModel) {
           var fmt = attrs.crmUiDateFormat ? $parse(attrs.crmUiDateFormat)() : "yy-mm-dd";
-          var model = $parse(attrs.crmUiDate);
 
           element.addClass('dateplugin');
           $(element).datepicker({
             dateFormat: fmt
           });
 
-          var updateChildren = (function() {
-            element.off('change', updateParent);
-            $(element).datepicker('setDate', model(scope.$parent));
-            element.on('change', updateParent);
-          });
+          ngModel.$render = function $render() {
+            $(element).datepicker('setDate', ngModel.$viewValue);
+          };
           var updateParent = (function() {
             $timeout(function () {
-              model.assign(scope.$parent, $(element).val());
+              ngModel.$setViewValue(element.val());
             });
           });
 
-          updateChildren();
-          scope.$parent.$watch(attrs.crmUiDate, updateChildren);
           element.on('change', updateParent);
         }
       };
     })
 
-    // example: <div crm-ui-date-time="myobj.mydatetimefield"></div>
+    // Display a date-time widget.
+    // example: <div crm-ui-date-time ng-model="myobj.mydatetimefield"></div>
     .directive('crmUiDateTime', function ($parse) {
       return {
         restrict: 'AE',
+        require: 'ngModel',
         scope: {
-          crmUiDateTime: '@'
+          ngRequired: '@'
         },
-        template: '<input crm-ui-date="dtparts.date" placeholder="{{dateLabel}}"/> <input crm-ui-time="dtparts.time" placeholder="{{timeLabel}}"/>',
-        link: function (scope, element, attrs) {
-          var model = $parse(attrs.crmUiDateTime);
+        templateUrl: '~/crmUi/datetime.html',
+        link: function (scope, element, attrs, ngModel) {
+          var ts = scope.ts = CRM.ts(null);
           scope.dateLabel = ts('Date');
           scope.timeLabel = ts('Time');
+          element.addClass('crm-ui-datetime');
 
-          var updateChildren = (function () {
-            var value = model(scope.$parent);
-            if (value) {
-              var dtparts = value.split(/ /);
+          ngModel.$render = function $render() {
+            if (!_.isEmpty(ngModel.$viewValue)) {
+              var dtparts = ngModel.$viewValue.split(/ /);
               scope.dtparts = {date: dtparts[0], time: dtparts[1]};
             }
             else {
               scope.dtparts = {date: '', time: ''};
             }
-          });
-          var updateParent = (function () {
-            model.assign(scope.$parent, scope.dtparts.date + " " + scope.dtparts.time);
-          });
+          };
+
+          function updateParent() {
+            var incompleteDateTime = _.isEmpty(scope.dtparts.date) ^ _.isEmpty(scope.dtparts.time);
+            ngModel.$setValidity('incompleteDateTime', !incompleteDateTime);
 
-          updateChildren();
-          scope.$parent.$watch(attrs.crmUiDateTime, updateChildren);
-          scope.$watch('dtparts.date', updateParent),
-          scope.$watch('dtparts.time', updateParent)
+            if (_.isEmpty(scope.dtparts.date) && _.isEmpty(scope.dtparts.time)) {
+              ngModel.$setViewValue(' ');
+            }
+            else {
+              //ngModel.$setViewValue(scope.dtparts.date + ' ' + scope.dtparts.time);
+              ngModel.$setViewValue((scope.dtparts.date ? scope.dtparts.date : '') + ' ' + (scope.dtparts.time ? scope.dtparts.time : ''));
+            }
+          }
+
+          scope.$watch('dtparts.date', updateParent);
+          scope.$watch('dtparts.time', updateParent);
+
+          function updateRequired() {
+            scope.required = scope.$parent.$eval(attrs.ngRequired);
+          }
+
+          if (attrs.ngRequired) {
+            updateRequired();
+            scope.$parent.$watch(attrs.ngRequired, updateRequired);
+          }
+
+          scope.reset = function reset() {
+            scope.dtparts = {date: '', time: ''};
+            ngModel.$setViewValue('');
+          };
         }
       };
     })
     .directive('crmUiField', function() {
       // Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
       var templateUrls = {
-        default: partialUrl('field.html'),
-        checkbox: partialUrl('field-cb.html')
+        default: '~/crmUi/field.html',
+        checkbox: '~/crmUi/field-cb.html'
       };
 
       return {
           // immediately for initialization. Use retries/retryDelay to initialize such elements.
           var init = function (retries, retryDelay) {
             var input = $('#' + id);
-            if (input.length == 0) {
+            if (input.length === 0) {
               if (retries) {
                 $timeout(function(){
                   init(retries-1, retryDelay);
               });
             }
             else {
-              scope.crmIsRequired = input.prop('required')
+              scope.crmIsRequired = input.prop('required');
             }
 
             ngModel = $parse(attrs.crmUiFor)(tgtScope);
       };
     })
 
+    // Define a scope in which a name like "subform.foo" maps to a unique ID.
     // example: <div ng-form="subform" crm-ui-id-scope><label crm-ui-for="subform.foo">Foo:</label><input crm-ui-id="subform.foo" name="foo"/></div>
     .directive('crmUiIdScope', function () {
       return {
               ids[name] = "crmUiId_" + (++uidCount);
             }
             return ids[name];
-          }
+          };
         },
         link: function (scope, element, attrs) {}
       };
     })
 
+    // Display an HTML blurb inside an IFRAME.
     // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
     .directive('crmUiIframe', function ($parse) {
       return {
             doc.open();
             doc.writeln(iframeHtml);
             doc.close();
-          }
+          };
 
           scope.$parent.$watch(attrs.crmUiIframe, refresh);
-          //setTimeout(function () { refresh(); }, 50);
         }
       };
     })
 
+    // Define a rich text editor.
     // example: <textarea crm-ui-id="myForm.body_html" crm-ui-richtext name="body_html" ng-model="mailing.body_html"></textarea>
+    // WISHLIST: use ngModel
     .directive('crmUiRichtext', function ($timeout) {
       return {
         require: '?ngModel',
       };
     })
 
+    // Display a lock icon (based on a boolean).
     // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
     // example: <a crm-ui-lock
     //            binding="mymodel.boolfield"
       var defaultVal = function (defaultValue) {
         var f = function (scope) {
           return defaultValue;
-        }
+        };
         f.assign = function (scope, value) {
           // ignore changes
-        }
+        };
         return f;
       };
 
       return {
         template: '',
         link: function (scope, element, attrs) {
-          var binding = parse(attrs['binding'], true);
-          var titleLocked = parse(attrs['titleLocked'], ts('Locked'));
-          var titleUnlocked = parse(attrs['titleUnlocked'], ts('Unlocked'));
+          var binding = parse(attrs.binding, true);
+          var titleLocked = parse(attrs.titleLocked, ts('Locked'));
+          var titleUnlocked = parse(attrs.titleUnlocked, ts('Unlocked'));
 
           $(element).addClass('ui-icon lock-button');
           var refresh = function () {
       };
     })
 
+    // Display a fancy SELECT (based on select2).
     // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
     .directive('crmUiSelect', function ($parse, $timeout) {
       return {
         scope: {
           crmUiTabSet: '@'
         },
-        templateUrl: partialUrl('tabset.html'),
+        templateUrl: '~/crmUi/tabset.html',
         transclude: true,
         controllerAs: 'crmUiTabSetCtrl',
         controller: function($scope, $parse) {
       };
     })
 
-    // example: <input crm-ui-time="myobj.mytimefield" />
+    // Display a time-entry field.
+    // example: <input crm-ui-time ng-model="myobj.mytimefield" />
     .directive('crmUiTime', function ($parse, $timeout) {
       return {
         restrict: 'AE',
+        require: 'ngModel',
         scope: {
-          crmUiTime: '@'
         },
-        link: function (scope, element, attrs) {
-          var model = $parse(attrs.crmUiTime);
-
+        link: function (scope, element, attrs, ngModel) {
           element.addClass('crm-form-text six');
-          $(element).timeEntry({show24Hours: true});
+          element.timeEntry({show24Hours: true});
+
+          ngModel.$render = function $render() {
+            element.timeEntry('setTime', ngModel.$viewValue);
+          };
 
-          var updateChildren = (function() {
-            element.off('change', updateParent);
-            $(element).timeEntry('setTime', model(scope.$parent));
-            element.on('change', updateParent);
-          });
           var updateParent = (function () {
             $timeout(function () {
-              model.assign(scope.$parent, element.val());
+              ngModel.$setViewValue(element.val());
             });
           });
-
-          updateChildren();
-          scope.$parent.$watch(attrs.crmUiTime, updateChildren);
           element.on('change', updateParent);
         }
-      }
+      };
+    })
+
+    // Generic, field-independent form validator.
+    // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" />
+    // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" crm-ui-validate-name="myError" />
+    .directive('crmUiValidate', function() {
+      return {
+        restrict: 'EA',
+        require: 'ngModel',
+        link: function(scope, element, attrs, ngModel) {
+          var validationKey = attrs.crmUiValidateName ? attrs.crmUiValidateName : 'crmUiValidate';
+          scope.$watch(attrs.crmUiValidate, function(newValue){
+            ngModel.$setValidity(validationKey, !!newValue);
+          });
+        }
+      };
     })
 
     // like ng-show, but hides/displays elements using "visibility" which maintains positioning
         scope: {
           crmUiWizard: '@'
         },
-        templateUrl: partialUrl('wizard.html'),
+        templateUrl: '~/crmUi/wizard.html',
         transclude: true,
         controllerAs: 'crmUiWizardCtrl',
         controller: function($scope, $parse) {
           this.$first = function() { return this.$index() === 0; };
           /// @return bool whether the current step is last
           this.$last = function() { return this.$index() === steps.length -1; };
-          this.$maxVisit = function() { return maxVisited; }
+          this.$maxVisit = function() { return maxVisited; };
+          this.$validStep = function() {
+            return steps[selectedIndex].isStepValid();
+          };
           this.iconFor = function(index) {
             if (index < this.$index()) return '√';
             if (index === this.$index()) return '»';
             return ' ';
-          }
+          };
           this.isSelectable = function(step) {
             if (step.selected) return false;
             var result = false;
             angular.forEach(steps, function(otherStep, otherKey) {
               if (otherStep === step) key = otherKey;
             });
-            if (key != null) {
+            if (key !== null) {
               steps.splice(key, 1);
             }
           };
           this.previous = function() { this.goto(this.$index()-1); };
           this.next = function() { this.goto(this.$index()+1); };
           if ($scope.crmUiWizard) {
-            $parse($scope.crmUiWizard).assign($scope.$parent, this)
+            $parse($scope.crmUiWizard).assign($scope.$parent, this);
           }
         },
         link: function (scope, element, attrs) {}
       };
     })
 
-    // example: <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
+    // example: <div crm-ui-wizard-step crm-title="ts('My Title')" ng-form="mySubForm">...content...</div>
     // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering.
     // example: <div crm-ui-wizard-step="100" crm-title="..." ng-if="...">...content...</div>
     .directive('crmUiWizardStep', function() {
       var nextWeight = 1;
       return {
-        require: '^crmUiWizard',
+        require: ['^crmUiWizard', 'form'],
         restrict: 'EA',
         scope: {
           crmTitle: '@', // expression, evaluates to a printable string
         },
         template: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
         transclude: true,
-        link: function (scope, element, attrs, crmUiWizardCtrl) {
+        link: function (scope, element, attrs, ctrls) {
+          var crmUiWizardCtrl = ctrls[0], form = ctrls[1];
           if (scope.crmUiWizardStep) {
             scope.crmUiWizardStep = parseInt(scope.crmUiWizardStep);
           } else {
             scope.crmUiWizardStep = nextWeight++;
           }
+          scope.isStepValid = function() {
+            return form.$valid;
+          };
           crmUiWizardCtrl.add(scope);
           element.on('$destroy', function(){
             crmUiWizardCtrl.remove(scope);
         template: '',
         link: function (scope, element, attrs) {
           $(element).click(function () {
-            var options = scope.$eval(attrs['crmConfirm']);
+            var options = scope.$eval(attrs.crmConfirm);
             var defaults = (options.type) ? defaultFuncs[options.type](options) : {};
             CRM.confirm(_.extend(defaults, options))
-              .on('crmConfirm:yes', function () { scope.$apply(attrs['onYes']); })
-              .on('crmConfirm:no', function () { scope.$apply(attrs['onNo']); });
+              .on('crmConfirm:yes', function () { scope.$apply(attrs.onYes); })
+              .on('crmConfirm:no', function () { scope.$apply(attrs.onNo); });
           });
         }
       };