text: /// crmUi: Sundry UI helpers
  (function (angular, $, _) {
    angular.module('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._);
  