+
+ // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
+ .directive('crmUiSelect', function ($parse, $timeout) {
+ return {
+ require: '?ngModel',
+ scope: {
+ crmUiSelect: '@'
+ },
+ link: function (scope, element, attrs, ngModel) {
+ // In cases where UI initiates update, there may be an extra
+ // call to refreshUI, but it doesn't create a cycle.
+
+ ngModel.$render = function () {
+ $timeout(function () {
+ // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
+ // new item is added before selection is made
+ $(element).select2('val', ngModel.$viewValue);
+ });
+ };
+ function refreshModel() {
+ var oldValue = ngModel.$viewValue, newValue = $(element).select2('val');
+ if (oldValue != newValue) {
+ scope.$parent.$apply(function () {
+ ngModel.$setViewValue(newValue);
+ });
+ }
+ }
+
+ function init() {
+ // TODO watch select2-options
+ var options = attrs.crmUiSelect ? scope.$parent.$eval(attrs.crmUiSelect) : {};
+ $(element).select2(options);
+ $(element).on('change', refreshModel);
+ $timeout(ngModel.$render);
+ }
+
+ init();
+ }
+ };
+ })
+
+ // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
+ // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
+ .directive('crmUiTab', function($parse) {
+ return {
+ require: '^crmUiTabSet',
+ restrict: 'EA',
+ scope: {
+ crmTitle: '@',
+ id: '@'
+ },
+ template: '<div ng-transclude></div>',
+ transclude: true,
+ link: function (scope, element, attrs, crmUiTabSetCtrl) {
+ crmUiTabSetCtrl.add(scope);
+ }
+ };
+ })
+
+ // example: <div crm-ui-tab-set><div crm-ui-tab crm-title="Tab 1">...</div><div crm-ui-tab crm-title="Tab 2">...</div></div>
+ .directive('crmUiTabSet', function() {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiTabSet: '@'
+ },
+ templateUrl: '~/crmUi/tabset.html',
+ transclude: true,
+ controllerAs: 'crmUiTabSetCtrl',
+ controller: function($scope, $parse) {
+ var tabs = $scope.tabs = []; // array<$scope>
+ this.add = function(tab) {
+ if (!tab.id) throw "Tab is missing 'id'";
+ tabs.push(tab);
+ };
+ },
+ link: function (scope, element, attrs) {}
+ };
+ })
+
+ // example: <input crm-ui-time="myobj.mytimefield" />
+ .directive('crmUiTime', function ($parse, $timeout) {
+ return {
+ restrict: 'AE',
+ scope: {
+ crmUiTime: '@'
+ },
+ link: function (scope, element, attrs) {
+ var model = $parse(attrs.crmUiTime);
+
+ element.addClass('crm-form-text six');
+ $(element).timeEntry({show24Hours: true});
+
+ 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());
+ });
+ });
+
+ updateChildren();
+ scope.$parent.$watch(attrs.crmUiTime, updateChildren);
+ element.on('change', updateParent);
+ }
+ };
+ })
+
+ // 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" />
+ // Generic, field-independent validator.
+ .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
+ // example <div crm-ui-visible="false">...content...</div>
+ .directive('crmUiVisible', function($parse) {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiVisible: '@'
+ },
+ link: function (scope, element, attrs) {
+ var model = $parse(attrs.crmUiVisible);
+ function updatecChildren() {
+ element.css('visibility', model(scope.$parent) ? 'inherit' : 'hidden');
+ }
+ updatecChildren();
+ scope.$parent.$watch(attrs.crmUiVisible, updatecChildren);
+ }
+ };
+ })
+
+ // example: <div crm-ui-wizard="myWizardCtrl"><div crm-ui-wizard-step crm-title="ts('Step 1')">...</div><div crm-ui-wizard-step crm-title="ts('Step 2')">...</div></div>
+ // Note: "myWizardCtrl" has various actions/properties like next() and $first().
+ // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
+ // WISHLIST: Allow each step to enable/disable (show/hide) itself
+ .directive('crmUiWizard', function() {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiWizard: '@'
+ },
+ templateUrl: '~/crmUi/wizard.html',
+ transclude: true,
+ controllerAs: 'crmUiWizardCtrl',
+ controller: function($scope, $parse) {
+ var steps = $scope.steps = []; // array<$scope>
+ var crmUiWizardCtrl = this;
+ var maxVisited = 0;
+ var selectedIndex = null;
+
+ var findIndex = function() {
+ var found = null;
+ angular.forEach(steps, function(step, stepKey) {
+ if (step.selected) found = stepKey;
+ });
+ return found;
+ };
+
+ /// @return int the index of the current step
+ this.$index = function() { return selectedIndex; };
+ /// @return bool whether the currentstep is first
+ 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.$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 (step === otherStep && otherKey <= maxVisited) result = true;
+ });
+ return result;
+ };
+
+ /*** @param Object step the $scope of the step */
+ this.select = function(step) {
+ angular.forEach(steps, function(otherStep, otherKey) {
+ otherStep.selected = (otherStep === step);
+ if (otherStep === step && maxVisited < otherKey) maxVisited = otherKey;
+ });
+ selectedIndex = findIndex();
+ };
+ /*** @param Object step the $scope of the step */
+ this.add = function(step) {
+ if (steps.length === 0) {
+ step.selected = true;
+ selectedIndex = 0;
+ }
+ steps.push(step);
+ steps.sort(function(a,b){
+ return a.crmUiWizardStep - b.crmUiWizardStep;
+ });
+ selectedIndex = findIndex();
+ };
+ this.remove = function(step) {
+ var key = null;
+ angular.forEach(steps, function(otherStep, otherKey) {
+ if (otherStep === step) key = otherKey;
+ });
+ if (key !== null) {
+ steps.splice(key, 1);
+ }
+ };
+ this.goto = function(index) {
+ if (index < 0) index = 0;
+ if (index >= steps.length) index = steps.length-1;
+ this.select(steps[index]);
+ };
+ 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);
+ }
+ },
+ link: function (scope, element, attrs) {}
+ };
+ })
+
+ // Use this to add extra markup to wizard
+ .directive('crmUiWizardButtons', function() {
+ return {
+ require: '^crmUiWizard',
+ restrict: 'EA',
+ scope: {},
+ template: '<span ng-transclude></span>',
+ transclude: true,
+ link: function (scope, element, attrs, crmUiWizardCtrl) {
+ var realButtonsEl = $(element).closest('.crm-wizard').find('.crm-wizard-buttons');
+ $(element).appendTo(realButtonsEl);
+ }
+ };
+ })
+
+ // 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', 'form'],
+ restrict: 'EA',
+ scope: {
+ crmTitle: '@', // expression, evaluates to a printable string
+ crmUiWizardStep: '@' // int, a weight which determines the ordering of the steps
+ },
+ template: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
+ transclude: true,
+ 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);
+ });
+ }
+ };
+ })
+