Update version to 4.6.alpha5
[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
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._);