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