CRM-15578 - crmMailing2 - Better buttons in skeleton
[civicrm-core.git] / js / angular-crm-ui.js
CommitLineData
685acae4 1/// crmUi: Sundry UI helpers
2(function (angular, $, _) {
438f2b52 3 var idCount = 0;
685acae4 4
6717e4b9
TO
5 var partialUrl = function (relPath) {
6 return CRM.resourceUrls['civicrm'] + '/partials/crmUi/' + relPath;
7 };
8
9
685acae4 10 angular.module('crmUi', [])
030dce01 11
0cbed02c
TO
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
030dce01
TO
14 .directive('crmUiAccordion', function() {
15 return {
16 scope: {
0cbed02c
TO
17 crmTitle: '@',
18 crmCollapsed: '@'
030dce01 19 },
0cbed02c 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>',
030dce01 21 transclude: true,
0cbed02c
TO
22 link: function (scope, element, attrs) {
23 scope.cssClasses = {
24 collapsed: scope.$parent.$eval(attrs.crmCollapsed)
25 };
26 }
030dce01
TO
27 };
28 })
29
438f2b52
TO
30 // example: <form name="myForm">...<label crm-ui-label crm-for="myField">My Field</span>...<input name="myField"/>...</form>
31 //
32 // Label adapts based on <input required>, <input ng-required>, or any other validation.
33 //
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) {
37 return {
38 scope: {
39 name: '@'
40 },
41 transclude: true,
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"');
46 }
47
48 // 1. Figure out form and input elements
49
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);
55 return;
56 }
57
58 // 2. Make sure that inputs are well-defined (with name+id).
59
60 if (!input.attr('id')) {
61 input.attr('id', 'crmUi_' + (++idCount));
62 }
63 $(element).attr('for', input.attr('id'));
64
65 // 3. Monitor is the "required" and "$valid" properties
66
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;
71 });
72 } else {
73 scope.crmRequired = input.prop('required');
74 }
75
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');
82 });
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');
87 });
88
89 }
90 };
91 })
685acae4 92
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) {
101 return defaultValue;
102 }
103 f.assign = function (scope, value) {
104 // ignore changes
105 }
106 return f;
107 };
108
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);
112 };
113
114 return {
115 template: '',
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'));
120
121 $(element).addClass('ui-icon lock-button');
122 var refresh = function () {
123 var locked = binding(scope);
124 if (locked) {
125 $(element)
126 .removeClass('ui-icon-unlocked')
127 .addClass('ui-icon-locked')
128 .prop('title', titleLocked(scope))
129 ;
130 }
131 else {
132 $(element)
133 .removeClass('ui-icon-locked')
134 .addClass('ui-icon-unlocked')
135 .prop('title', titleUnlocked(scope))
136 ;
137 }
138 };
139
140 $(element).click(function () {
141 binding.assign(scope, !binding(scope));
142 //scope.$digest();
143 $rootScope.$digest();
144 });
145
146 scope.$watch(attrs.binding, refresh);
147 scope.$watch(attrs.titleLocked, refresh);
148 scope.$watch(attrs.titleUnlocked, refresh);
149
150 refresh();
151 }
152 };
153 })
030dce01
TO
154
155 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
156 .directive('crmUiTab', function($parse) {
157 return {
158 scope: {
159 crmTitle: '@'
160 },
161 template: '<div><b>(Tab: {{$parent.$eval(crmTitle)}})</b><span ng-transclude/></div>',
162 transclude: true,
163 link: function (scope, element, attrs) {}
164 };
165 })
166
167 // 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>
168 .directive('crmUiTabSet', function() {
169 return {
170 template: '<div><span ng-transclude/></div>',
171 transclude: true,
172 link: function (scope, element, attrs) {}
173 };
174 })
175
6717e4b9
TO
176 // 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>
177 // Note: "myWizardCtrl" has various actions/properties like next() and $first().
178 // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
179 // WISHLIST: Allow each step to enable/disable (show/hide) itself
030dce01
TO
180 .directive('crmUiWizard', function() {
181 return {
6717e4b9
TO
182 restrict: 'EA',
183 scope: {
184 crmUiWizard: '@'
185 },
186 templateUrl: partialUrl('wizard.html'),
030dce01 187 transclude: true,
6717e4b9
TO
188 controllerAs: 'crmUiWizardCtrl',
189 controller: function($scope, $parse) {
190 var steps = $scope.steps = []; // array<$scope>
191 var crmUiWizardCtrl = this;
192 var maxVisited = 0;
193 var selectedIndex = null;
194
195 var findIndex = function() {
196 var found = null;
197 angular.forEach(steps, function(step, stepKey) {
198 if (step.selected) found = stepKey;
199 });
200 return found;
201 };
202
203 /// @return int the index of the current step
204 this.$index = function() { return selectedIndex; };
205 /// @return bool whether the currentstep is first
206 this.$first = function() { return this.$index() === 0; };
207 /// @return bool whether the current step is last
208 this.$last = function() { return this.$index() === steps.length -1; };
209 this.$maxVisit = function() { return maxVisited; }
210 this.iconFor = function(index) {
211 if (index < this.$index()) return '√';
212 if (index === this.$index()) return '»';
213 return ' ';
214 }
215 this.isSelectable = function(step) {
216 if (step.selected) return false;
217 var result = false;
218 angular.forEach(steps, function(otherStep, otherKey) {
219 if (step === otherStep && otherKey <= maxVisited) result = true;
220 });
221 return result;
222 };
223
224 /*** @param Object step the $scope of the step */
225 this.select = function(step) {
226 angular.forEach(steps, function(otherStep, otherKey) {
227 otherStep.selected = (otherStep === step);
228 if (otherStep === step && maxVisited < otherKey) maxVisited = otherKey;
229 });
230 selectedIndex = findIndex();
231 };
232 /*** @param Object step the $scope of the step */
233 this.add = function(step) {
234 if (steps.length === 0) {
235 step.selected = true;
236 selectedIndex = 0;
237 }
238 steps.push(step);
239 };
240 this.goto = function(index) {
241 if (index < 0) index = 0;
242 if (index >= steps.length) index = steps.length-1;
243 this.select(steps[index]);
244 };
245 this.previous = function() { this.goto(this.$index()-1); };
246 this.next = function() { this.goto(this.$index()+1); };
247 if ($scope.crmUiWizard) {
248 $parse($scope.crmUiWizard).assign($scope.$parent, this)
249 }
250 },
030dce01
TO
251 link: function (scope, element, attrs) {}
252 };
253 })
254
6717e4b9
TO
255 // Use this to add extra markup to wizard
256 .directive('crmUiWizardButtons', function() {
030dce01 257 return {
6717e4b9
TO
258 require: '^crmUiWizard',
259 restrict: 'EA',
260 scope: {},
261 template: '<span ng-transclude></span>',
030dce01 262 transclude: true,
6717e4b9
TO
263 link: function (scope, element, attrs, crmUiWizardCtrl) {
264 var realButtonsEl = $(element).closest('.crm-wizard').find('.crm-wizard-buttons');
265 $(element).appendTo(realButtonsEl);
266 }
030dce01
TO
267 };
268 })
269
270 // example <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
271 .directive('crmUiWizardStep', function() {
272 return {
6717e4b9
TO
273 require: '^crmUiWizard',
274 restrict: 'EA',
030dce01
TO
275 scope: {
276 crmTitle: '@'
277 },
6717e4b9 278 template: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
030dce01 279 transclude: true,
6717e4b9
TO
280 link: function (scope, element, attrs, crmUiWizardCtrl) {
281 crmUiWizardCtrl.add(scope);
282 }
030dce01
TO
283 };
284 })
285
5fb5b3cf 286 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
4b8c8b42
TO
287 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
288 .directive('crmConfirm', function () {
88fcc9f1 289 // Helpers to calculate default options for CRM.confirm()
4b8c8b42
TO
290 var defaultFuncs = {
291 'disable': function (options) {
292 return {
293 message: ts('Are you sure you want to disable this?'),
294 options: {no: ts('Cancel'), yes: ts('Disable')},
295 width: 300,
296 title: ts('Disable %1?', {
297 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
298 })
299 };
300 },
470a458e
TO
301 'revert': function (options) {
302 return {
303 message: ts('Are you sure you want to revert this?'),
304 options: {no: ts('Cancel'), yes: ts('Revert')},
305 width: 300,
306 title: ts('Revert %1?', {
307 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
308 })
309 };
310 },
4b8c8b42
TO
311 'delete': function (options) {
312 return {
313 message: ts('Are you sure you want to delete this?'),
314 options: {no: ts('Cancel'), yes: ts('Delete')},
315 width: 300,
316 title: ts('Delete %1?', {
317 1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
318 })
319 };
320 }
321 };
322 return {
323 template: '',
324 link: function (scope, element, attrs) {
325 $(element).click(function () {
326 var options = scope.$eval(attrs['crmConfirm']);
327 var defaults = (options.type) ? defaultFuncs[options.type](options) : {};
328 CRM.confirm(_.extend(defaults, options))
329 .on('crmConfirm:yes', function () { scope.$apply(attrs['onYes']); })
330 .on('crmConfirm:no', function () { scope.$apply(attrs['onNo']); });
331 });
332 }
333 };
334 })
02308c07
TO
335 .run(function($rootScope, $location) {
336 /// Example: <button ng-click="goto('home')">Go home!</button>
337 $rootScope.goto = function(path) {
338 $location.path(path);
339 };
4b8c8b42 340 // useful for debugging: $rootScope.log = console.log || function() {};
02308c07 341 })
685acae4 342 ;
343
5fb5b3cf 344})(angular, CRM.$, CRM._);