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