diff options
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.js')
-rw-r--r-- | www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.js | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.js new file mode 100644 index 00000000..ab460ab7 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUtil.js @@ -0,0 +1,361 @@ +/// 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',...)); }; + // <button ng-click="save()" ng-disabled="block.check()">Do something</button> + 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.module('crmUtil').factory('crmLoadScript', function($q) { + return function(url) { + var deferred = $q.defer(); + + CRM.loadScript(url).done(function() { + deferred.resolve(true); + }); + + return deferred.promise; + }; + }); + +})(angular, CRM.$, CRM._); |