drupal-civicrm/sites/all/modules/civicrm/ang/crmUtil.js

350 lines
11 KiB
JavaScript
Raw Normal View History

2018-01-14 15:10:16 +02:00
/// 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, CRM.$, CRM._);