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