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