1 /// crmUi: Sundry UI helpers
2 (function (angular
, $, _
) {
5 var partialUrl = function (relPath
) {
6 return CRM
.resourceUrls
['civicrm'] + '/partials/crmUi/' + relPath
;
10 angular
.module('crmUi', [])
12 // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
13 // WISHLIST: crmCollapsed should support two-way/continous binding
14 .directive('crmUiAccordion', function() {
20 template
: '<div class="crm-accordion-wrapper" ng-class="cssClasses"><div class="crm-accordion-header">{{$parent.$eval(crmTitle)}}</div><div class="crm-accordion-body" ng-transclude></div></div>',
22 link: function (scope
, element
, attrs
) {
24 collapsed
: scope
.$parent
.$eval(attrs
.crmCollapsed
)
30 // example: <form name="myForm">...<label crm-ui-label crm-for="myField">My Field</span>...<input name="myField"/>...</form>
32 // Label adapts based on <input required>, <input ng-required>, or any other validation.
34 // Note: This should work in the normal case where <label> and <input> are in roughly the same scope,
35 // but if the scopes are materially different then problems could arise.
36 .directive('crmUiLabel', function($parse
) {
42 template
: '<span ng-class="cssClasses"><span ng-transclude></span> <span ng-show="crmRequired" class="crm-marker" title="This field is required.">*</span></span>',
43 link: function(scope
, element
, attrs
) {
44 if (attrs
.crmFor
== 'name') {
45 throw new Error('Validation monitoring does not work for field name "name"');
48 // 1. Figure out form and input elements
50 var form
= $(element
).closest('form');
51 var formCtrl
= scope
.$parent
.$eval(form
.attr('name'));
52 var input
= $('input[name="' + attrs
.crmFor
+ '"],select[name="' + attrs
.crmFor
+ '"],textarea[name="' + attrs
.crmFor
+ '"]', form
);
53 if (form
.length
!= 1 || input
.length
!= 1) {
54 if (console
.log
) console
.log('Label cannot be matched to input element. Expected to find one form and one input.', form
.length
, input
.length
);
58 // 2. Make sure that inputs are well-defined (with name+id).
60 if (!input
.attr('id')) {
61 input
.attr('id', 'crmUi_' + (++idCount
));
63 $(element
).attr('for', input
.attr('id'));
65 // 3. Monitor is the "required" and "$valid" properties
67 if (input
.attr('ng-required')) {
68 scope
.crmRequired
= scope
.$parent
.$eval(input
.attr('ng-required'));
69 scope
.$parent
.$watch(input
.attr('ng-required'), function(isRequired
) {
70 scope
.crmRequired
= isRequired
;
73 scope
.crmRequired
= input
.prop('required');
76 var inputCtrl
= form
.attr('name') + '.' + input
.attr('name');
77 scope
.cssClasses
= {};
78 scope
.$parent
.$watch(inputCtrl
+ '.$valid', function(newValue
) {
79 //scope.cssClasses['ng-valid'] = newValue;
80 //scope.cssClasses['ng-invalid'] = !newValue;
81 scope
.cssClasses
['crm-error'] = !scope
.$parent
.$eval(inputCtrl
+ '.$valid') && !scope
.$parent
.$eval(inputCtrl
+ '.$pristine');
83 scope
.$parent
.$watch(inputCtrl
+ '.$pristine', function(newValue
) {
84 //scope.cssClasses['ng-pristine'] = newValue;
85 //scope.cssClasses['ng-dirty'] = !newValue;
86 scope
.cssClasses
['crm-error'] = !scope
.$parent
.$eval(inputCtrl
+ '.$valid') && !scope
.$parent
.$eval(inputCtrl
+ '.$pristine');
93 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
94 // example: <a crm-ui-lock
95 // binding="mymodel.boolfield"
96 // title-locked="ts('Boolfield is locked')"
97 // title-unlocked="ts('Boolfield is unlocked')"></a>
98 .directive('crmUiLock', function ($parse
, $rootScope
) {
99 var defaultVal = function (defaultValue
) {
100 var f = function (scope
) {
103 f
.assign = function (scope
, value
) {
109 // like $parse, but accepts a defaultValue in case expr is undefined
110 var parse = function (expr
, defaultValue
) {
111 return expr
? $parse(expr
) : defaultVal(defaultValue
);
116 link: function (scope
, element
, attrs
) {
117 var binding
= parse(attrs
['binding'], true);
118 var titleLocked
= parse(attrs
['titleLocked'], ts('Locked'));
119 var titleUnlocked
= parse(attrs
['titleUnlocked'], ts('Unlocked'));
121 $(element
).addClass('ui-icon lock-button');
122 var refresh = function () {
123 var locked
= binding(scope
);
126 .removeClass('ui-icon-unlocked')
127 .addClass('ui-icon-locked')
128 .prop('title', titleLocked(scope
))
133 .removeClass('ui-icon-locked')
134 .addClass('ui-icon-unlocked')
135 .prop('title', titleUnlocked(scope
))
140 $(element
).click(function () {
141 binding
.assign(scope
, !binding(scope
));
143 $rootScope
.$digest();
146 scope
.$watch(attrs
.binding
, refresh
);
147 scope
.$watch(attrs
.titleLocked
, refresh
);
148 scope
.$watch(attrs
.titleUnlocked
, refresh
);
155 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
156 // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
157 .directive('crmUiTab', function($parse
) {
159 require
: '^crmUiTabSet',
165 template
: '<div ng-transclude></div>',
167 link: function (scope
, element
, attrs
, crmUiTabSetCtrl
) {
168 crmUiTabSetCtrl
.add(scope
);
173 // 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>
174 .directive('crmUiTabSet', function() {
180 templateUrl
: partialUrl('tabset.html'),
182 controllerAs
: 'crmUiTabSetCtrl',
183 controller: function($scope
, $parse
) {
184 var tabs
= $scope
.tabs
= []; // array<$scope>
185 this.add = function(tab
) {
186 if (!tab
.id
) throw "Tab is missing 'id'";
190 link: function (scope
, element
, attrs
) {}
194 // 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>
195 // Note: "myWizardCtrl" has various actions/properties like next() and $first().
196 // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
197 // WISHLIST: Allow each step to enable/disable (show/hide) itself
198 .directive('crmUiWizard', function() {
204 templateUrl
: partialUrl('wizard.html'),
206 controllerAs
: 'crmUiWizardCtrl',
207 controller: function($scope
, $parse
) {
208 var steps
= $scope
.steps
= []; // array<$scope>
209 var crmUiWizardCtrl
= this;
211 var selectedIndex
= null;
213 var findIndex = function() {
215 angular
.forEach(steps
, function(step
, stepKey
) {
216 if (step
.selected
) found
= stepKey
;
221 /// @return int the index of the current step
222 this.$index = function() { return selectedIndex
; };
223 /// @return bool whether the currentstep is first
224 this.$first = function() { return this.$index() === 0; };
225 /// @return bool whether the current step is last
226 this.$last = function() { return this.$index() === steps
.length
-1; };
227 this.$maxVisit = function() { return maxVisited
; }
228 this.iconFor = function(index
) {
229 if (index
< this.$index()) return '√';
230 if (index
=== this.$index()) return '»';
233 this.isSelectable = function(step
) {
234 if (step
.selected
) return false;
236 angular
.forEach(steps
, function(otherStep
, otherKey
) {
237 if (step
=== otherStep
&& otherKey
<= maxVisited
) result
= true;
242 /*** @param Object step the $scope of the step */
243 this.select = function(step
) {
244 angular
.forEach(steps
, function(otherStep
, otherKey
) {
245 otherStep
.selected
= (otherStep
=== step
);
246 if (otherStep
=== step
&& maxVisited
< otherKey
) maxVisited
= otherKey
;
248 selectedIndex
= findIndex();
250 /*** @param Object step the $scope of the step */
251 this.add = function(step
) {
252 if (steps
.length
=== 0) {
253 step
.selected
= true;
258 this.goto = function(index
) {
259 if (index
< 0) index
= 0;
260 if (index
>= steps
.length
) index
= steps
.length
-1;
261 this.select(steps
[index
]);
263 this.previous = function() { this.goto(this.$index()-1); };
264 this.next = function() { this.goto(this.$index()+1); };
265 if ($scope
.crmUiWizard
) {
266 $parse($scope
.crmUiWizard
).assign($scope
.$parent
, this)
269 link: function (scope
, element
, attrs
) {}
273 // Use this to add extra markup to wizard
274 .directive('crmUiWizardButtons', function() {
276 require
: '^crmUiWizard',
279 template
: '<span ng-transclude></span>',
281 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
282 var realButtonsEl
= $(element
).closest('.crm-wizard').find('.crm-wizard-buttons');
283 $(element
).appendTo(realButtonsEl
);
288 // example <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
289 .directive('crmUiWizardStep', function() {
291 require
: '^crmUiWizard',
296 template
: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
298 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
299 crmUiWizardCtrl
.add(scope
);
304 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
305 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
306 .directive('crmConfirm', function () {
307 // Helpers to calculate default options for CRM.confirm()
309 'disable': function (options
) {
311 message
: ts('Are you sure you want to disable this?'),
312 options
: {no
: ts('Cancel'), yes
: ts('Disable')},
314 title
: ts('Disable %1?', {
315 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
319 'revert': function (options
) {
321 message
: ts('Are you sure you want to revert this?'),
322 options
: {no
: ts('Cancel'), yes
: ts('Revert')},
324 title
: ts('Revert %1?', {
325 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
329 'delete': function (options
) {
331 message
: ts('Are you sure you want to delete this?'),
332 options
: {no
: ts('Cancel'), yes
: ts('Delete')},
334 title
: ts('Delete %1?', {
335 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
342 link: function (scope
, element
, attrs
) {
343 $(element
).click(function () {
344 var options
= scope
.$eval(attrs
['crmConfirm']);
345 var defaults
= (options
.type
) ? defaultFuncs
[options
.type
](options
) : {};
346 CRM
.confirm(_
.extend(defaults
, options
))
347 .on('crmConfirm:yes', function () { scope
.$apply(attrs
['onYes']); })
348 .on('crmConfirm:no', function () { scope
.$apply(attrs
['onNo']); });
353 .run(function($rootScope
, $location
) {
354 /// Example: <button ng-click="goto('home')">Go home!</button>
355 $rootScope
.goto = function(path
) {
356 $location
.path(path
);
358 // useful for debugging: $rootScope.log = console.log || function() {};
362 })(angular
, CRM
.$, CRM
._
);