Commit | Line | Data |
---|---|---|
685acae4 | 1 | /// crmUi: Sundry UI helpers |
2 | (function (angular, $, _) { | |
3 | ||
f8601d61 TO |
4 | var uidCount = 0; |
5 | ||
685acae4 | 6 | angular.module('crmUi', []) |
030dce01 | 7 | |
0cbed02c TO |
8 | // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div> |
9 | // WISHLIST: crmCollapsed should support two-way/continous binding | |
030dce01 TO |
10 | .directive('crmUiAccordion', function() { |
11 | return { | |
12 | scope: { | |
0cbed02c TO |
13 | crmTitle: '@', |
14 | crmCollapsed: '@' | |
030dce01 | 15 | }, |
0cbed02c | 16 | 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 | 17 | transclude: true, |
0cbed02c TO |
18 | link: function (scope, element, attrs) { |
19 | scope.cssClasses = { | |
20 | collapsed: scope.$parent.$eval(attrs.crmCollapsed) | |
21 | }; | |
22 | } | |
030dce01 TO |
23 | }; |
24 | }) | |
25 | ||
510d515d TO |
26 | // example: <input crm-ui-date="myobj.datefield" /> |
27 | // example: <input crm-ui-date="myobj.datefield" crm-ui-date-format="yy-mm-dd" /> | |
3074caa9 | 28 | .directive('crmUiDate', function ($parse, $timeout) { |
510d515d TO |
29 | return { |
30 | restrict: 'AE', | |
31 | scope: { | |
32 | crmUiDate: '@', // expression, model binding | |
33 | crmUiDateFormat: '@' // expression, date format (default: "yy-mm-dd") | |
34 | }, | |
35 | link: function (scope, element, attrs) { | |
36 | var fmt = attrs.crmUiDateFormat ? $parse(attrs.crmUiDateFormat)() : "yy-mm-dd"; | |
37 | var model = $parse(attrs.crmUiDate); | |
510d515d TO |
38 | |
39 | element.addClass('dateplugin'); | |
40 | $(element).datepicker({ | |
41 | dateFormat: fmt | |
42 | }); | |
43 | ||
44 | var updateChildren = (function() { | |
3074caa9 TO |
45 | element.off('change', updateParent); |
46 | $(element).datepicker('setDate', model(scope.$parent)); | |
47 | element.on('change', updateParent); | |
510d515d TO |
48 | }); |
49 | var updateParent = (function() { | |
50 | $timeout(function () { | |
51 | model.assign(scope.$parent, $(element).val()); | |
52 | }); | |
53 | }); | |
54 | ||
55 | updateChildren(); | |
3074caa9 TO |
56 | scope.$parent.$watch(attrs.crmUiDate, updateChildren); |
57 | element.on('change', updateParent); | |
510d515d TO |
58 | } |
59 | }; | |
60 | }) | |
61 | ||
62 | // example: <div crm-ui-date-time="myobj.mydatetimefield"></div> | |
3074caa9 | 63 | .directive('crmUiDateTime', function ($parse) { |
510d515d TO |
64 | return { |
65 | restrict: 'AE', | |
66 | scope: { | |
67 | crmUiDateTime: '@' | |
68 | }, | |
69 | template: '<input crm-ui-date="dtparts.date" placeholder="{{dateLabel}}"/> <input crm-ui-time="dtparts.time" placeholder="{{timeLabel}}"/>', | |
70 | link: function (scope, element, attrs) { | |
71 | var model = $parse(attrs.crmUiDateTime); | |
510d515d TO |
72 | scope.dateLabel = ts('Date'); |
73 | scope.timeLabel = ts('Time'); | |
74 | ||
75 | var updateChildren = (function () { | |
76 | var value = model(scope.$parent); | |
77 | if (value) { | |
78 | var dtparts = value.split(/ /); | |
79 | scope.dtparts = {date: dtparts[0], time: dtparts[1]}; | |
80 | } | |
81 | else { | |
82 | scope.dtparts = {date: '', time: ''}; | |
83 | } | |
84 | }); | |
85 | var updateParent = (function () { | |
3074caa9 | 86 | model.assign(scope.$parent, scope.dtparts.date + " " + scope.dtparts.time); |
510d515d TO |
87 | }); |
88 | ||
89 | updateChildren(); | |
3074caa9 | 90 | scope.$parent.$watch(attrs.crmUiDateTime, updateChildren); |
f2bad133 TO |
91 | scope.$watch('dtparts.date', updateParent); |
92 | scope.$watch('dtparts.time', updateParent); | |
510d515d TO |
93 | } |
94 | }; | |
95 | }) | |
96 | ||
489c2674 TO |
97 | // Display a field/row in a field list |
98 | // example: <div crm-ui-field crm-title="My Field"> {{mydata}} </div> | |
f8601d61 TO |
99 | // example: <div crm-ui-field="subform.myfield" crm-title="'My Field'"> <input crm-ui-id="subform.myfield" name="myfield" /> </div> |
100 | // example: <div crm-ui-field="subform.myfield" crm-title="'My Field'"> <input crm-ui-id="subform.myfield" name="myfield" required /> </div> | |
3cc9c048 | 101 | .directive('crmUiField', function() { |
489c2674 TO |
102 | // Note: When writing new templates, the "label" position is particular. See/patch "var label" below. |
103 | var templateUrls = { | |
ef5d18a1 TO |
104 | default: '~/crmUi/field.html', |
105 | checkbox: '~/crmUi/field-cb.html' | |
489c2674 TO |
106 | }; |
107 | ||
108 | return { | |
f8601d61 TO |
109 | require: '^crmUiIdScope', |
110 | restrict: 'EA', | |
489c2674 | 111 | scope: { |
f8601d61 TO |
112 | crmUiField: '@', |
113 | crmTitle: '@' | |
489c2674 TO |
114 | }, |
115 | templateUrl: function(tElement, tAttrs){ | |
116 | var layout = tAttrs.crmLayout ? tAttrs.crmLayout : 'default'; | |
117 | return templateUrls[layout]; | |
118 | }, | |
119 | transclude: true, | |
f8601d61 | 120 | link: function (scope, element, attrs, crmUiIdCtrl) { |
489c2674 | 121 | $(element).addClass('crm-section'); |
489c2674 | 122 | scope.crmUiField = attrs.crmUiField; |
f8601d61 TO |
123 | scope.crmTitle = attrs.crmTitle; |
124 | } | |
125 | }; | |
126 | }) | |
127 | ||
128 | // 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> | |
129 | .directive('crmUiId', function () { | |
130 | return { | |
131 | require: '^crmUiIdScope', | |
132 | restrict: 'EA', | |
3cc9c048 TO |
133 | link: { |
134 | pre: function (scope, element, attrs, crmUiIdCtrl) { | |
135 | var id = crmUiIdCtrl.get(attrs.crmUiId); | |
136 | element.attr('id', id); | |
137 | } | |
f8601d61 TO |
138 | } |
139 | }; | |
140 | }) | |
489c2674 | 141 | |
f8601d61 TO |
142 | // 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> |
143 | .directive('crmUiFor', function ($parse, $timeout) { | |
144 | return { | |
145 | require: '^crmUiIdScope', | |
146 | restrict: 'EA', | |
147 | template: '<span ng-class="cssClasses"><span ng-transclude/><span crm-ui-visible="crmIsRequired" class="crm-marker" title="This field is required.">*</span></span>', | |
148 | transclude: true, | |
149 | link: function (scope, element, attrs, crmUiIdCtrl) { | |
150 | scope.crmIsRequired = false; | |
151 | scope.cssClasses = {}; | |
489c2674 | 152 | |
f8601d61 | 153 | if (!attrs.crmUiFor) return; |
489c2674 | 154 | |
f8601d61 TO |
155 | var id = crmUiIdCtrl.get(attrs.crmUiFor); |
156 | element.attr('for', id); | |
157 | var ngModel = null; | |
489c2674 | 158 | |
f8601d61 TO |
159 | var updateCss = function () { |
160 | scope.cssClasses['crm-error'] = !ngModel.$valid && !ngModel.$pristine; | |
161 | }; | |
489c2674 | 162 | |
f8601d61 TO |
163 | // Note: if target element is dynamically generated (eg via ngInclude), then it may not be available |
164 | // immediately for initialization. Use retries/retryDelay to initialize such elements. | |
165 | var init = function (retries, retryDelay) { | |
166 | var input = $('#' + id); | |
4109d03e | 167 | if (input.length === 0) { |
f8601d61 TO |
168 | if (retries) { |
169 | $timeout(function(){ | |
170 | init(retries-1, retryDelay); | |
171 | }, retryDelay); | |
172 | } | |
173 | return; | |
174 | } | |
489c2674 | 175 | |
f8601d61 TO |
176 | var tgtScope = scope;//.$parent; |
177 | if (attrs.crmDepth) { | |
178 | for (var i = attrs.crmDepth; i > 0; i--) { | |
179 | tgtScope = tgtScope.$parent; | |
180 | } | |
181 | } | |
489c2674 | 182 | |
f8601d61 TO |
183 | if (input.attr('ng-required')) { |
184 | scope.crmIsRequired = scope.$parent.$eval(input.attr('ng-required')); | |
185 | scope.$parent.$watch(input.attr('ng-required'), function (isRequired) { | |
186 | scope.crmIsRequired = isRequired; | |
187 | }); | |
188 | } | |
189 | else { | |
f2bad133 | 190 | scope.crmIsRequired = input.prop('required'); |
f8601d61 | 191 | } |
489c2674 | 192 | |
f8601d61 TO |
193 | ngModel = $parse(attrs.crmUiFor)(tgtScope); |
194 | if (ngModel) { | |
195 | ngModel.$viewChangeListeners.push(updateCss); | |
196 | } | |
197 | }; | |
489c2674 | 198 | |
f8601d61 TO |
199 | $timeout(function(){ |
200 | init(3, 100); | |
489c2674 TO |
201 | }); |
202 | } | |
203 | }; | |
204 | }) | |
205 | ||
f8601d61 TO |
206 | // 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> |
207 | .directive('crmUiIdScope', function () { | |
208 | return { | |
209 | restrict: 'EA', | |
210 | scope: {}, | |
211 | controllerAs: 'crmUiIdCtrl', | |
212 | controller: function($scope) { | |
213 | var ids = {}; | |
214 | this.get = function(name) { | |
215 | if (!ids[name]) { | |
216 | ids[name] = "crmUiId_" + (++uidCount); | |
217 | } | |
218 | return ids[name]; | |
f2bad133 | 219 | }; |
f8601d61 TO |
220 | }, |
221 | link: function (scope, element, attrs) {} | |
222 | }; | |
223 | }) | |
224 | ||
107c5cc7 TO |
225 | // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe> |
226 | .directive('crmUiIframe', function ($parse) { | |
227 | return { | |
228 | scope: { | |
229 | crmUiIframe: '@' // expression which evalutes to HTML content | |
230 | }, | |
231 | link: function (scope, elm, attrs) { | |
232 | var iframe = $(elm)[0]; | |
233 | iframe.setAttribute('width', '100%'); | |
234 | iframe.setAttribute('frameborder', '0'); | |
235 | ||
236 | var refresh = function () { | |
237 | // var iframeHtml = '<html><head><base target="_blank"></head><body onload="parent.document.getElementById(\'' + iframe.id + '\').style.height=document.body.scrollHeight + \'px\'"><scr' + 'ipt type="text/javascript" src="https://gist.github.com/' + iframeId + '.js"></sc' + 'ript></body></html>'; | |
238 | var iframeHtml = scope.$parent.$eval(attrs.crmUiIframe); | |
239 | ||
240 | var doc = iframe.document; | |
241 | if (iframe.contentDocument) { | |
242 | doc = iframe.contentDocument; | |
243 | } | |
244 | else if (iframe.contentWindow) { | |
245 | doc = iframe.contentWindow.document; | |
246 | } | |
247 | ||
248 | doc.open(); | |
249 | doc.writeln(iframeHtml); | |
250 | doc.close(); | |
f2bad133 | 251 | }; |
107c5cc7 TO |
252 | |
253 | scope.$parent.$watch(attrs.crmUiIframe, refresh); | |
254 | //setTimeout(function () { refresh(); }, 50); | |
255 | } | |
256 | }; | |
257 | }) | |
258 | ||
3cc9c048 TO |
259 | // example: <textarea crm-ui-id="myForm.body_html" crm-ui-richtext name="body_html" ng-model="mailing.body_html"></textarea> |
260 | .directive('crmUiRichtext', function ($timeout) { | |
38737af8 TO |
261 | return { |
262 | require: '?ngModel', | |
263 | link: function (scope, elm, attr, ngModel) { | |
38737af8 TO |
264 | var ck = CKEDITOR.replace(elm[0]); |
265 | ||
266 | if (!ngModel) { | |
267 | return; | |
268 | } | |
269 | ||
270 | ck.on('pasteState', function () { | |
271 | scope.$apply(function () { | |
272 | ngModel.$setViewValue(ck.getData()); | |
273 | }); | |
274 | }); | |
275 | ||
276 | ck.on('insertText', function () { | |
277 | $timeout(function () { | |
278 | ngModel.$setViewValue(ck.getData()); | |
279 | }); | |
280 | }); | |
281 | ||
282 | ngModel.$render = function (value) { | |
283 | ck.setData(ngModel.$viewValue); | |
284 | }; | |
285 | } | |
286 | }; | |
287 | }) | |
288 | ||
685acae4 | 289 | // example: <a crm-ui-lock binding="mymodel.boolfield"></a> |
290 | // example: <a crm-ui-lock | |
291 | // binding="mymodel.boolfield" | |
292 | // title-locked="ts('Boolfield is locked')" | |
293 | // title-unlocked="ts('Boolfield is unlocked')"></a> | |
294 | .directive('crmUiLock', function ($parse, $rootScope) { | |
295 | var defaultVal = function (defaultValue) { | |
296 | var f = function (scope) { | |
297 | return defaultValue; | |
f2bad133 | 298 | }; |
685acae4 | 299 | f.assign = function (scope, value) { |
300 | // ignore changes | |
f2bad133 | 301 | }; |
685acae4 | 302 | return f; |
303 | }; | |
304 | ||
305 | // like $parse, but accepts a defaultValue in case expr is undefined | |
306 | var parse = function (expr, defaultValue) { | |
307 | return expr ? $parse(expr) : defaultVal(defaultValue); | |
308 | }; | |
309 | ||
310 | return { | |
311 | template: '', | |
312 | link: function (scope, element, attrs) { | |
f2bad133 TO |
313 | var binding = parse(attrs.binding, true); |
314 | var titleLocked = parse(attrs.titleLocked, ts('Locked')); | |
315 | var titleUnlocked = parse(attrs.titleUnlocked, ts('Unlocked')); | |
685acae4 | 316 | |
317 | $(element).addClass('ui-icon lock-button'); | |
318 | var refresh = function () { | |
319 | var locked = binding(scope); | |
320 | if (locked) { | |
321 | $(element) | |
322 | .removeClass('ui-icon-unlocked') | |
323 | .addClass('ui-icon-locked') | |
324 | .prop('title', titleLocked(scope)) | |
325 | ; | |
326 | } | |
327 | else { | |
328 | $(element) | |
329 | .removeClass('ui-icon-locked') | |
330 | .addClass('ui-icon-unlocked') | |
331 | .prop('title', titleUnlocked(scope)) | |
332 | ; | |
333 | } | |
334 | }; | |
335 | ||
336 | $(element).click(function () { | |
337 | binding.assign(scope, !binding(scope)); | |
338 | //scope.$digest(); | |
339 | $rootScope.$digest(); | |
340 | }); | |
341 | ||
342 | scope.$watch(attrs.binding, refresh); | |
343 | scope.$watch(attrs.titleLocked, refresh); | |
344 | scope.$watch(attrs.titleUnlocked, refresh); | |
345 | ||
346 | refresh(); | |
347 | } | |
348 | }; | |
349 | }) | |
030dce01 | 350 | |
ebfe3efb TO |
351 | // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select> |
352 | .directive('crmUiSelect', function ($parse, $timeout) { | |
8c632f2b | 353 | return { |
ebfe3efb | 354 | require: '?ngModel', |
8c632f2b | 355 | scope: { |
ebfe3efb | 356 | crmUiSelect: '@' |
8c632f2b | 357 | }, |
ebfe3efb | 358 | link: function (scope, element, attrs, ngModel) { |
8c632f2b TO |
359 | // In cases where UI initiates update, there may be an extra |
360 | // call to refreshUI, but it doesn't create a cycle. | |
361 | ||
ebfe3efb TO |
362 | ngModel.$render = function () { |
363 | $timeout(function () { | |
364 | // ex: msg_template_id adds new item then selects it; use $timeout to ensure that | |
365 | // new item is added before selection is made | |
366 | $(element).select2('val', ngModel.$viewValue); | |
367 | }); | |
368 | }; | |
8c632f2b | 369 | function refreshModel() { |
ebfe3efb | 370 | var oldValue = ngModel.$viewValue, newValue = $(element).select2('val'); |
8c632f2b | 371 | if (oldValue != newValue) { |
ebfe3efb TO |
372 | scope.$parent.$apply(function () { |
373 | ngModel.$setViewValue(newValue); | |
8c632f2b | 374 | }); |
8c632f2b TO |
375 | } |
376 | } | |
ebfe3efb | 377 | |
8c632f2b TO |
378 | function init() { |
379 | // TODO watch select2-options | |
380 | var options = attrs.crmUiSelect ? scope.$parent.$eval(attrs.crmUiSelect) : {}; | |
381 | $(element).select2(options); | |
382 | $(element).on('change', refreshModel); | |
ebfe3efb | 383 | $timeout(ngModel.$render); |
8c632f2b | 384 | } |
ebfe3efb | 385 | |
8c632f2b TO |
386 | init(); |
387 | } | |
388 | }; | |
389 | }) | |
390 | ||
030dce01 | 391 | // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div> |
5f3568fd | 392 | // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper |
030dce01 TO |
393 | .directive('crmUiTab', function($parse) { |
394 | return { | |
5f3568fd TO |
395 | require: '^crmUiTabSet', |
396 | restrict: 'EA', | |
030dce01 | 397 | scope: { |
5f3568fd TO |
398 | crmTitle: '@', |
399 | id: '@' | |
030dce01 | 400 | }, |
5f3568fd | 401 | template: '<div ng-transclude></div>', |
030dce01 | 402 | transclude: true, |
5f3568fd TO |
403 | link: function (scope, element, attrs, crmUiTabSetCtrl) { |
404 | crmUiTabSetCtrl.add(scope); | |
405 | } | |
030dce01 TO |
406 | }; |
407 | }) | |
408 | ||
409 | // 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> | |
410 | .directive('crmUiTabSet', function() { | |
411 | return { | |
5f3568fd TO |
412 | restrict: 'EA', |
413 | scope: { | |
414 | crmUiTabSet: '@' | |
415 | }, | |
ef5d18a1 | 416 | templateUrl: '~/crmUi/tabset.html', |
030dce01 | 417 | transclude: true, |
5f3568fd TO |
418 | controllerAs: 'crmUiTabSetCtrl', |
419 | controller: function($scope, $parse) { | |
420 | var tabs = $scope.tabs = []; // array<$scope> | |
421 | this.add = function(tab) { | |
422 | if (!tab.id) throw "Tab is missing 'id'"; | |
423 | tabs.push(tab); | |
424 | }; | |
425 | }, | |
030dce01 TO |
426 | link: function (scope, element, attrs) {} |
427 | }; | |
428 | }) | |
429 | ||
510d515d | 430 | // example: <input crm-ui-time="myobj.mytimefield" /> |
3074caa9 | 431 | .directive('crmUiTime', function ($parse, $timeout) { |
510d515d TO |
432 | return { |
433 | restrict: 'AE', | |
434 | scope: { | |
435 | crmUiTime: '@' | |
436 | }, | |
437 | link: function (scope, element, attrs) { | |
438 | var model = $parse(attrs.crmUiTime); | |
510d515d TO |
439 | |
440 | element.addClass('crm-form-text six'); | |
441 | $(element).timeEntry({show24Hours: true}); | |
442 | ||
443 | var updateChildren = (function() { | |
3074caa9 TO |
444 | element.off('change', updateParent); |
445 | $(element).timeEntry('setTime', model(scope.$parent)); | |
446 | element.on('change', updateParent); | |
510d515d TO |
447 | }); |
448 | var updateParent = (function () { | |
449 | $timeout(function () { | |
450 | model.assign(scope.$parent, element.val()); | |
451 | }); | |
452 | }); | |
453 | ||
454 | updateChildren(); | |
3074caa9 TO |
455 | scope.$parent.$watch(attrs.crmUiTime, updateChildren); |
456 | element.on('change', updateParent); | |
510d515d | 457 | } |
f2bad133 | 458 | }; |
510d515d TO |
459 | }) |
460 | ||
3afb86ef TO |
461 | // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" /> |
462 | // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" crm-ui-validate-name="myError" /> | |
463 | // Generic, field-independent validator. | |
464 | .directive('crmUiValidate', function() { | |
465 | return { | |
466 | restrict: 'EA', | |
467 | require: 'ngModel', | |
468 | link: function(scope, element, attrs, ngModel) { | |
469 | var validationKey = attrs.crmUiValidateName ? attrs.crmUiValidateName : 'crmUiValidate'; | |
470 | scope.$watch(attrs.crmUiValidate, function(newValue){ | |
471 | ngModel.$setValidity(validationKey, !!newValue); | |
472 | }); | |
473 | } | |
474 | }; | |
475 | }) | |
476 | ||
e84a11d8 TO |
477 | // like ng-show, but hides/displays elements using "visibility" which maintains positioning |
478 | // example <div crm-ui-visible="false">...content...</div> | |
479 | .directive('crmUiVisible', function($parse) { | |
480 | return { | |
481 | restrict: 'EA', | |
482 | scope: { | |
483 | crmUiVisible: '@' | |
484 | }, | |
485 | link: function (scope, element, attrs) { | |
486 | var model = $parse(attrs.crmUiVisible); | |
487 | function updatecChildren() { | |
488 | element.css('visibility', model(scope.$parent) ? 'inherit' : 'hidden'); | |
489 | } | |
490 | updatecChildren(); | |
491 | scope.$parent.$watch(attrs.crmUiVisible, updatecChildren); | |
492 | } | |
493 | }; | |
494 | }) | |
495 | ||
6717e4b9 TO |
496 | // 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> |
497 | // Note: "myWizardCtrl" has various actions/properties like next() and $first(). | |
498 | // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable" | |
499 | // WISHLIST: Allow each step to enable/disable (show/hide) itself | |
030dce01 TO |
500 | .directive('crmUiWizard', function() { |
501 | return { | |
6717e4b9 TO |
502 | restrict: 'EA', |
503 | scope: { | |
504 | crmUiWizard: '@' | |
505 | }, | |
ef5d18a1 | 506 | templateUrl: '~/crmUi/wizard.html', |
030dce01 | 507 | transclude: true, |
6717e4b9 TO |
508 | controllerAs: 'crmUiWizardCtrl', |
509 | controller: function($scope, $parse) { | |
510 | var steps = $scope.steps = []; // array<$scope> | |
511 | var crmUiWizardCtrl = this; | |
512 | var maxVisited = 0; | |
513 | var selectedIndex = null; | |
514 | ||
515 | var findIndex = function() { | |
516 | var found = null; | |
517 | angular.forEach(steps, function(step, stepKey) { | |
518 | if (step.selected) found = stepKey; | |
519 | }); | |
520 | return found; | |
521 | }; | |
522 | ||
523 | /// @return int the index of the current step | |
524 | this.$index = function() { return selectedIndex; }; | |
525 | /// @return bool whether the currentstep is first | |
526 | this.$first = function() { return this.$index() === 0; }; | |
527 | /// @return bool whether the current step is last | |
528 | this.$last = function() { return this.$index() === steps.length -1; }; | |
f2bad133 | 529 | this.$maxVisit = function() { return maxVisited; }; |
3f0da451 TO |
530 | this.$validStep = function() { |
531 | return steps[selectedIndex].isStepValid(); | |
532 | }; | |
6717e4b9 TO |
533 | this.iconFor = function(index) { |
534 | if (index < this.$index()) return '√'; | |
535 | if (index === this.$index()) return '»'; | |
536 | return ' '; | |
f2bad133 | 537 | }; |
6717e4b9 TO |
538 | this.isSelectable = function(step) { |
539 | if (step.selected) return false; | |
540 | var result = false; | |
541 | angular.forEach(steps, function(otherStep, otherKey) { | |
542 | if (step === otherStep && otherKey <= maxVisited) result = true; | |
543 | }); | |
544 | return result; | |
545 | }; | |
546 | ||
547 | /*** @param Object step the $scope of the step */ | |
548 | this.select = function(step) { | |
549 | angular.forEach(steps, function(otherStep, otherKey) { | |
550 | otherStep.selected = (otherStep === step); | |
551 | if (otherStep === step && maxVisited < otherKey) maxVisited = otherKey; | |
552 | }); | |
553 | selectedIndex = findIndex(); | |
554 | }; | |
555 | /*** @param Object step the $scope of the step */ | |
556 | this.add = function(step) { | |
557 | if (steps.length === 0) { | |
558 | step.selected = true; | |
559 | selectedIndex = 0; | |
560 | } | |
561 | steps.push(step); | |
8688b463 TO |
562 | steps.sort(function(a,b){ |
563 | return a.crmUiWizardStep - b.crmUiWizardStep; | |
564 | }); | |
565 | selectedIndex = findIndex(); | |
566 | }; | |
567 | this.remove = function(step) { | |
568 | var key = null; | |
569 | angular.forEach(steps, function(otherStep, otherKey) { | |
570 | if (otherStep === step) key = otherKey; | |
571 | }); | |
4109d03e | 572 | if (key !== null) { |
8688b463 TO |
573 | steps.splice(key, 1); |
574 | } | |
6717e4b9 TO |
575 | }; |
576 | this.goto = function(index) { | |
577 | if (index < 0) index = 0; | |
578 | if (index >= steps.length) index = steps.length-1; | |
579 | this.select(steps[index]); | |
580 | }; | |
581 | this.previous = function() { this.goto(this.$index()-1); }; | |
582 | this.next = function() { this.goto(this.$index()+1); }; | |
583 | if ($scope.crmUiWizard) { | |
f2bad133 | 584 | $parse($scope.crmUiWizard).assign($scope.$parent, this); |
6717e4b9 TO |
585 | } |
586 | }, | |
030dce01 TO |
587 | link: function (scope, element, attrs) {} |
588 | }; | |
589 | }) | |
590 | ||
6717e4b9 TO |
591 | // Use this to add extra markup to wizard |
592 | .directive('crmUiWizardButtons', function() { | |
030dce01 | 593 | return { |
6717e4b9 TO |
594 | require: '^crmUiWizard', |
595 | restrict: 'EA', | |
596 | scope: {}, | |
597 | template: '<span ng-transclude></span>', | |
030dce01 | 598 | transclude: true, |
6717e4b9 TO |
599 | link: function (scope, element, attrs, crmUiWizardCtrl) { |
600 | var realButtonsEl = $(element).closest('.crm-wizard').find('.crm-wizard-buttons'); | |
601 | $(element).appendTo(realButtonsEl); | |
602 | } | |
030dce01 TO |
603 | }; |
604 | }) | |
605 | ||
3f0da451 | 606 | // example: <div crm-ui-wizard-step crm-title="ts('My Title')" ng-form="mySubForm">...content...</div> |
8688b463 TO |
607 | // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering. |
608 | // example: <div crm-ui-wizard-step="100" crm-title="..." ng-if="...">...content...</div> | |
030dce01 | 609 | .directive('crmUiWizardStep', function() { |
8688b463 | 610 | var nextWeight = 1; |
030dce01 | 611 | return { |
3f0da451 | 612 | require: ['^crmUiWizard', 'form'], |
6717e4b9 | 613 | restrict: 'EA', |
030dce01 | 614 | scope: { |
8688b463 TO |
615 | crmTitle: '@', // expression, evaluates to a printable string |
616 | crmUiWizardStep: '@' // int, a weight which determines the ordering of the steps | |
030dce01 | 617 | }, |
6717e4b9 | 618 | template: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>', |
030dce01 | 619 | transclude: true, |
3f0da451 TO |
620 | link: function (scope, element, attrs, ctrls) { |
621 | var crmUiWizardCtrl = ctrls[0], form = ctrls[1]; | |
8688b463 TO |
622 | if (scope.crmUiWizardStep) { |
623 | scope.crmUiWizardStep = parseInt(scope.crmUiWizardStep); | |
624 | } else { | |
625 | scope.crmUiWizardStep = nextWeight++; | |
626 | } | |
3f0da451 TO |
627 | scope.isStepValid = function() { |
628 | return form.$valid; | |
629 | }; | |
6717e4b9 | 630 | crmUiWizardCtrl.add(scope); |
8688b463 TO |
631 | element.on('$destroy', function(){ |
632 | crmUiWizardCtrl.remove(scope); | |
633 | }); | |
6717e4b9 | 634 | } |
030dce01 TO |
635 | }; |
636 | }) | |
637 | ||
5fb5b3cf | 638 | // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button> |
4b8c8b42 TO |
639 | // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button> |
640 | .directive('crmConfirm', function () { | |
88fcc9f1 | 641 | // Helpers to calculate default options for CRM.confirm() |
4b8c8b42 TO |
642 | var defaultFuncs = { |
643 | 'disable': function (options) { | |
644 | return { | |
645 | message: ts('Are you sure you want to disable this?'), | |
646 | options: {no: ts('Cancel'), yes: ts('Disable')}, | |
647 | width: 300, | |
648 | title: ts('Disable %1?', { | |
649 | 1: options.obj.title || options.obj.label || options.obj.name || ts('the record') | |
650 | }) | |
651 | }; | |
652 | }, | |
470a458e TO |
653 | 'revert': function (options) { |
654 | return { | |
655 | message: ts('Are you sure you want to revert this?'), | |
656 | options: {no: ts('Cancel'), yes: ts('Revert')}, | |
657 | width: 300, | |
658 | title: ts('Revert %1?', { | |
659 | 1: options.obj.title || options.obj.label || options.obj.name || ts('the record') | |
660 | }) | |
661 | }; | |
662 | }, | |
4b8c8b42 TO |
663 | 'delete': function (options) { |
664 | return { | |
665 | message: ts('Are you sure you want to delete this?'), | |
666 | options: {no: ts('Cancel'), yes: ts('Delete')}, | |
667 | width: 300, | |
668 | title: ts('Delete %1?', { | |
669 | 1: options.obj.title || options.obj.label || options.obj.name || ts('the record') | |
670 | }) | |
671 | }; | |
672 | } | |
673 | }; | |
674 | return { | |
675 | template: '', | |
676 | link: function (scope, element, attrs) { | |
677 | $(element).click(function () { | |
f2bad133 | 678 | var options = scope.$eval(attrs.crmConfirm); |
4b8c8b42 TO |
679 | var defaults = (options.type) ? defaultFuncs[options.type](options) : {}; |
680 | CRM.confirm(_.extend(defaults, options)) | |
f2bad133 TO |
681 | .on('crmConfirm:yes', function () { scope.$apply(attrs.onYes); }) |
682 | .on('crmConfirm:no', function () { scope.$apply(attrs.onNo); }); | |
4b8c8b42 TO |
683 | }); |
684 | } | |
685 | }; | |
686 | }) | |
02308c07 TO |
687 | .run(function($rootScope, $location) { |
688 | /// Example: <button ng-click="goto('home')">Go home!</button> | |
689 | $rootScope.goto = function(path) { | |
690 | $location.path(path); | |
691 | }; | |
4b8c8b42 | 692 | // useful for debugging: $rootScope.log = console.log || function() {}; |
02308c07 | 693 | }) |
685acae4 | 694 | ; |
695 | ||
5fb5b3cf | 696 | })(angular, CRM.$, CRM._); |