/// crmUi: Sundry UI helpers (function (angular, $, _) { angular.module('crmUtil', CRM.angRequires('crmUtil')); // Angular implementation of CRM.api3 // @link http://wiki.civicrm.org/confluence/display/CRMDOC/AJAX+Interface#AJAXInterface-CRM.api3 // // Note: To mock API results in unit-tests, override crmApi.backend, e.g. // var apiSpy = jasmine.createSpy('crmApi'); // crmApi.backend = apiSpy.and.returnValue(crmApi.val({ // is_error: 1 // })); angular.module('crmUtil').factory('crmApi', function($q) { var crmApi = function(entity, action, params, message) { // JSON serialization in CRM.api3 is not aware of Angular metadata like $$hash, so use angular.toJson() var deferred = $q.defer(); var p; var backend = crmApi.backend || CRM.api3; if (params && params.body_html) { // CRM-18474 - remove Unicode Character 'LINE SEPARATOR' (U+2028) // and 'PARAGRAPH SEPARATOR' (U+2029) from the html if present. params.body_html = params.body_html.replace(/([\u2028]|[\u2029])/g, '\n'); } if (_.isObject(entity)) { // eval content is locally generated. /*jshint -W061 */ p = backend(eval('('+angular.toJson(entity)+')'), action); } else { // eval content is locally generated. /*jshint -W061 */ p = backend(entity, action, eval('('+angular.toJson(params)+')'), message); } // CRM.api3 returns a promise, but the promise doesn't really represent errors as errors, so we // convert them p.then( function(result) { if (result.is_error) { deferred.reject(result); } else { deferred.resolve(result); } }, function(error) { deferred.reject(error); } ); return deferred.promise; }; crmApi.backend = null; crmApi.val = function(value) { var d = $.Deferred(); d.resolve(value); return d.promise(); }; return crmApi; }); // Get and cache the metadata for an API entity. // usage: // $q.when(crmMetadata.getFields('MyEntity'), function(fields){ // console.log('The fields are:', options); // }); angular.module('crmUtil').factory('crmMetadata', function($q, crmApi) { // Convert {key:$,value:$} sequence to unordered {$key: $value} map. function convertOptionsToMap(options) { var result = {}; angular.forEach(options, function(o) { result[o.key] = o.value; }); return result; } var cache = {}; // cache[entityName+'::'+action][fieldName].title var deferreds = {}; // deferreds[cacheKey].push($q.defer()) var crmMetadata = { // usage: $q.when(crmMetadata.getField('MyEntity', 'my_field')).then(...); getField: function getField(entity, field) { return $q.when(crmMetadata.getFields(entity)).then(function(fields){ return fields[field]; }); }, // usage: $q.when(crmMetadata.getFields('MyEntity')).then(...); // usage: $q.when(crmMetadata.getFields(['MyEntity', 'myaction'])).then(...); getFields: function getFields(entity) { var action = '', cacheKey; if (_.isArray(entity)) { action = entity[1]; entity = entity[0]; cacheKey = entity + '::' + action; } else { cacheKey = entity; } if (_.isObject(cache[cacheKey])) { return cache[cacheKey]; } var needFetch = _.isEmpty(deferreds[cacheKey]); deferreds[cacheKey] = deferreds[cacheKey] || []; var deferred = $q.defer(); deferreds[cacheKey].push(deferred); if (needFetch) { crmApi(entity, 'getfields', {action: action, sequential: 1, options: {get_options: 'all'}}) .then( // on success: function(fields) { cache[cacheKey] = _.indexBy(fields.values, 'name'); angular.forEach(cache[cacheKey],function (field){ if (field.options) { field.optionsMap = convertOptionsToMap(field.options); } }); angular.forEach(deferreds[cacheKey], function(dfr) { dfr.resolve(cache[cacheKey]); }); delete deferreds[cacheKey]; }, // on error: function() { cache[cacheKey] = {}; // cache nack angular.forEach(deferreds[cacheKey], function(dfr) { dfr.reject(); }); delete deferreds[cacheKey]; } ); } return deferred.promise; } }; return crmMetadata; }); // usage: // var block = $scope.block = crmBlocker(); // $scope.save = function() { return block(crmApi('MyEntity','create',...)); }; // angular.module('crmUtil').factory('crmBlocker', function() { return function() { var blocks = 0; var result = function(promise) { blocks++; return promise.finally(function() { blocks--; }); }; result.check = function() { return blocks > 0; }; return result; }; }); angular.module('crmUtil').factory('crmLegacy', function() { return CRM; }); // example: scope.$watch('foo', crmLog.wrap(function(newValue, oldValue){ ... })); angular.module('crmUtil').factory('crmLog', function(){ var level = 0; var write = console.log; function indent() { var s = '>'; for (var i = 0; i < level; i++) s = s + ' '; return s; } var crmLog = { log: function(msg, vars) { write(indent() + msg, vars); }, wrap: function(label, f) { return function(){ level++; crmLog.log(label + ": start", arguments); var r; try { r = f.apply(this, arguments); } finally { crmLog.log(label + ": end"); level--; } return r; }; } }; return crmLog; }); angular.module('crmUtil').factory('crmNavigator', ['$window', function($window) { return { redirect: function(path) { $window.location.href = path; } }; }]); // Wrap an async function in a queue, ensuring that independent async calls are issued in strict sequence. // usage: qApi = crmQueue(crmApi); qApi(entity,action,...).then(...); qApi(entity2,action2,...).then(...); // This is similar to promise-chaining, but allows chaining independent procs (without explicitly sharing promises). angular.module('crmUtil').factory('crmQueue', function($q) { // @param worker A function which generates promises return function crmQueue(worker) { var queue = []; function next() { var task = queue[0]; worker.apply(null, task.a).then( function onOk(data) { queue.shift(); task.dfr.resolve(data); if (queue.length > 0) next(); }, function onErr(err) { queue.shift(); task.dfr.reject(err); if (queue.length > 0) next(); } ); } function enqueue() { var dfr = $q.defer(); queue.push({a: arguments, dfr: dfr}); if (queue.length === 1) { next(); } return dfr.promise; } return enqueue; }; }); // Adapter for CRM.status which supports Angular promises (instead of jQuery promises) // example: crmStatus('Saving', crmApi(...)).then(function(result){...}) angular.module('crmUtil').factory('crmStatus', function($q){ return function(options, aPromise){ if (aPromise) { return CRM.toAPromise($q, CRM.status(options, CRM.toJqPromise(aPromise))); } else { return CRM.toAPromise($q, CRM.status(options)); } }; }); // crmWatcher allows one to setup event listeners and temporarily suspend // them en masse. // // example: // angular.controller(... function($scope, crmWatcher){ // var watcher = crmWatcher(); // function myfunc() { // watcher.suspend('foo', function(){ // ...do stuff... // }); // } // watcher.setup('foo', function(){ // return [ // $scope.$watch('foo', myfunc), // $scope.$watch('bar', myfunc), // $scope.$watch('whiz', otherfunc) // ]; // }); // }); angular.module('crmUtil').factory('crmWatcher', function(){ return function() { var unwatches = {}, watchFactories = {}, suspends = {}; // Specify the list of watches this.setup = function(name, newWatchFactory) { watchFactories[name] = newWatchFactory; unwatches[name] = watchFactories[name](); suspends[name] = 0; return this; }; // Temporarily disable watches and run some logic this.suspend = function(name, f) { suspends[name]++; this.teardown(name); var r; try { r = f.apply(this, []); } finally { if (suspends[name] === 1) { unwatches[name] = watchFactories[name](); if (!angular.isArray(unwatches[name])) { unwatches[name] = [unwatches[name]]; } } suspends[name]--; } return r; }; this.teardown = function(name) { if (!unwatches[name]) return; _.each(unwatches[name], function(unwatch){ unwatch(); }); delete unwatches[name]; }; return this; }; }); // Run a given function. If it is already running, wait for it to finish before running again. // If multiple requests are made before the first request finishes, all but the last will be ignored. // This prevents overwhelming the server with redundant queries during e.g. an autocomplete search while the user types. // Given function should return an angular promise. crmThrottle will deliver the contents when resolved. angular.module('crmUtil').factory('crmThrottle', function($q) { var pending = [], executing = []; return function(func) { var deferred = $q.defer(); function checkResult(result, success) { _.pull(executing, func); if (_.includes(pending, func)) { runNext(); } else if (success) { deferred.resolve(result); } else { deferred.reject(result); } } function runNext() { executing.push(func); _.pull(pending, func); func().then(function(result) { checkResult(result, true); }, function(result) { checkResult(result, false); }); } if (!_.includes(executing, func)) { runNext(); } else if (!_.includes(pending, func)) { pending.push(func); } return deferred.promise; }; }); })(angular, CRM.$, CRM._);