Merge pull request #5457 from colemanw/CRM-15705
[civicrm-core.git] / js / angular-crm-util.js
1 /// crmUi: Sundry UI helpers
2 (function (angular, $, _) {
3 angular.module('crmUtil', []);
4
5 // usage:
6 // crmApi('Entity', 'action', {...}).then(function(apiResult){...})
7 //
8 // Note: To mock API results in unit-tests, override crmApi.backend, e.g.
9 // var apiSpy = jasmine.createSpy('crmApi');
10 // crmApi.backend = apiSpy.and.returnValue(crmApi.val({
11 // is_error: 1
12 // }));
13 angular.module('crmUtil').factory('crmApi', function($q) {
14 var crmApi = function(entity, action, params, message) {
15 // JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash, so use angular.toJson()
16 var deferred = $q.defer();
17 var p;
18 var backend = crmApi.backend || CRM.api3;
19 if (_.isObject(entity)) {
20 // eval content is locally generated.
21 /*jshint -W061 */
22 p = backend(eval('('+angular.toJson(entity)+')'), message);
23 } else {
24 // eval content is locally generated.
25 /*jshint -W061 */
26 p = backend(entity, action, eval('('+angular.toJson(params)+')'), message);
27 }
28 // CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we
29 // convert them
30 p.then(
31 function(result) {
32 if (result.is_error) {
33 deferred.reject(result);
34 } else {
35 deferred.resolve(result);
36 }
37 },
38 function(error) {
39 deferred.reject(error);
40 }
41 );
42 return deferred.promise;
43 };
44 crmApi.backend = null;
45 crmApi.val = function(value) {
46 var d = $.Deferred();
47 d.resolve(value);
48 return d.promise();
49 };
50 return crmApi;
51 });
52
53 // Get and cache the metadata for an API entity.
54 // usage:
55 // $q.when(crmMetadata.getFields('MyEntity'), function(fields){
56 // console.log('The fields are:', options);
57 // });
58 angular.module('crmUtil').factory('crmMetadata', function($q, crmApi) {
59
60 // Convert {key:$,value:$} sequence to unordered {$key: $value} map.
61 function convertOptionsToMap(options) {
62 var result = {};
63 angular.forEach(options, function(o) {
64 result[o.key] = o.value;
65 });
66 return result;
67 }
68
69 var cache = {}; // cache[entityName+'::'+action][fieldName].title
70 var deferreds = {}; // deferreds[cacheKey].push($q.defer())
71 var crmMetadata = {
72 // usage: $q.when(crmMetadata.getField('MyEntity', 'my_field')).then(...);
73 getField: function getField(entity, field) {
74 return $q.when(crmMetadata.getFields(entity)).then(function(fields){
75 return fields[field];
76 });
77 },
78 // usage: $q.when(crmMetadata.getFields('MyEntity')).then(...);
79 // usage: $q.when(crmMetadata.getFields(['MyEntity', 'myaction'])).then(...);
80 getFields: function getFields(entity) {
81 var action = '', cacheKey;
82 if (_.isArray(entity)) {
83 action = entity[1];
84 entity = entity[0];
85 cacheKey = entity + '::' + action;
86 } else {
87 cacheKey = entity;
88 }
89
90 if (_.isObject(cache[cacheKey])) {
91 return cache[cacheKey];
92 }
93
94 var needFetch = _.isEmpty(deferreds[cacheKey]);
95 deferreds[cacheKey] = deferreds[cacheKey] || [];
96 var deferred = $q.defer();
97 deferreds[cacheKey].push(deferred);
98
99 if (needFetch) {
100 crmApi(entity, 'getfields', {action: action, sequential: 1, options: {get_options: 'all'}})
101 .then(
102 // on success:
103 function(fields) {
104 cache[cacheKey] = _.indexBy(fields.values, 'name');
105 angular.forEach(cache[cacheKey],function (field){
106 if (field.options) {
107 field.optionsMap = convertOptionsToMap(field.options);
108 }
109 });
110 angular.forEach(deferreds[cacheKey], function(dfr) {
111 dfr.resolve(cache[cacheKey]);
112 });
113 delete deferreds[cacheKey];
114 },
115 // on error:
116 function() {
117 cache[cacheKey] = {}; // cache nack
118 angular.forEach(deferreds[cacheKey], function(dfr) {
119 dfr.reject();
120 });
121 delete deferreds[cacheKey];
122 }
123 );
124 }
125
126 return deferred.promise;
127 }
128 };
129
130 return crmMetadata;
131 });
132
133 // usage:
134 // var block = $scope.block = crmBlocker();
135 // $scope.save = function() { return block(crmApi('MyEntity','create',...)); };
136 // <button ng-click="save()" ng-disabled="block.check()">Do something</button>
137 angular.module('crmUtil').factory('crmBlocker', function() {
138 return function() {
139 var blocks = 0;
140 var result = function(promise) {
141 blocks++;
142 return promise.finally(function() {
143 blocks--;
144 });
145 };
146 result.check = function() {
147 return blocks > 0;
148 };
149 return result;
150 };
151 });
152
153 angular.module('crmUtil').factory('crmLegacy', function() {
154 return CRM;
155 });
156
157 // example: scope.$watch('foo', crmLog.wrap(function(newValue, oldValue){ ... }));
158 angular.module('crmUtil').factory('crmLog', function(){
159 var level = 0;
160 var write = console.log;
161 function indent() {
162 var s = '>';
163 for (var i = 0; i < level; i++) s = s + ' ';
164 return s;
165 }
166 var crmLog = {
167 log: function(msg, vars) {
168 write(indent() + msg, vars);
169 },
170 wrap: function(label, f) {
171 return function(){
172 level++;
173 crmLog.log(label + ": start", arguments);
174 var r;
175 try {
176 r = f.apply(this, arguments);
177 } finally {
178 crmLog.log(label + ": end");
179 level--;
180 }
181 return r;
182 };
183 }
184 };
185 return crmLog;
186 });
187
188 angular.module('crmUtil').factory('crmNavigator', ['$window', function($window) {
189 return {
190 redirect: function(path) {
191 $window.location.href = path;
192 }
193 };
194 }]);
195
196 angular.module('crmUtil').factory('crmNow', function($q){
197 // FIXME: surely there's already some helper which can do this in one line?
198 // @return string "YYYY-MM-DD hh:mm:ss"
199 return function crmNow() {
200 var currentdate = new Date();
201 var yyyy = currentdate.getFullYear();
202 var mm = currentdate.getMonth() + 1;
203 mm = mm < 10 ? '0' + mm : mm;
204 var dd = currentdate.getDate();
205 dd = dd < 10 ? '0' + dd : dd;
206 var hh = currentdate.getHours();
207 hh = hh < 10 ? '0' + hh : hh;
208 var min = currentdate.getMinutes();
209 min = min < 10 ? '0' + min : min;
210 var sec = currentdate.getSeconds();
211 sec = sec < 10 ? '0' + sec : sec;
212 return yyyy + "-" + mm + "-" + dd + " " + hh + ":" + min + ":" + sec;
213 };
214 });
215
216 // Adapter for CRM.status which supports Angular promises (instead of jQuery promises)
217 // example: crmStatus('Saving', crmApi(...)).then(function(result){...})
218 angular.module('crmUtil').factory('crmStatus', function($q){
219 return function(options, aPromise){
220 if (aPromise) {
221 return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise)));
222 } else {
223 return CRM.toAPromise($q, CRM.status(options));
224 }
225 };
226 });
227
228 // crmWatcher allows one to setup event listeners and temporarily suspend
229 // them en masse.
230 //
231 // example:
232 // angular.controller(... function($scope, crmWatcher){
233 // var watcher = crmWatcher();
234 // function myfunc() {
235 // watcher.suspend('foo', function(){
236 // ...do stuff...
237 // });
238 // }
239 // watcher.setup('foo', function(){
240 // return [
241 // $scope.$watch('foo', myfunc),
242 // $scope.$watch('bar', myfunc),
243 // $scope.$watch('whiz', otherfunc)
244 // ];
245 // });
246 // });
247 angular.module('crmUtil').factory('crmWatcher', function(){
248 return function() {
249 var unwatches = {}, watchFactories = {}, suspends = {};
250
251 // Specify the list of watches
252 this.setup = function(name, newWatchFactory) {
253 watchFactories[name] = newWatchFactory;
254 unwatches[name] = watchFactories[name]();
255 suspends[name] = 0;
256 return this;
257 };
258
259 // Temporarily disable watches and run some logic
260 this.suspend = function(name, f) {
261 suspends[name]++;
262 this.teardown(name);
263 var r;
264 try {
265 r = f.apply(this, []);
266 } finally {
267 if (suspends[name] === 1) {
268 unwatches[name] = watchFactories[name]();
269 if (!angular.isArray(unwatches[name])) {
270 unwatches[name] = [unwatches[name]];
271 }
272 }
273 suspends[name]--;
274 }
275 return r;
276 };
277
278 this.teardown = function(name) {
279 if (!unwatches[name]) return;
280 _.each(unwatches[name], function(unwatch){
281 unwatch();
282 });
283 delete unwatches[name];
284 };
285
286 return this;
287 };
288 });
289
290 })(angular, CRM.$, CRM._);