summaryrefslogtreecommitdiff
path: root/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js
diff options
context:
space:
mode:
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js')
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js1092
1 files changed, 1092 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js
new file mode 100644
index 00000000..0b09c60d
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js
@@ -0,0 +1,1092 @@
+/// crmUi: Sundry UI helpers
+(function (angular, $, _) {
+
+ var uidCount = 0,
+ pageTitle = 'CiviCRM',
+ documentTitle = 'CiviCRM';
+
+ angular.module('crmUi', CRM.angRequires('crmUi'))
+
+ // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
+ // WISHLIST: crmCollapsed should support two-way/continuous binding
+ .directive('crmUiAccordion', function() {
+ return {
+ scope: {
+ crmUiAccordion: '='
+ },
+ template: '<div ng-class="cssClasses"><div class="crm-accordion-header">{{crmUiAccordion.title}} <a crm-ui-help="help" ng-if="help"></a></div><div class="crm-accordion-body" ng-transclude></div></div>',
+ transclude: true,
+ link: function (scope, element, attrs) {
+ scope.cssClasses = {
+ 'crm-accordion-wrapper': true,
+ collapsed: scope.crmUiAccordion.collapsed
+ };
+ scope.help = null;
+ scope.$watch('crmUiAccordion', function(crmUiAccordion) {
+ if (crmUiAccordion && crmUiAccordion.help) {
+ scope.help = crmUiAccordion.help.clone({}, {
+ title: crmUiAccordion.title
+ });
+ }
+ });
+ }
+ };
+ })
+
+ // Examples:
+ // crmUiAlert({text: 'My text', title: 'My title', type: 'error'});
+ // crmUiAlert({template: '<a ng-click="ok()">Hello</a>', scope: $scope.$new()});
+ // var h = crmUiAlert({templateUrl: '~/crmFoo/alert.html', scope: $scope.$new()});
+ // ... h.close(); ...
+ .service('crmUiAlert', function($compile, $rootScope, $templateRequest, $q) {
+ var count = 0;
+ return function crmUiAlert(params) {
+ var id = 'crmUiAlert_' + (++count);
+ var tpl = null;
+ if (params.templateUrl) {
+ tpl = $templateRequest(params.templateUrl);
+ }
+ else if (params.template) {
+ tpl = params.template;
+ }
+ if (tpl) {
+ params.text = '<div id="' + id + '"></div>'; // temporary stub
+ }
+ var result = CRM.alert(params.text, params.title, params.type, params.options);
+ if (tpl) {
+ $q.when(tpl, function(html) {
+ var scope = params.scope || $rootScope.$new();
+ var linker = $compile(html);
+ $('#' + id).append($(linker(scope)));
+ });
+ }
+ return result;
+ };
+ })
+
+ // Simple wrapper around $.crmDatepicker.
+ // example with no time input: <input crm-ui-datepicker="{time: false}" ng-model="myobj.datefield"/>
+ // example with custom date format: <input crm-ui-datepicker="{date: 'm/d/y'}" ng-model="myobj.datefield"/>
+ .directive('crmUiDatepicker', function () {
+ return {
+ restrict: 'AE',
+ require: 'ngModel',
+ scope: {
+ crmUiDatepicker: '='
+ },
+ link: function (scope, element, attrs, ngModel) {
+ ngModel.$render = function () {
+ element.val(ngModel.$viewValue).change();
+ };
+
+ element
+ .crmDatepicker(scope.crmUiDatepicker)
+ .on('change', function() {
+ var requiredLength = 19;
+ if (scope.crmUiDatepicker && scope.crmUiDatepicker.time === false) {
+ requiredLength = 10;
+ }
+ if (scope.crmUiDatepicker && scope.crmUiDatepicker.date === false) {
+ requiredLength = 8;
+ }
+ ngModel.$setValidity('incompleteDateTime', !($(this).val().length && $(this).val().length !== requiredLength));
+ });
+ }
+ };
+ })
+
+ // Display debug information (if available)
+ // For richer DX, checkout Batarang/ng-inspector (Chrome/Safari), or AngScope/ng-inspect (Firefox).
+ // example: <div crm-ui-debug="myobject" />
+ .directive('crmUiDebug', function ($location) {
+ return {
+ restrict: 'AE',
+ scope: {
+ crmUiDebug: '@'
+ },
+ template: function() {
+ var args = $location.search();
+ return (args && args.angularDebug) ? '<div crm-ui-accordion=\'{title: ts("Debug (%1)", {1: crmUiDebug}), collapsed: true}\'><pre>{{data|json}}</pre></div>' : '';
+ },
+ link: function(scope, element, attrs) {
+ var args = $location.search();
+ if (args && args.angularDebug) {
+ scope.ts = CRM.ts(null);
+ scope.$parent.$watch(attrs.crmUiDebug, function(data) {
+ scope.data = data;
+ });
+ }
+ }
+ };
+ })
+
+ // Display a field/row in a field list
+ // example: <div crm-ui-field="{title: ts('My Field')}"> {{mydata}} </div>
+ // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field')}"> <input crm-ui-id="subform.myfield" name="myfield" /> </div>
+ // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field')}"> <input crm-ui-id="subform.myfield" name="myfield" required /> </div>
+ // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field'), help: hs('help_field_name'), required: true}"> {{mydata}} </div>
+ .directive('crmUiField', function() {
+ // Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
+ var templateUrls = {
+ default: '~/crmUi/field.html',
+ checkbox: '~/crmUi/field-cb.html'
+ };
+
+ return {
+ require: '^crmUiIdScope',
+ restrict: 'EA',
+ scope: {
+ // {title, name, help, helpFile}
+ crmUiField: '='
+ },
+ templateUrl: function(tElement, tAttrs){
+ var layout = tAttrs.crmLayout ? tAttrs.crmLayout : 'default';
+ return templateUrls[layout];
+ },
+ transclude: true,
+ link: function (scope, element, attrs, crmUiIdCtrl) {
+ $(element).addClass('crm-section');
+ scope.help = null;
+ scope.$watch('crmUiField', function(crmUiField) {
+ if (crmUiField && crmUiField.help) {
+ scope.help = crmUiField.help.clone({}, {
+ title: crmUiField.title
+ });
+ }
+ });
+ }
+ };
+ })
+
+ // example: <div ng-form="subform" crm-ui-id-scope><label crm-ui-for="subform.foo">Foo:</label><input crm-ui-id="subform.foo" name="foo"/></div>
+ .directive('crmUiId', function () {
+ return {
+ require: '^crmUiIdScope',
+ restrict: 'EA',
+ link: {
+ pre: function (scope, element, attrs, crmUiIdCtrl) {
+ var id = crmUiIdCtrl.get(attrs.crmUiId);
+ element.attr('id', id);
+ }
+ }
+ };
+ })
+
+ // for example, see crmUiHelp
+ .service('crmUiHelp', function(){
+ // example: var h = new FieldHelp({id: 'foo'}); h.open();
+ function FieldHelp(options) {
+ this.options = options;
+ }
+ angular.extend(FieldHelp.prototype, {
+ get: function(n) {
+ return this.options[n];
+ },
+ open: function open() {
+ CRM.help(this.options.title, {id: this.options.id, file: this.options.file});
+ },
+ clone: function clone(options, defaults) {
+ return new FieldHelp(angular.extend({}, defaults, this.options, options));
+ }
+ });
+
+ // example: var hs = crmUiHelp({file: 'CRM/Foo/Bar'});
+ return function(defaults){
+ // example: hs('myfield')
+ // example: hs({id: 'myfield', title: 'Foo Bar', file: 'Whiz/Bang'})
+ return function(options) {
+ if (_.isString(options)) {
+ options = {id: options};
+ }
+ return new FieldHelp(angular.extend({}, defaults, options));
+ };
+ };
+ })
+
+ // Display a help icon
+ // Example: Use a default *.hlp file
+ // scope.hs = crmUiHelp({file: 'Path/To/Help/File'});
+ // HTML: <a crm-ui-help="hs({title:ts('My Field'), id:'my_field'})">
+ // Example: Use an explicit *.hlp file
+ // HTML: <a crm-ui-help="hs({title:ts('My Field'), id:'my_field', file:'CRM/Foo/Bar'})">
+ .directive('crmUiHelp', function() {
+ return {
+ restrict: 'EA',
+ link: function(scope, element, attrs) {
+ setTimeout(function() {
+ var crmUiHelp = scope.$eval(attrs.crmUiHelp);
+ var title = crmUiHelp && crmUiHelp.get('title') ? ts('%1 Help', {1: crmUiHelp.get('title')}) : ts('Help');
+ element.attr('title', title);
+ }, 50);
+
+ element
+ .addClass('helpicon')
+ .attr('href', '#')
+ .on('click', function(e) {
+ e.preventDefault();
+ scope.$eval(attrs.crmUiHelp).open();
+ });
+ }
+ };
+ })
+
+ // example: <div ng-form="subform" crm-ui-id-scope><label crm-ui-for="subform.foo">Foo:</label><input crm-ui-id="subform.foo" name="foo"/></div>
+ .directive('crmUiFor', function ($parse, $timeout) {
+ return {
+ require: '^crmUiIdScope',
+ restrict: 'EA',
+ template: '<span ng-class="cssClasses"><span ng-transclude/><span crm-ui-visible="crmIsRequired" class="crm-marker" title="This field is required.">*</span></span>',
+ transclude: true,
+ link: function (scope, element, attrs, crmUiIdCtrl) {
+ scope.crmIsRequired = false;
+ scope.cssClasses = {};
+
+ if (!attrs.crmUiFor) return;
+
+ var id = crmUiIdCtrl.get(attrs.crmUiFor);
+ element.attr('for', id);
+ var ngModel = null;
+
+ var updateCss = function () {
+ scope.cssClasses['crm-error'] = !ngModel.$valid && !ngModel.$pristine;
+ };
+
+ // Note: if target element is dynamically generated (eg via ngInclude), then it may not be available
+ // immediately for initialization. Use retries/retryDelay to initialize such elements.
+ var init = function (retries, retryDelay) {
+ var input = $('#' + id);
+ if (input.length === 0 && !attrs.crmUiForceRequired) {
+ if (retries) {
+ $timeout(function(){
+ init(retries-1, retryDelay);
+ }, retryDelay);
+ }
+ return;
+ }
+
+ if (attrs.crmUiForceRequired) {
+ scope.crmIsRequired = true;
+ return;
+ }
+
+ var tgtScope = scope;//.$parent;
+ if (attrs.crmDepth) {
+ for (var i = attrs.crmDepth; i > 0; i--) {
+ tgtScope = tgtScope.$parent;
+ }
+ }
+
+ if (input.attr('ng-required')) {
+ scope.crmIsRequired = scope.$parent.$eval(input.attr('ng-required'));
+ scope.$parent.$watch(input.attr('ng-required'), function (isRequired) {
+ scope.crmIsRequired = isRequired;
+ });
+ }
+ else {
+ scope.crmIsRequired = input.prop('required');
+ }
+
+ ngModel = $parse(attrs.crmUiFor)(tgtScope);
+ if (ngModel) {
+ ngModel.$viewChangeListeners.push(updateCss);
+ }
+ };
+
+ $timeout(function(){
+ init(3, 100);
+ });
+ }
+ };
+ })
+
+ // Define a scope in which a name like "subform.foo" maps to a unique ID.
+ // example: <div ng-form="subform" crm-ui-id-scope><label crm-ui-for="subform.foo">Foo:</label><input crm-ui-id="subform.foo" name="foo"/></div>
+ .directive('crmUiIdScope', function () {
+ return {
+ restrict: 'EA',
+ scope: {},
+ controllerAs: 'crmUiIdCtrl',
+ controller: function($scope) {
+ var ids = {};
+ this.get = function(name) {
+ if (!ids[name]) {
+ ids[name] = "crmUiId_" + (++uidCount);
+ }
+ return ids[name];
+ };
+ },
+ link: function (scope, element, attrs) {}
+ };
+ })
+
+ // Display an HTML blurb inside an IFRAME.
+ // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
+ // example: <iframe crm-ui-iframe crm-ui-iframe-src="getUrl()"></iframe>
+ .directive('crmUiIframe', function ($parse) {
+ return {
+ scope: {
+ crmUiIframeSrc: '@', // expression which evaluates to a URL
+ crmUiIframe: '@' // expression which evaluates to HTML content
+ },
+ link: function (scope, elm, attrs) {
+ var iframe = $(elm)[0];
+ iframe.setAttribute('width', '100%');
+ iframe.setAttribute('height', '250px');
+ iframe.setAttribute('frameborder', '0');
+
+ var refresh = function () {
+ if (attrs.crmUiIframeSrc) {
+ iframe.setAttribute('src', scope.$parent.$eval(attrs.crmUiIframeSrc));
+ }
+ else {
+ var iframeHtml = scope.$parent.$eval(attrs.crmUiIframe);
+
+ var doc = iframe.document;
+ if (iframe.contentDocument) {
+ doc = iframe.contentDocument;
+ }
+ else if (iframe.contentWindow) {
+ doc = iframe.contentWindow.document;
+ }
+
+ doc.open();
+ doc.writeln(iframeHtml);
+ doc.close();
+ }
+ };
+
+ // If the iframe is in a dialog, respond to resize events
+ $(elm).parent().on('dialogresize dialogopen', function(e, ui) {
+ $(this).css({padding: '0', margin: '0', overflow: 'hidden'});
+ iframe.setAttribute('height', '' + $(this).innerHeight() + 'px');
+ });
+
+ $(elm).parent().on('dialogresize', function(e, ui) {
+ iframe.setAttribute('class', 'resized');
+ });
+
+ scope.$parent.$watch(attrs.crmUiIframe, refresh);
+ }
+ };
+ })
+
+ // Example:
+ // <a ng-click="$broadcast('my-insert-target', 'some new text')>Insert</a>
+ // <textarea crm-ui-insert-rx='my-insert-target'></textarea>
+ .directive('crmUiInsertRx', function() {
+ return {
+ link: function(scope, element, attrs) {
+ scope.$on(attrs.crmUiInsertRx, function(e, tokenName) {
+ CRM.wysiwyg.insert(element, tokenName);
+ $(element).select2('close').select2('val', '');
+ CRM.wysiwyg.focus(element);
+ });
+ }
+ };
+ })
+
+ // Define a rich text editor.
+ // example: <textarea crm-ui-id="myForm.body_html" crm-ui-richtext name="body_html" ng-model="mailing.body_html"></textarea>
+ .directive('crmUiRichtext', function ($timeout) {
+ return {
+ require: '?ngModel',
+ link: function (scope, elm, attr, ngModel) {
+
+ var editor = CRM.wysiwyg.create(elm);
+ if (!ngModel) {
+ return;
+ }
+
+ if (attr.ngBlur) {
+ $(elm).on('blur', function() {
+ $timeout(function() {
+ scope.$eval(attr.ngBlur);
+ });
+ });
+ }
+
+ ngModel.$render = function(value) {
+ editor.done(function() {
+ CRM.wysiwyg.setVal(elm, ngModel.$viewValue || '');
+ });
+ };
+ }
+ };
+ })
+
+ // Display a lock icon (based on a boolean).
+ // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
+ // example: <a crm-ui-lock
+ // binding="mymodel.boolfield"
+ // title-locked="ts('Boolfield is locked')"
+ // title-unlocked="ts('Boolfield is unlocked')"></a>
+ .directive('crmUiLock', function ($parse, $rootScope) {
+ var defaultVal = function (defaultValue) {
+ var f = function (scope) {
+ return defaultValue;
+ };
+ f.assign = function (scope, value) {
+ // ignore changes
+ };
+ return f;
+ };
+
+ // like $parse, but accepts a defaultValue in case expr is undefined
+ var parse = function (expr, defaultValue) {
+ return expr ? $parse(expr) : defaultVal(defaultValue);
+ };
+
+ return {
+ template: '',
+ link: function (scope, element, attrs) {
+ var binding = parse(attrs.binding, true);
+ var titleLocked = parse(attrs.titleLocked, ts('Locked'));
+ var titleUnlocked = parse(attrs.titleUnlocked, ts('Unlocked'));
+
+ $(element).addClass('crm-i lock-button');
+ var refresh = function () {
+ var locked = binding(scope);
+ if (locked) {
+ $(element)
+ .removeClass('fa-unlock')
+ .addClass('fa-lock')
+ .prop('title', titleLocked(scope))
+ ;
+ }
+ else {
+ $(element)
+ .removeClass('fa-lock')
+ .addClass('fa-unlock')
+ .prop('title', titleUnlocked(scope))
+ ;
+ }
+ };
+
+ $(element).click(function () {
+ binding.assign(scope, !binding(scope));
+ //scope.$digest();
+ $rootScope.$digest();
+ });
+
+ scope.$watch(attrs.binding, refresh);
+ scope.$watch(attrs.titleLocked, refresh);
+ scope.$watch(attrs.titleUnlocked, refresh);
+
+ refresh();
+ }
+ };
+ })
+
+ // CrmUiOrderCtrl is a controller class which manages sort orderings.
+ // Ex:
+ // JS: $scope.myOrder = new CrmUiOrderCtrl(['+field1', '-field2]);
+ // $scope.myOrder.toggle('field1');
+ // $scope.myOrder.setDir('field2', '');
+ // HTML: <tr ng-repeat="... | order:myOrder.get()">...</tr>
+ .service('CrmUiOrderCtrl', function(){
+ //
+ function CrmUiOrderCtrl(defaults){
+ this.values = defaults;
+ }
+ angular.extend(CrmUiOrderCtrl.prototype, {
+ get: function get() {
+ return this.values;
+ },
+ getDir: function getDir(name) {
+ if (this.values.indexOf(name) >= 0 || this.values.indexOf('+' + name) >= 0) {
+ return '+';
+ }
+ if (this.values.indexOf('-' + name) >= 0) {
+ return '-';
+ }
+ return '';
+ },
+ // @return bool TRUE if something is removed
+ remove: function remove(name) {
+ var idx = this.values.indexOf(name);
+ if (idx >= 0) {
+ this.values.splice(idx, 1);
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+ setDir: function setDir(name, dir) {
+ return this.toggle(name, dir);
+ },
+ // Toggle sort order on a field.
+ // To set a specific order, pass optional parameter 'next' ('+', '-', or '').
+ toggle: function toggle(name, next) {
+ if (!next && next !== '') {
+ next = '+';
+ if (this.remove(name) || this.remove('+' + name)) {
+ next = '-';
+ }
+ if (this.remove('-' + name)) {
+ next = '';
+ }
+ }
+
+ if (next == '+') {
+ this.values.unshift('+' + name);
+ }
+ else if (next == '-') {
+ this.values.unshift('-' + name);
+ }
+ }
+ });
+ return CrmUiOrderCtrl;
+ })
+
+ // Define a controller which manages sort order. You may interact with the controller
+ // directly ("myOrder.toggle('fieldname')") order using the helper, crm-ui-order-by.
+ // example:
+ // <span crm-ui-order="{var: 'myOrder', defaults: {'-myField'}}"></span>
+ // <th><a crm-ui-order-by="[myOrder,'myField']">My Field</a></th>
+ // <tr ng-repeat="... | order:myOrder.get()">...</tr>
+ // <button ng-click="myOrder.toggle('myField')">
+ .directive('crmUiOrder', function(CrmUiOrderCtrl) {
+ return {
+ link: function(scope, element, attrs){
+ var options = angular.extend({var: 'crmUiOrderBy'}, scope.$eval(attrs.crmUiOrder));
+ scope[options.var] = new CrmUiOrderCtrl(options.defaults);
+ }
+ };
+ })
+
+ // For usage, see crmUiOrder (above)
+ .directive('crmUiOrderBy', function() {
+ return {
+ link: function(scope, element, attrs) {
+ function updateClass(crmUiOrderCtrl, name) {
+ var dir = crmUiOrderCtrl.getDir(name);
+ element
+ .toggleClass('sorting_asc', dir === '+')
+ .toggleClass('sorting_desc', dir === '-')
+ .toggleClass('sorting', dir === '');
+ }
+
+ element.on('click', function(e){
+ var tgt = scope.$eval(attrs.crmUiOrderBy);
+ tgt[0].toggle(tgt[1]);
+ updateClass(tgt[0], tgt[1]);
+ e.preventDefault();
+ scope.$digest();
+ });
+
+ var tgt = scope.$eval(attrs.crmUiOrderBy);
+ updateClass(tgt[0], tgt[1]);
+ }
+ };
+ })
+
+ // Display a fancy SELECT (based on select2).
+ // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
+ .directive('crmUiSelect', function ($parse, $timeout) {
+ return {
+ require: '?ngModel',
+ priority: 1,
+ scope: {
+ crmUiSelect: '='
+ },
+ link: function (scope, element, attrs, ngModel) {
+ // In cases where UI initiates update, there may be an extra
+ // call to refreshUI, but it doesn't create a cycle.
+
+ if (ngModel) {
+ ngModel.$render = function () {
+ $timeout(function () {
+ // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
+ // new item is added before selection is made
+ var newVal = _.cloneDeep(ngModel.$modelValue);
+ // Fix possible data-type mismatch
+ if (typeof newVal === 'string' && element.select2('container').hasClass('select2-container-multi')) {
+ newVal = newVal.length ? newVal.split(',') : [];
+ }
+ element.select2('val', newVal);
+ });
+ };
+ }
+ function refreshModel() {
+ var oldValue = ngModel.$viewValue, newValue = element.select2('val');
+ if (oldValue != newValue) {
+ scope.$parent.$apply(function () {
+ ngModel.$setViewValue(newValue);
+ });
+ }
+ }
+
+ function init() {
+ // TODO watch select2-options
+ element.crmSelect2(scope.crmUiSelect || {});
+ if (ngModel) {
+ element.on('change', refreshModel);
+ }
+ }
+
+ init();
+ }
+ };
+ })
+
+ // Render a crmEntityRef widget
+ // usage: <input crm-entityref="{entity: 'Contact', select: {allowClear:true}}" ng-model="myobj.field" />
+ .directive('crmEntityref', function ($parse, $timeout) {
+ return {
+ require: '?ngModel',
+ scope: {
+ crmEntityref: '='
+ },
+ link: function (scope, element, attrs, ngModel) {
+ // In cases where UI initiates update, there may be an extra
+ // call to refreshUI, but it doesn't create a cycle.
+
+ ngModel.$render = function () {
+ $timeout(function () {
+ // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
+ // new item is added before selection is made
+ var newVal = _.cloneDeep(ngModel.$modelValue);
+ // Fix possible data-type mismatch
+ if (typeof newVal === 'string' && element.select2('container').hasClass('select2-container-multi')) {
+ newVal = newVal.length ? newVal.split(',') : [];
+ }
+ element.select2('val', newVal);
+ });
+ };
+ function refreshModel() {
+ var oldValue = ngModel.$viewValue, newValue = element.select2('val');
+ if (oldValue != newValue) {
+ scope.$parent.$apply(function () {
+ ngModel.$setViewValue(newValue);
+ });
+ }
+ }
+
+ function init() {
+ // TODO can we infer "entity" from model?
+ element.crmEntityRef(scope.crmEntityref || {});
+ element.on('change', refreshModel);
+ $timeout(ngModel.$render);
+ }
+
+ init();
+ }
+ };
+ })
+
+ // validate multiple email text
+ // usage: <input crm-multiple-email type="text" ng-model="myobj.field" />
+ .directive('crmMultipleEmail', function ($parse, $timeout) {
+ return {
+ require: 'ngModel',
+ link: function(scope, element, attrs, ctrl) {
+ ctrl.$parsers.unshift(function(viewValue) {
+ // if empty value provided simply bypass validation
+ if (_.isEmpty(viewValue)) {
+ ctrl.$setValidity('crmMultipleEmail', true);
+ return viewValue;
+ }
+
+ // split email string on basis of comma
+ var emails = viewValue.split(',');
+ // regex pattern for single email
+ var emailRegex = /\S+@\S+\.\S+/;
+
+ var validityArr = emails.map(function(str){
+ return emailRegex.test(str.trim());
+ });
+
+ if ($.inArray(false, validityArr) > -1) {
+ ctrl.$setValidity('crmMultipleEmail', false);
+ } else {
+ ctrl.$setValidity('crmMultipleEmail', true);
+ }
+ return viewValue;
+ });
+ }
+ };
+ })
+ // example <div crm-ui-tab id="tab-1" crm-title="ts('My Title')" count="3">...content...</div>
+ // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
+ .directive('crmUiTab', function($parse) {
+ return {
+ require: '^crmUiTabSet',
+ restrict: 'EA',
+ scope: {
+ crmTitle: '@',
+ crmIcon: '@',
+ count: '@',
+ id: '@'
+ },
+ template: '<div ng-transclude></div>',
+ transclude: true,
+ link: function (scope, element, attrs, crmUiTabSetCtrl) {
+ crmUiTabSetCtrl.add(scope);
+ }
+ };
+ })
+
+ // example: <div crm-ui-tab-set><div crm-ui-tab crm-title="Tab 1">...</div><div crm-ui-tab crm-title="Tab 2">...</div></div>
+ .directive('crmUiTabSet', function() {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiTabSet: '@',
+ tabSetOptions: '@'
+ },
+ templateUrl: '~/crmUi/tabset.html',
+ transclude: true,
+ controllerAs: 'crmUiTabSetCtrl',
+ controller: function($scope, $parse) {
+ var tabs = $scope.tabs = []; // array<$scope>
+ this.add = function(tab) {
+ if (!tab.id) throw "Tab is missing 'id'";
+ tabs.push(tab);
+ };
+ },
+ link: function (scope, element, attrs) {}
+ };
+ })
+
+ // Generic, field-independent form validator.
+ // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" />
+ // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" crm-ui-validate-name="myError" />
+ .directive('crmUiValidate', function() {
+ return {
+ restrict: 'EA',
+ require: 'ngModel',
+ link: function(scope, element, attrs, ngModel) {
+ var validationKey = attrs.crmUiValidateName ? attrs.crmUiValidateName : 'crmUiValidate';
+ scope.$watch(attrs.crmUiValidate, function(newValue){
+ ngModel.$setValidity(validationKey, !!newValue);
+ });
+ }
+ };
+ })
+
+ // like ng-show, but hides/displays elements using "visibility" which maintains positioning
+ // example <div crm-ui-visible="false">...content...</div>
+ .directive('crmUiVisible', function($parse) {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiVisible: '@'
+ },
+ link: function (scope, element, attrs) {
+ var model = $parse(attrs.crmUiVisible);
+ function updatecChildren() {
+ element.css('visibility', model(scope.$parent) ? 'inherit' : 'hidden');
+ }
+ updatecChildren();
+ scope.$parent.$watch(attrs.crmUiVisible, updatecChildren);
+ }
+ };
+ })
+
+ // example: <div crm-ui-wizard="myWizardCtrl"><div crm-ui-wizard-step crm-title="ts('Step 1')">...</div><div crm-ui-wizard-step crm-title="ts('Step 2')">...</div></div>
+ // example with custom nav classes: <div crm-ui-wizard crm-ui-wizard-nav-class="ng-animate-out ...">...</div>
+ // Note: "myWizardCtrl" has various actions/properties like next() and $first().
+ // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
+ // WISHLIST: Allow each step to enable/disable (show/hide) itself
+ .directive('crmUiWizard', function() {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiWizard: '@',
+ crmUiWizardNavClass: '@' // string, A list of classes that will be added to the nav items
+ },
+ templateUrl: '~/crmUi/wizard.html',
+ transclude: true,
+ controllerAs: 'crmUiWizardCtrl',
+ controller: function($scope, $parse) {
+ var steps = $scope.steps = []; // array<$scope>
+ var crmUiWizardCtrl = this;
+ var maxVisited = 0;
+ var selectedIndex = null;
+
+ var findIndex = function() {
+ var found = null;
+ angular.forEach(steps, function(step, stepKey) {
+ if (step.selected) found = stepKey;
+ });
+ return found;
+ };
+
+ /// @return int the index of the current step
+ this.$index = function() { return selectedIndex; };
+ /// @return bool whether the currentstep is first
+ this.$first = function() { return this.$index() === 0; };
+ /// @return bool whether the current step is last
+ this.$last = function() { return this.$index() === steps.length -1; };
+ this.$maxVisit = function() { return maxVisited; };
+ this.$validStep = function() {
+ return steps[selectedIndex] && steps[selectedIndex].isStepValid();
+ };
+ this.iconFor = function(index) {
+ if (index < this.$index()) return '√';
+ if (index === this.$index()) return '»';
+ return ' ';
+ };
+ this.isSelectable = function(step) {
+ if (step.selected) return false;
+ return this.$validStep();
+ };
+
+ /*** @param Object step the $scope of the step */
+ this.select = function(step) {
+ angular.forEach(steps, function(otherStep, otherKey) {
+ otherStep.selected = (otherStep === step);
+ if (otherStep === step && maxVisited < otherKey) maxVisited = otherKey;
+ });
+ selectedIndex = findIndex();
+ };
+ /*** @param Object step the $scope of the step */
+ this.add = function(step) {
+ if (steps.length === 0) {
+ step.selected = true;
+ selectedIndex = 0;
+ }
+ steps.push(step);
+ steps.sort(function(a,b){
+ return a.crmUiWizardStep - b.crmUiWizardStep;
+ });
+ selectedIndex = findIndex();
+ };
+ this.remove = function(step) {
+ var key = null;
+ angular.forEach(steps, function(otherStep, otherKey) {
+ if (otherStep === step) key = otherKey;
+ });
+ if (key !== null) {
+ steps.splice(key, 1);
+ }
+ };
+ this.goto = function(index) {
+ if (index < 0) index = 0;
+ if (index >= steps.length) index = steps.length-1;
+ this.select(steps[index]);
+ };
+ this.previous = function() { this.goto(this.$index()-1); };
+ this.next = function() { this.goto(this.$index()+1); };
+ if ($scope.crmUiWizard) {
+ $parse($scope.crmUiWizard).assign($scope.$parent, this);
+ }
+ },
+ link: function (scope, element, attrs) {
+ scope.ts = CRM.ts(null);
+
+ element.find('.crm-wizard-buttons button[ng-click^=crmUiWizardCtrl]').click(function () {
+ // These values are captured inside the click handler to ensure the
+ // positions/sizes of the elements are captured at the time of the
+ // click vs. at the time this directive is initialized.
+ var topOfWizard = element.offset().top;
+ var heightOfMenu = $('#civicrm-menu').height() || 0;
+
+ $('html')
+ // stop any other animations that might be happening...
+ .stop()
+ // gracefully slide the user to the top of the wizard
+ .animate({scrollTop: topOfWizard - heightOfMenu}, 1000);
+ });
+ }
+ };
+ })
+
+ // Use this to add extra markup to wizard
+ .directive('crmUiWizardButtons', function() {
+ return {
+ require: '^crmUiWizard',
+ restrict: 'EA',
+ scope: {},
+ template: '<span ng-transclude></span>',
+ transclude: true,
+ link: function (scope, element, attrs, crmUiWizardCtrl) {
+ var realButtonsEl = $(element).closest('.crm-wizard').find('.crm-wizard-buttons');
+ $(element).appendTo(realButtonsEl);
+ }
+ };
+ })
+
+ // Example for Font Awesome: <button crm-icon="fa-check">Save</button>
+ // Example for jQuery UI (deprecated): <button crm-icon="check">Save</button>
+ .directive('crmIcon', function() {
+ return {
+ restrict: 'EA',
+ link: function (scope, element, attrs) {
+ if (element.is('[crm-ui-tab]')) {
+ // handled in crmUiTab ctrl
+ return;
+ }
+ if (attrs.crmIcon.substring(0,3) == 'fa-') {
+ $(element).prepend('<i class="crm-i ' + attrs.crmIcon + '"></i> ');
+ }
+ else {
+ $(element).prepend('<span class="icon ui-icon-' + attrs.crmIcon + '"></span> ');
+ }
+ if ($(element).is('button')) {
+ $(element).addClass('crm-button');
+ }
+ }
+ };
+ })
+
+ // example: <div crm-ui-wizard-step crm-title="ts('My Title')" ng-form="mySubForm">...content...</div>
+ // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering.
+ // example: <div crm-ui-wizard-step="100" crm-title="..." ng-if="...">...content...</div>
+ // example with custom classes: <div crm-ui-wizard-step="100" crm-ui-wizard-step-class="ng-animate-out ...">...content...</div>
+ .directive('crmUiWizardStep', function() {
+ var nextWeight = 1;
+ return {
+ require: ['^crmUiWizard', 'form'],
+ restrict: 'EA',
+ scope: {
+ crmTitle: '@', // expression, evaluates to a printable string
+ crmUiWizardStep: '@', // int, a weight which determines the ordering of the steps
+ crmUiWizardStepClass: '@' // string, A list of classes that will be added to the template
+ },
+ template: '<div class="crm-wizard-step {{crmUiWizardStepClass}}" ng-show="selected" ng-transclude/></div>',
+ transclude: true,
+ link: function (scope, element, attrs, ctrls) {
+ var crmUiWizardCtrl = ctrls[0], form = ctrls[1];
+ if (scope.crmUiWizardStep) {
+ scope.crmUiWizardStep = parseInt(scope.crmUiWizardStep);
+ } else {
+ scope.crmUiWizardStep = nextWeight++;
+ }
+ scope.isStepValid = function() {
+ return form.$valid;
+ };
+ crmUiWizardCtrl.add(scope);
+ scope.$on('$destroy', function(){
+ crmUiWizardCtrl.remove(scope);
+ });
+ }
+ };
+ })
+
+ // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
+ // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
+ // Example: <button crm-confirm="{templateUrl: '~/path/to/view.html', export: {foo: bar}}" on-yes="frobnicate(123)">Frobincate</button>
+ .directive('crmConfirm', function ($compile, $rootScope, $templateRequest, $q) {
+ // Helpers to calculate default options for CRM.confirm()
+ var defaultFuncs = {
+ 'disable': function (options) {
+ return {
+ message: ts('Are you sure you want to disable this?'),
+ options: {no: ts('Cancel'), yes: ts('Disable')},
+ width: 300,
+ title: ts('Disable %1?', {
+ 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
+ })
+ };
+ },
+ 'revert': function (options) {
+ return {
+ message: ts('Are you sure you want to revert this?'),
+ options: {no: ts('Cancel'), yes: ts('Revert')},
+ width: 300,
+ title: ts('Revert %1?', {
+ 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
+ })
+ };
+ },
+ 'delete': function (options) {
+ return {
+ message: ts('Are you sure you want to delete this?'),
+ options: {no: ts('Cancel'), yes: ts('Delete')},
+ width: 300,
+ title: ts('Delete %1?', {
+ 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
+ })
+ };
+ }
+ };
+ var confirmCount = 0;
+ return {
+ link: function (scope, element, attrs) {
+ $(element).click(function () {
+ var options = scope.$eval(attrs.crmConfirm);
+ if (attrs.title && !options.title) {
+ options.title = attrs.title;
+ }
+ var defaults = (options.type) ? defaultFuncs[options.type](options) : {};
+
+ var tpl = null, stubId = null;
+ if (!options.message) {
+ if (options.templateUrl) {
+ tpl = $templateRequest(options.templateUrl);
+ }
+ else if (options.template) {
+ tpl = options.template;
+ }
+ if (tpl) {
+ stubId = 'crmUiConfirm_' + (++confirmCount);
+ options.message = '<div id="' + stubId + '"></div>';
+ }
+ }
+
+ CRM.confirm(_.extend(defaults, options))
+ .on('crmConfirm:yes', function() { scope.$apply(attrs.onYes); })
+ .on('crmConfirm:no', function() { scope.$apply(attrs.onNo); });
+
+ if (tpl && stubId) {
+ $q.when(tpl, function(html) {
+ var scope = options.scope || $rootScope.$new();
+ if (options.export) {
+ angular.extend(scope, options.export);
+ }
+ var linker = $compile(html);
+ $('#' + stubId).append($(linker(scope)));
+ });
+ }
+ });
+ }
+ };
+ })
+
+ // Sets document title & page title; attempts to override CMS title markup for the latter
+ // WARNING: Use only once per route!
+ // WARNING: This directive works only if your AngularJS base page does not
+ // set a custom title (i.e., it has an initial title of "CiviCRM"). See the
+ // global variables pageTitle and documentTitle.
+ // Example (same title for both): <h1 crm-page-title>{{ts('Hello')}}</h1>
+ // Example (separate document title): <h1 crm-document-title="ts('Hello')" crm-page-title><i class="crm-i fa-flag"></i>{{ts('Hello')}}</h1>
+ .directive('crmPageTitle', function($timeout) {
+ return {
+ scope: {
+ crmDocumentTitle: '='
+ },
+ link: function(scope, $el, attrs) {
+ function update() {
+ $timeout(function() {
+ var newPageTitle = _.trim($el.html()),
+ newDocumentTitle = scope.crmDocumentTitle || $el.text();
+ document.title = $('title').text().replace(documentTitle, newDocumentTitle);
+ // If the CMS has already added title markup to the page, use it
+ $('h1').not('.crm-container h1').each(function() {
+ if (_.trim($(this).html()) === pageTitle) {
+ $(this).addClass('crm-page-title').html(newPageTitle);
+ $el.hide();
+ }
+ });
+ pageTitle = newPageTitle;
+ documentTitle = newDocumentTitle;
+ });
+ }
+
+ scope.$watch(function() {return scope.crmDocumentTitle + $el.html();}, update);
+ }
+ };
+ })
+
+ .run(function($rootScope, $location) {
+ /// Example: <button ng-click="goto('home')">Go home!</button>
+ $rootScope.goto = function(path) {
+ $location.path(path);
+ };
+ // useful for debugging: $rootScope.log = console.log || function() {};
+ })
+ ;
+
+})(angular, CRM.$, CRM._);