Merge remote-tracking branch 'upstream/4.4' into 4.4-4.5-2014-09-08-20-42-29
[civicrm-core.git] / js / angular-crm-ui.js
1 /// crmUi: Sundry UI helpers
2 (function (angular, $, _) {
3 var idCount = 0;
4
5 angular.module('crmUi', [])
6 // example: <form name="myForm">...<label crm-ui-label crm-for="myField">My Field</span>...<input name="myField"/>...</form>
7 //
8 // Label adapts based on <input required>, <input ng-required>, or any other validation.
9 //
10 // Note: This should work in the normal case where <label> and <input> are in roughly the same scope,
11 // but if the scopes are materially different then problems could arise.
12 .directive('crmUiLabel', function($parse) {
13 return {
14 scope: {
15 name: '@'
16 },
17 transclude: true,
18 template: '<span ng-class="cssClasses"><span ng-transclude></span> <span ng-show="crmRequired" class="crm-marker" title="This field is required.">*</span></span>',
19 link: function(scope, element, attrs) {
20 if (attrs.crmFor == 'name') {
21 throw new Error('Validation monitoring does not work for field name "name"');
22 }
23
24 // 1. Figure out form and input elements
25
26 var form = $(element).closest('form');
27 var formCtrl = scope.$parent.$eval(form.attr('name'));
28 var input = $('input[name="' + attrs.crmFor + '"],select[name="' + attrs.crmFor + '"],textarea[name="' + attrs.crmFor + '"]', form);
29 if (form.length != 1 || input.length != 1) {
30 if (console.log) console.log('Label cannot be matched to input element. Expected to find one form and one input.', form.length, input.length);
31 return;
32 }
33
34 // 2. Make sure that inputs are well-defined (with name+id).
35
36 if (!input.attr('id')) {
37 input.attr('id', 'crmUi_' + (++idCount));
38 }
39 $(element).attr('for', input.attr('id'));
40
41 // 3. Monitor is the "required" and "$valid" properties
42
43 if (input.attr('ng-required')) {
44 scope.crmRequired = scope.$parent.$eval(input.attr('ng-required'));
45 scope.$parent.$watch(input.attr('ng-required'), function(isRequired) {
46 scope.crmRequired = isRequired;
47 });
48 } else {
49 scope.crmRequired = input.prop('required');
50 }
51
52 var inputCtrl = form.attr('name') + '.' + input.attr('name');
53 scope.cssClasses = {};
54 scope.$parent.$watch(inputCtrl + '.$valid', function(newValue) {
55 //scope.cssClasses['ng-valid'] = newValue;
56 //scope.cssClasses['ng-invalid'] = !newValue;
57 scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
58 });
59 scope.$parent.$watch(inputCtrl + '.$pristine', function(newValue) {
60 //scope.cssClasses['ng-pristine'] = newValue;
61 //scope.cssClasses['ng-dirty'] = !newValue;
62 scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
63 });
64
65 }
66 };
67 })
68
69 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
70 // example: <a crm-ui-lock
71 // binding="mymodel.boolfield"
72 // title-locked="ts('Boolfield is locked')"
73 // title-unlocked="ts('Boolfield is unlocked')"></a>
74 .directive('crmUiLock', function ($parse, $rootScope) {
75 var defaultVal = function (defaultValue) {
76 var f = function (scope) {
77 return defaultValue;
78 }
79 f.assign = function (scope, value) {
80 // ignore changes
81 }
82 return f;
83 };
84
85 // like $parse, but accepts a defaultValue in case expr is undefined
86 var parse = function (expr, defaultValue) {
87 return expr ? $parse(expr) : defaultVal(defaultValue);
88 };
89
90 return {
91 template: '',
92 link: function (scope, element, attrs) {
93 var binding = parse(attrs['binding'], true);
94 var titleLocked = parse(attrs['titleLocked'], ts('Locked'));
95 var titleUnlocked = parse(attrs['titleUnlocked'], ts('Unlocked'));
96
97 $(element).addClass('ui-icon lock-button');
98 var refresh = function () {
99 var locked = binding(scope);
100 if (locked) {
101 $(element)
102 .removeClass('ui-icon-unlocked')
103 .addClass('ui-icon-locked')
104 .prop('title', titleLocked(scope))
105 ;
106 }
107 else {
108 $(element)
109 .removeClass('ui-icon-locked')
110 .addClass('ui-icon-unlocked')
111 .prop('title', titleUnlocked(scope))
112 ;
113 }
114 };
115
116 $(element).click(function () {
117 binding.assign(scope, !binding(scope));
118 //scope.$digest();
119 $rootScope.$digest();
120 });
121
122 scope.$watch(attrs.binding, refresh);
123 scope.$watch(attrs.titleLocked, refresh);
124 scope.$watch(attrs.titleUnlocked, refresh);
125
126 refresh();
127 }
128 };
129 })
130 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
131 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
132 .directive('crmConfirm', function () {
133 // Helpers to calculate default options for CRM.confirm()
134 var defaultFuncs = {
135 'disable': function (options) {
136 return {
137 message: ts('Are you sure you want to disable this?'),
138 options: {no: ts('Cancel'), yes: ts('Disable')},
139 width: 300,
140 title: ts('Disable %1?', {
141 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
142 })
143 };
144 },
145 'revert': function (options) {
146 return {
147 message: ts('Are you sure you want to revert this?'),
148 options: {no: ts('Cancel'), yes: ts('Revert')},
149 width: 300,
150 title: ts('Revert %1?', {
151 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
152 })
153 };
154 },
155 'delete': function (options) {
156 return {
157 message: ts('Are you sure you want to delete this?'),
158 options: {no: ts('Cancel'), yes: ts('Delete')},
159 width: 300,
160 title: ts('Delete %1?', {
161 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
162 })
163 };
164 }
165 };
166 return {
167 template: '',
168 link: function (scope, element, attrs) {
169 $(element).click(function () {
170 var options = scope.$eval(attrs['crmConfirm']);
171 var defaults = (options.type) ? defaultFuncs[options.type](options) : {};
172 CRM.confirm(_.extend(defaults, options))
173 .on('crmConfirm:yes', function () { scope.$apply(attrs['onYes']); })
174 .on('crmConfirm:no', function () { scope.$apply(attrs['onNo']); });
175 });
176 }
177 };
178 })
179 .run(function($rootScope, $location) {
180 /// Example: <button ng-click="goto('home')">Go home!</button>
181 $rootScope.goto = function(path) {
182 $location.path(path);
183 };
184 // useful for debugging: $rootScope.log = console.log || function() {};
185 })
186 ;
187
188 })(angular, CRM.$, CRM._);