Commit | Line | Data |
---|---|---|
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._); |