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