1 /// crmUi: Sundry UI helpers
2 (function (angular
, $, _
) {
5 angular
.module('crmUi', [])
7 // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
8 // WISHLIST: crmCollapsed should support two-way/continous binding
9 .directive('crmUiAccordion', function() {
15 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>',
17 link: function (scope
, element
, attrs
) {
19 collapsed
: scope
.$parent
.$eval(attrs
.crmCollapsed
)
25 // example: <form name="myForm">...<label crm-ui-label crm-for="myField">My Field</span>...<input name="myField"/>...</form>
27 // Label adapts based on <input required>, <input ng-required>, or any other validation.
29 // Note: This should work in the normal case where <label> and <input> are in roughly the same scope,
30 // but if the scopes are materially different then problems could arise.
31 .directive('crmUiLabel', function($parse
) {
37 template
: '<span ng-class="cssClasses"><span ng-transclude></span> <span ng-show="crmRequired" class="crm-marker" title="This field is required.">*</span></span>',
38 link: function(scope
, element
, attrs
) {
39 if (attrs
.crmFor
== 'name') {
40 throw new Error('Validation monitoring does not work for field name "name"');
43 // 1. Figure out form and input elements
45 var form
= $(element
).closest('form');
46 var formCtrl
= scope
.$parent
.$eval(form
.attr('name'));
47 var input
= $('input[name="' + attrs
.crmFor
+ '"],select[name="' + attrs
.crmFor
+ '"],textarea[name="' + attrs
.crmFor
+ '"]', form
);
48 if (form
.length
!= 1 || input
.length
!= 1) {
49 if (console
.log
) console
.log('Label cannot be matched to input element. Expected to find one form and one input.', form
.length
, input
.length
);
53 // 2. Make sure that inputs are well-defined (with name+id).
55 if (!input
.attr('id')) {
56 input
.attr('id', 'crmUi_' + (++idCount
));
58 $(element
).attr('for', input
.attr('id'));
60 // 3. Monitor is the "required" and "$valid" properties
62 if (input
.attr('ng-required')) {
63 scope
.crmRequired
= scope
.$parent
.$eval(input
.attr('ng-required'));
64 scope
.$parent
.$watch(input
.attr('ng-required'), function(isRequired
) {
65 scope
.crmRequired
= isRequired
;
68 scope
.crmRequired
= input
.prop('required');
71 var inputCtrl
= form
.attr('name') + '.' + input
.attr('name');
72 scope
.cssClasses
= {};
73 scope
.$parent
.$watch(inputCtrl
+ '.$valid', function(newValue
) {
74 //scope.cssClasses['ng-valid'] = newValue;
75 //scope.cssClasses['ng-invalid'] = !newValue;
76 scope
.cssClasses
['crm-error'] = !scope
.$parent
.$eval(inputCtrl
+ '.$valid') && !scope
.$parent
.$eval(inputCtrl
+ '.$pristine');
78 scope
.$parent
.$watch(inputCtrl
+ '.$pristine', function(newValue
) {
79 //scope.cssClasses['ng-pristine'] = newValue;
80 //scope.cssClasses['ng-dirty'] = !newValue;
81 scope
.cssClasses
['crm-error'] = !scope
.$parent
.$eval(inputCtrl
+ '.$valid') && !scope
.$parent
.$eval(inputCtrl
+ '.$pristine');
88 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
89 // example: <a crm-ui-lock
90 // binding="mymodel.boolfield"
91 // title-locked="ts('Boolfield is locked')"
92 // title-unlocked="ts('Boolfield is unlocked')"></a>
93 .directive('crmUiLock', function ($parse
, $rootScope
) {
94 var defaultVal = function (defaultValue
) {
95 var f = function (scope
) {
98 f
.assign = function (scope
, value
) {
104 // like $parse, but accepts a defaultValue in case expr is undefined
105 var parse = function (expr
, defaultValue
) {
106 return expr
? $parse(expr
) : defaultVal(defaultValue
);
111 link: function (scope
, element
, attrs
) {
112 var binding
= parse(attrs
['binding'], true);
113 var titleLocked
= parse(attrs
['titleLocked'], ts('Locked'));
114 var titleUnlocked
= parse(attrs
['titleUnlocked'], ts('Unlocked'));
116 $(element
).addClass('ui-icon lock-button');
117 var refresh = function () {
118 var locked
= binding(scope
);
121 .removeClass('ui-icon-unlocked')
122 .addClass('ui-icon-locked')
123 .prop('title', titleLocked(scope
))
128 .removeClass('ui-icon-locked')
129 .addClass('ui-icon-unlocked')
130 .prop('title', titleUnlocked(scope
))
135 $(element
).click(function () {
136 binding
.assign(scope
, !binding(scope
));
138 $rootScope
.$digest();
141 scope
.$watch(attrs
.binding
, refresh
);
142 scope
.$watch(attrs
.titleLocked
, refresh
);
143 scope
.$watch(attrs
.titleUnlocked
, refresh
);
150 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
151 .directive('crmUiTab', function($parse
) {
156 template
: '<div><b>(Tab: {{$parent.$eval(crmTitle)}})</b><span ng-transclude/></div>',
158 link: function (scope
, element
, attrs
) {}
162 // 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>
163 .directive('crmUiTabSet', function() {
165 template
: '<div><span ng-transclude/></div>',
167 link: function (scope
, element
, attrs
) {}
171 // example: <div crm-ui-wizard><div crm-ui-wizard-step crm-title="Step 1">...</div><div crm-ui-wizard-step crm-title="Step 2">...</div></div>
172 .directive('crmUiWizard', function() {
174 template
: '<div><span ng-transclude/></div>',
176 link: function (scope
, element
, attrs
) {}
180 .directive('crmUiWizardFooter', function() {
182 template
: '<div><span ng-transclude/></div>',
184 link: function (scope
, element
, attrs
) {}
188 // example <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
189 .directive('crmUiWizardStep', function() {
194 template
: '<div><b>(Step: {{$parent.$eval(crmTitle)}})</b><span ng-transclude/></div>',
196 link: function (scope
, element
, attrs
) {}
200 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
201 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
202 .directive('crmConfirm', function () {
203 // Helpers to calculate default options for CRM.confirm()
205 'disable': function (options
) {
207 message
: ts('Are you sure you want to disable this?'),
208 options
: {no
: ts('Cancel'), yes
: ts('Disable')},
210 title
: ts('Disable %1?', {
211 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
215 'revert': function (options
) {
217 message
: ts('Are you sure you want to revert this?'),
218 options
: {no
: ts('Cancel'), yes
: ts('Revert')},
220 title
: ts('Revert %1?', {
221 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
225 'delete': function (options
) {
227 message
: ts('Are you sure you want to delete this?'),
228 options
: {no
: ts('Cancel'), yes
: ts('Delete')},
230 title
: ts('Delete %1?', {
231 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
238 link: function (scope
, element
, attrs
) {
239 $(element
).click(function () {
240 var options
= scope
.$eval(attrs
['crmConfirm']);
241 var defaults
= (options
.type
) ? defaultFuncs
[options
.type
](options
) : {};
242 CRM
.confirm(_
.extend(defaults
, options
))
243 .on('crmConfirm:yes', function () { scope
.$apply(attrs
['onYes']); })
244 .on('crmConfirm:no', function () { scope
.$apply(attrs
['onNo']); });
249 .run(function($rootScope
, $location
) {
250 /// Example: <button ng-click="goto('home')">Go home!</button>
251 $rootScope
.goto = function(path
) {
252 $location
.path(path
);
254 // useful for debugging: $rootScope.log = console.log || function() {};
258 })(angular
, CRM
.$, CRM
._
);