1 /// crmUi: Sundry UI helpers
2 (function (angular
, $, _
) {
6 documentTitle
= 'CiviCRM';
8 angular
.module('crmUi', CRM
.angRequires('crmUi'))
10 // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
11 // WISHLIST: crmCollapsed should support two-way/continuous binding
12 .directive('crmUiAccordion', function() {
17 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>',
19 link: function (scope
, element
, attrs
) {
21 'crm-accordion-wrapper': true,
22 collapsed
: scope
.crmUiAccordion
.collapsed
25 scope
.$watch('crmUiAccordion', function(crmUiAccordion
) {
26 if (crmUiAccordion
&& crmUiAccordion
.help
) {
27 scope
.help
= crmUiAccordion
.help
.clone({}, {
28 title
: crmUiAccordion
.title
37 // crmUiAlert({text: 'My text', title: 'My title', type: 'error'});
38 // crmUiAlert({template: '<a ng-click="ok()">Hello</a>', scope: $scope.$new()});
39 // var h = crmUiAlert({templateUrl: '~/crmFoo/alert.html', scope: $scope.$new()});
41 .service('crmUiAlert', function($compile
, $rootScope
, $templateRequest
, $q
) {
43 return function crmUiAlert(params
) {
44 var id
= 'crmUiAlert_' + (++count
);
46 if (params
.templateUrl
) {
47 tpl
= $templateRequest(params
.templateUrl
);
49 else if (params
.template
) {
50 tpl
= params
.template
;
53 params
.text
= '<div id="' + id
+ '"></div>'; // temporary stub
55 var result
= CRM
.alert(params
.text
, params
.title
, params
.type
, params
.options
);
57 $q
.when(tpl
, function(html
) {
58 var scope
= params
.scope
|| $rootScope
.$new();
59 var linker
= $compile(html
);
60 $('#' + id
).append($(linker(scope
)));
67 // Simple wrapper around $.crmDatepicker.
68 // example with no time input: <input crm-ui-datepicker="{time: false}" ng-model="myobj.datefield"/>
69 // example with custom date format: <input crm-ui-datepicker="{date: 'm/d/y'}" ng-model="myobj.datefield"/>
70 .directive('crmUiDatepicker', function ($timeout
) {
77 link: function (scope
, element
, attrs
, ngModel
) {
78 ngModel
.$render = function () {
79 element
.val(ngModel
.$viewValue
).change();
83 .crmDatepicker(scope
.crmUiDatepicker
)
84 .on('change', function() {
85 // Because change gets triggered from the $render function we could be either inside or outside the $digest cycle
87 var requiredLength
= 19;
88 if (scope
.crmUiDatepicker
&& scope
.crmUiDatepicker
.time
=== false) {
91 if (scope
.crmUiDatepicker
&& scope
.crmUiDatepicker
.date
=== false) {
94 ngModel
.$setValidity('incompleteDateTime', !(element
.val().length
&& element
.val().length
!== requiredLength
));
101 // Display debug information (if available)
102 // For richer DX, checkout Batarang/ng-inspector (Chrome/Safari), or AngScope/ng-inspect (Firefox).
103 // example: <div crm-ui-debug="myobject" />
104 .directive('crmUiDebug', function ($location
) {
110 template: function() {
111 var args
= $location
.search();
112 if (args
&& args
.angularDebug
) {
113 var jsonTpl
= (CRM
.angular
.modules
.indexOf('jsonFormatter') < 0) ? '<pre>{{data|json}}</pre>' : '<json-formatter json="data" open="1"></json-formatter>';
114 return '<div crm-ui-accordion=\'{title: ts("Debug (%1)", {1: crmUiDebug}), collapsed: true}\'>' + jsonTpl
+ '</div>';
118 link: function(scope
, element
, attrs
) {
119 var args
= $location
.search();
120 if (args
&& args
.angularDebug
) {
121 scope
.ts
= CRM
.ts(null);
122 scope
.$parent
.$watch(attrs
.crmUiDebug
, function(data
) {
130 // Display a field/row in a field list
131 // example: <div crm-ui-field="{title: ts('My Field')}"> {{mydata}} </div>
132 // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field')}"> <input crm-ui-id="subform.myfield" name="myfield" /> </div>
133 // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field')}"> <input crm-ui-id="subform.myfield" name="myfield" required /> </div>
134 // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field'), help: hs('help_field_name'), required: true}"> {{mydata}} </div>
135 .directive('crmUiField', function() {
136 // Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
138 default: '~/crmUi/field.html',
139 checkbox
: '~/crmUi/field-cb.html'
143 require
: '^crmUiIdScope',
146 // {title, name, help, helpFile}
149 templateUrl: function(tElement
, tAttrs
){
150 var layout
= tAttrs
.crmLayout
? tAttrs
.crmLayout
: 'default';
151 return templateUrls
[layout
];
154 link: function (scope
, element
, attrs
, crmUiIdCtrl
) {
155 $(element
).addClass('crm-section');
157 scope
.$watch('crmUiField', function(crmUiField
) {
158 if (crmUiField
&& crmUiField
.help
) {
159 scope
.help
= crmUiField
.help
.clone({}, {
160 title
: crmUiField
.title
168 // 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>
169 .directive('crmUiId', function () {
171 require
: '^crmUiIdScope',
174 pre: function (scope
, element
, attrs
, crmUiIdCtrl
) {
175 var id
= crmUiIdCtrl
.get(attrs
.crmUiId
);
176 element
.attr('id', id
);
182 // for example, see crmUiHelp
183 .service('crmUiHelp', function(){
184 // example: var h = new FieldHelp({id: 'foo'}); h.open();
185 function FieldHelp(options
) {
186 this.options
= options
;
188 angular
.extend(FieldHelp
.prototype, {
190 return this.options
[n
];
192 open
: function open() {
193 CRM
.help(this.options
.title
, {id
: this.options
.id
, file
: this.options
.file
});
195 clone
: function clone(options
, defaults
) {
196 return new FieldHelp(angular
.extend({}, defaults
, this.options
, options
));
200 // example: var hs = crmUiHelp({file: 'CRM/Foo/Bar'});
201 return function(defaults
){
202 // example: hs('myfield')
203 // example: hs({id: 'myfield', title: 'Foo Bar', file: 'Whiz/Bang'})
204 return function(options
) {
205 if (_
.isString(options
)) {
206 options
= {id
: options
};
208 return new FieldHelp(angular
.extend({}, defaults
, options
));
213 // Display a help icon
214 // Example: Use a default *.hlp file
215 // scope.hs = crmUiHelp({file: 'Path/To/Help/File'});
216 // HTML: <a crm-ui-help="hs({title:ts('My Field'), id:'my_field'})">
217 // Example: Use an explicit *.hlp file
218 // HTML: <a crm-ui-help="hs({title:ts('My Field'), id:'my_field', file:'CRM/Foo/Bar'})">
219 .directive('crmUiHelp', function() {
222 link: function(scope
, element
, attrs
) {
223 setTimeout(function() {
224 var crmUiHelp
= scope
.$eval(attrs
.crmUiHelp
);
225 var title
= crmUiHelp
&& crmUiHelp
.get('title') ? ts('%1 Help', {1: crmUiHelp
.get('title')}) : ts('Help');
226 element
.attr('title', title
);
230 .addClass('helpicon')
232 .on('click', function(e
) {
234 scope
.$eval(attrs
.crmUiHelp
).open();
240 // 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>
241 .directive('crmUiFor', function ($parse
, $timeout
) {
243 require
: '^crmUiIdScope',
245 template
: '<span ng-class="cssClasses"><span ng-transclude/><span crm-ui-visible="crmIsRequired" class="crm-marker" title="This field is required.">*</span></span>',
247 link: function (scope
, element
, attrs
, crmUiIdCtrl
) {
248 scope
.crmIsRequired
= false;
249 scope
.cssClasses
= {};
251 if (!attrs
.crmUiFor
) return;
253 var id
= crmUiIdCtrl
.get(attrs
.crmUiFor
);
254 element
.attr('for', id
);
257 var updateCss = function () {
258 scope
.cssClasses
['crm-error'] = !ngModel
.$valid
&& !ngModel
.$pristine
;
261 // Note: if target element is dynamically generated (eg via ngInclude), then it may not be available
262 // immediately for initialization. Use retries/retryDelay to initialize such elements.
263 var init = function (retries
, retryDelay
) {
264 var input
= $('#' + id
);
265 if (input
.length
=== 0 && !attrs
.crmUiForceRequired
) {
268 init(retries
-1, retryDelay
);
274 if (attrs
.crmUiForceRequired
) {
275 scope
.crmIsRequired
= true;
279 var tgtScope
= scope
;//.$parent;
280 if (attrs
.crmDepth
) {
281 for (var i
= attrs
.crmDepth
; i
> 0; i
--) {
282 tgtScope
= tgtScope
.$parent
;
286 if (input
.attr('ng-required')) {
287 scope
.crmIsRequired
= scope
.$parent
.$eval(input
.attr('ng-required'));
288 scope
.$parent
.$watch(input
.attr('ng-required'), function (isRequired
) {
289 scope
.crmIsRequired
= isRequired
;
293 scope
.crmIsRequired
= input
.prop('required');
296 ngModel
= $parse(attrs
.crmUiFor
)(tgtScope
);
298 ngModel
.$viewChangeListeners
.push(updateCss
);
309 // Define a scope in which a name like "subform.foo" maps to a unique ID.
310 // 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>
311 .directive('crmUiIdScope', function () {
315 controllerAs
: 'crmUiIdCtrl',
316 controller: function($scope
) {
318 this.get = function(name
) {
320 ids
[name
] = "crmUiId_" + (++uidCount
);
325 link: function (scope
, element
, attrs
) {}
329 // Display an HTML blurb inside an IFRAME.
330 // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
331 // example: <iframe crm-ui-iframe crm-ui-iframe-src="getUrl()"></iframe>
332 .directive('crmUiIframe', function ($parse
) {
335 crmUiIframeSrc
: '@', // expression which evaluates to a URL
336 crmUiIframe
: '@' // expression which evaluates to HTML content
338 link: function (scope
, elm
, attrs
) {
339 var iframe
= $(elm
)[0];
340 iframe
.setAttribute('width', '100%');
341 iframe
.setAttribute('height', '250px');
342 iframe
.setAttribute('frameborder', '0');
344 var refresh = function () {
345 if (attrs
.crmUiIframeSrc
) {
346 iframe
.setAttribute('src', scope
.$parent
.$eval(attrs
.crmUiIframeSrc
));
349 var iframeHtml
= scope
.$parent
.$eval(attrs
.crmUiIframe
);
351 var doc
= iframe
.document
;
352 if (iframe
.contentDocument
) {
353 doc
= iframe
.contentDocument
;
355 else if (iframe
.contentWindow
) {
356 doc
= iframe
.contentWindow
.document
;
360 doc
.writeln(iframeHtml
);
365 // If the iframe is in a dialog, respond to resize events
366 $(elm
).parent().on('dialogresize dialogopen', function(e
, ui
) {
367 $(this).css({padding
: '0', margin
: '0', overflow
: 'hidden'});
368 iframe
.setAttribute('height', '' + $(this).innerHeight() + 'px');
371 $(elm
).parent().on('dialogresize', function(e
, ui
) {
372 iframe
.setAttribute('class', 'resized');
375 scope
.$parent
.$watch(attrs
.crmUiIframe
, refresh
);
381 // <a ng-click="$broadcast('my-insert-target', 'some new text')>Insert</a>
382 // <textarea crm-ui-insert-rx='my-insert-target'></textarea>
383 .directive('crmUiInsertRx', function() {
385 link: function(scope
, element
, attrs
) {
386 scope
.$on(attrs
.crmUiInsertRx
, function(e
, tokenName
) {
387 CRM
.wysiwyg
.insert(element
, tokenName
);
388 $(element
).select2('close').select2('val', '');
389 CRM
.wysiwyg
.focus(element
);
395 // Define a rich text editor.
396 // example: <textarea crm-ui-id="myForm.body_html" crm-ui-richtext name="body_html" ng-model="mailing.body_html"></textarea>
397 .directive('crmUiRichtext', function ($timeout
) {
400 link: function (scope
, elm
, attr
, ngModel
) {
402 var editor
= CRM
.wysiwyg
.create(elm
);
408 $(elm
).on('blur', function() {
409 $timeout(function() {
410 scope
.$eval(attr
.ngBlur
);
415 ngModel
.$render = function(value
) {
416 editor
.done(function() {
417 CRM
.wysiwyg
.setVal(elm
, ngModel
.$viewValue
|| '');
424 // Display a lock icon (based on a boolean).
425 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
426 // example: <a crm-ui-lock
427 // binding="mymodel.boolfield"
428 // title-locked="ts('Boolfield is locked')"
429 // title-unlocked="ts('Boolfield is unlocked')"></a>
430 .directive('crmUiLock', function ($parse
, $rootScope
) {
431 var defaultVal = function (defaultValue
) {
432 var f = function (scope
) {
435 f
.assign = function (scope
, value
) {
441 // like $parse, but accepts a defaultValue in case expr is undefined
442 var parse = function (expr
, defaultValue
) {
443 return expr
? $parse(expr
) : defaultVal(defaultValue
);
448 link: function (scope
, element
, attrs
) {
449 var binding
= parse(attrs
.binding
, true);
450 var titleLocked
= parse(attrs
.titleLocked
, ts('Locked'));
451 var titleUnlocked
= parse(attrs
.titleUnlocked
, ts('Unlocked'));
453 $(element
).addClass('crm-i lock-button');
454 var refresh = function () {
455 var locked
= binding(scope
);
458 .removeClass('fa-unlock')
460 .prop('title', titleLocked(scope
))
465 .removeClass('fa-lock')
466 .addClass('fa-unlock')
467 .prop('title', titleUnlocked(scope
))
472 $(element
).click(function () {
473 binding
.assign(scope
, !binding(scope
));
475 $rootScope
.$digest();
478 scope
.$watch(attrs
.binding
, refresh
);
479 scope
.$watch(attrs
.titleLocked
, refresh
);
480 scope
.$watch(attrs
.titleUnlocked
, refresh
);
487 // CrmUiOrderCtrl is a controller class which manages sort orderings.
489 // JS: $scope.myOrder = new CrmUiOrderCtrl(['+field1', '-field2]);
490 // $scope.myOrder.toggle('field1');
491 // $scope.myOrder.setDir('field2', '');
492 // HTML: <tr ng-repeat="... | order:myOrder.get()">...</tr>
493 .service('CrmUiOrderCtrl', function(){
494 function CrmUiOrderCtrl(defaults
){
495 this.values
= defaults
;
497 angular
.extend(CrmUiOrderCtrl
.prototype, {
498 get: function get() {
501 getDir
: function getDir(name
) {
502 if (this.values
.indexOf(name
) >= 0 || this.values
.indexOf('+' + name
) >= 0) {
505 if (this.values
.indexOf('-' + name
) >= 0) {
510 // @return bool TRUE if something is removed
511 remove
: function remove(name
) {
512 var idx
= this.values
.indexOf(name
);
514 this.values
.splice(idx
, 1);
521 setDir
: function setDir(name
, dir
) {
522 return this.toggle(name
, dir
);
524 // Toggle sort order on a field.
525 // To set a specific order, pass optional parameter 'next' ('+', '-', or '').
526 toggle
: function toggle(name
, next
) {
527 if (!next
&& next
!== '') {
529 if (this.remove(name
) || this.remove('+' + name
)) {
532 if (this.remove('-' + name
)) {
538 this.values
.unshift('+' + name
);
540 else if (next
== '-') {
541 this.values
.unshift('-' + name
);
545 return CrmUiOrderCtrl
;
548 // Define a controller which manages sort order. You may interact with the controller
549 // directly ("myOrder.toggle('fieldname')") order using the helper, crm-ui-order-by.
551 // <span crm-ui-order="{var: 'myOrder', defaults: {'-myField'}}"></span>
552 // <th><a crm-ui-order-by="[myOrder,'myField']">My Field</a></th>
553 // <tr ng-repeat="... | order:myOrder.get()">...</tr>
554 // <button ng-click="myOrder.toggle('myField')">
555 .directive('crmUiOrder', function(CrmUiOrderCtrl
) {
557 link: function(scope
, element
, attrs
){
558 var options
= angular
.extend({var: 'crmUiOrderBy'}, scope
.$eval(attrs
.crmUiOrder
));
559 scope
[options
.var] = new CrmUiOrderCtrl(options
.defaults
);
564 // For usage, see crmUiOrder (above)
565 .directive('crmUiOrderBy', function() {
567 link: function(scope
, element
, attrs
) {
568 function updateClass(crmUiOrderCtrl
, name
) {
569 var dir
= crmUiOrderCtrl
.getDir(name
);
571 .toggleClass('sorting_asc', dir
=== '+')
572 .toggleClass('sorting_desc', dir
=== '-')
573 .toggleClass('sorting', dir
=== '');
576 element
.on('click', function(e
){
577 var tgt
= scope
.$eval(attrs
.crmUiOrderBy
);
578 tgt
[0].toggle(tgt
[1]);
579 updateClass(tgt
[0], tgt
[1]);
584 var tgt
= scope
.$eval(attrs
.crmUiOrderBy
);
585 updateClass(tgt
[0], tgt
[1]);
590 // Display a fancy SELECT (based on select2).
591 // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
592 .directive('crmUiSelect', function ($parse
, $timeout
) {
599 link: function (scope
, element
, attrs
, ngModel
) {
600 // In cases where UI initiates update, there may be an extra
601 // call to refreshUI, but it doesn't create a cycle.
604 ngModel
.$render = function () {
605 $timeout(function () {
606 // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
607 // new item is added before selection is made
608 var newVal
= _
.cloneDeep(ngModel
.$modelValue
);
609 // Fix possible data-type mismatch
610 if (typeof newVal
=== 'string' && element
.select2('container').hasClass('select2-container-multi')) {
611 newVal
= newVal
.length
? newVal
.split(',') : [];
613 element
.select2('val', newVal
);
617 function refreshModel() {
618 var oldValue
= ngModel
.$viewValue
, newValue
= element
.select2('val');
619 if (oldValue
!= newValue
) {
620 scope
.$parent
.$apply(function () {
621 ngModel
.$setViewValue(newValue
);
627 // TODO watch select2-options
628 element
.crmSelect2(scope
.crmUiSelect
|| {});
630 element
.on('change', refreshModel
);
634 // If using ngOptions, wait for them to load
635 if (attrs
.ngOptions
) {
644 // Use a select2 widget as a pick-list. Instead of updating ngModel, the select2 widget will fire an event.
645 // This similar to ngModel+ngChange, except that value is never stored in a model. It is only fired in the event.
646 // usage: <select crm-ui-select='{...}' on-crm-ui-select="alert("User picked this item: " + selection)"></select>
647 .directive('onCrmUiSelect', function () {
650 link: function (scope
, element
, attrs
) {
651 element
.on('select2-selecting', function(e
) {
653 element
.select2('close').select2('val', '');
654 scope
.$apply(function() {
655 scope
.$eval(attrs
.onCrmUiSelect
, {selection
: e
.val
});
662 // Render a crmEntityRef widget
663 // usage: <input crm-entityref="{entity: 'Contact', select: {allowClear:true}}" ng-model="myobj.field" />
664 .directive('crmEntityref', function ($parse
, $timeout
) {
670 link: function (scope
, element
, attrs
, ngModel
) {
671 // In cases where UI initiates update, there may be an extra
672 // call to refreshUI, but it doesn't create a cycle.
674 ngModel
.$render = function () {
675 $timeout(function () {
676 // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
677 // new item is added before selection is made
678 var newVal
= _
.cloneDeep(ngModel
.$modelValue
);
679 // Fix possible data-type mismatch
680 if (typeof newVal
=== 'string' && element
.select2('container').hasClass('select2-container-multi')) {
681 newVal
= newVal
.length
? newVal
.split(',') : [];
683 element
.select2('val', newVal
);
686 function refreshModel() {
687 var oldValue
= ngModel
.$viewValue
, newValue
= element
.select2('val');
688 if (oldValue
!= newValue
) {
689 scope
.$parent
.$apply(function () {
690 ngModel
.$setViewValue(newValue
);
696 // TODO can we infer "entity" from model?
697 element
.crmEntityRef(scope
.crmEntityref
|| {});
698 element
.on('change', refreshModel
);
699 $timeout(ngModel
.$render
);
707 // validate multiple email text
708 // usage: <input crm-multiple-email type="text" ng-model="myobj.field" />
709 .directive('crmMultipleEmail', function ($parse
, $timeout
) {
712 link: function(scope
, element
, attrs
, ctrl
) {
713 ctrl
.$parsers
.unshift(function(viewValue
) {
714 // if empty value provided simply bypass validation
715 if (_
.isEmpty(viewValue
)) {
716 ctrl
.$setValidity('crmMultipleEmail', true);
720 // split email string on basis of comma
721 var emails
= viewValue
.split(',');
722 // regex pattern for single email
723 var emailRegex
= /\S+@\S+\.\S+/;
725 var validityArr
= emails
.map(function(str
){
726 return emailRegex
.test(str
.trim());
729 if ($.inArray(false, validityArr
) > -1) {
730 ctrl
.$setValidity('crmMultipleEmail', false);
732 ctrl
.$setValidity('crmMultipleEmail', true);
739 // example <div crm-ui-tab id="tab-1" crm-title="ts('My Title')" count="3">...content...</div>
740 // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
741 .directive('crmUiTab', function($parse
) {
743 require
: '^crmUiTabSet',
751 template
: '<div ng-transclude></div>',
753 link: function (scope
, element
, attrs
, crmUiTabSetCtrl
) {
754 crmUiTabSetCtrl
.add(scope
);
759 // 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>
760 .directive('crmUiTabSet', function() {
767 templateUrl
: '~/crmUi/tabset.html',
769 controllerAs
: 'crmUiTabSetCtrl',
770 controller: function($scope
, $element
, $timeout
) {
773 this.add = function(tab
) {
774 if (!tab
.id
) throw "Tab is missing 'id'";
775 $scope
.tabs
.push(tab
);
777 // Init jQuery.tabs() once all tabs have been added
779 $timeout
.cancel(init
);
781 init
= $timeout(function() {
782 $element
.find('.crm-tabset').tabs($scope
.tabSetOptions
);
789 // Generic, field-independent form validator.
790 // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" />
791 // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" crm-ui-validate-name="myError" />
792 .directive('crmUiValidate', function() {
796 link: function(scope
, element
, attrs
, ngModel
) {
797 var validationKey
= attrs
.crmUiValidateName
? attrs
.crmUiValidateName
: 'crmUiValidate';
798 scope
.$watch(attrs
.crmUiValidate
, function(newValue
){
799 ngModel
.$setValidity(validationKey
, !!newValue
);
805 // like ng-show, but hides/displays elements using "visibility" which maintains positioning
806 // example <div crm-ui-visible="false">...content...</div>
807 .directive('crmUiVisible', function($parse
) {
813 link: function (scope
, element
, attrs
) {
814 var model
= $parse(attrs
.crmUiVisible
);
815 function updatecChildren() {
816 element
.css('visibility', model(scope
.$parent
) ? 'inherit' : 'hidden');
819 scope
.$parent
.$watch(attrs
.crmUiVisible
, updatecChildren
);
824 // 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>
825 // example with custom nav classes: <div crm-ui-wizard crm-ui-wizard-nav-class="ng-animate-out ...">...</div>
826 // Note: "myWizardCtrl" has various actions/properties like next() and $first().
827 // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
828 // WISHLIST: Allow each step to enable/disable (show/hide) itself
829 .directive('crmUiWizard', function() {
834 crmUiWizardNavClass
: '@' // string, A list of classes that will be added to the nav items
836 templateUrl
: '~/crmUi/wizard.html',
838 controllerAs
: 'crmUiWizardCtrl',
839 controller: function($scope
, $parse
) {
840 var steps
= $scope
.steps
= []; // array<$scope>
841 var crmUiWizardCtrl
= this;
843 var selectedIndex
= null;
845 var findIndex = function() {
847 angular
.forEach(steps
, function(step
, stepKey
) {
848 if (step
.selected
) found
= stepKey
;
853 /// @return int the index of the current step
854 this.$index = function() { return selectedIndex
; };
855 /// @return bool whether the currentstep is first
856 this.$first = function() { return this.$index() === 0; };
857 /// @return bool whether the current step is last
858 this.$last = function() { return this.$index() === steps
.length
-1; };
859 this.$maxVisit = function() { return maxVisited
; };
860 this.$validStep = function() {
861 return steps
[selectedIndex
] && steps
[selectedIndex
].isStepValid();
863 this.iconFor = function(index
) {
864 if (index
< this.$index()) return 'crm-i fa-check';
865 if (index
=== this.$index()) return 'crm-i fa-angle-double-right';
868 this.isSelectable = function(step
) {
869 if (step
.selected
) return false;
870 return this.$validStep();
873 /*** @param Object step the $scope of the step */
874 this.select = function(step
) {
875 angular
.forEach(steps
, function(otherStep
, otherKey
) {
876 otherStep
.selected
= (otherStep
=== step
);
877 if (otherStep
=== step
&& maxVisited
< otherKey
) maxVisited
= otherKey
;
879 selectedIndex
= findIndex();
881 /*** @param Object step the $scope of the step */
882 this.add = function(step
) {
883 if (steps
.length
=== 0) {
884 step
.selected
= true;
888 steps
.sort(function(a
,b
){
889 return a
.crmUiWizardStep
- b
.crmUiWizardStep
;
891 selectedIndex
= findIndex();
893 this.remove = function(step
) {
895 angular
.forEach(steps
, function(otherStep
, otherKey
) {
896 if (otherStep
=== step
) key
= otherKey
;
899 steps
.splice(key
, 1);
902 this.goto = function(index
) {
903 if (index
< 0) index
= 0;
904 if (index
>= steps
.length
) index
= steps
.length
-1;
905 this.select(steps
[index
]);
907 this.previous = function() { this.goto(this.$index()-1); };
908 this.next = function() { this.goto(this.$index()+1); };
909 if ($scope
.crmUiWizard
) {
910 $parse($scope
.crmUiWizard
).assign($scope
.$parent
, this);
913 link: function (scope
, element
, attrs
) {
914 scope
.ts
= CRM
.ts(null);
916 element
.find('.crm-wizard-buttons button[ng-click^=crmUiWizardCtrl]').click(function () {
917 // These values are captured inside the click handler to ensure the
918 // positions/sizes of the elements are captured at the time of the
919 // click vs. at the time this directive is initialized.
920 var topOfWizard
= element
.offset().top
;
921 var heightOfMenu
= $('#civicrm-menu').height() || 0;
924 // stop any other animations that might be happening...
926 // gracefully slide the user to the top of the wizard
927 .animate({scrollTop
: topOfWizard
- heightOfMenu
}, 1000);
933 // Use this to add extra markup to wizard
934 .directive('crmUiWizardButtons', function() {
936 require
: '^crmUiWizard',
939 template
: '<span ng-transclude></span>',
941 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
942 var realButtonsEl
= $(element
).closest('.crm-wizard').find('.crm-wizard-buttons');
943 $(element
).appendTo(realButtonsEl
);
948 // Example for Font Awesome: <button crm-icon="fa-check">Save</button>
949 // Example for jQuery UI (deprecated): <button crm-icon="fa-check">Save</button>
950 .directive('crmIcon', function() {
953 link: function (scope
, element
, attrs
) {
954 if (element
.is('[crm-ui-tab]')) {
955 // handled in crmUiTab ctrl
959 if (attrs
.crmIcon
.substring(0,3) == 'fa-') {
960 $(element
).prepend('<i class="crm-i ' + attrs
.crmIcon
+ '" aria-hidden="true"></i> ');
963 $(element
).prepend('<span class="icon ui-icon-' + attrs
.crmIcon
+ '"></span> ');
967 // Add crm-* class to non-bootstrap buttons
968 if ($(element
).is('button:not(.btn)')) {
969 $(element
).addClass('crm-button');
975 // example: <div crm-ui-wizard-step crm-title="ts('My Title')" ng-form="mySubForm">...content...</div>
976 // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering.
977 // example: <div crm-ui-wizard-step="100" crm-title="..." ng-if="...">...content...</div>
978 // example with custom classes: <div crm-ui-wizard-step="100" crm-ui-wizard-step-class="ng-animate-out ...">...content...</div>
979 .directive('crmUiWizardStep', function() {
982 require
: ['^crmUiWizard', 'form'],
985 crmTitle
: '@', // expression, evaluates to a printable string
986 crmUiWizardStep
: '@', // int, a weight which determines the ordering of the steps
987 crmUiWizardStepClass
: '@' // string, A list of classes that will be added to the template
989 template
: '<div class="crm-wizard-step {{crmUiWizardStepClass}}" ng-show="selected" ng-transclude/></div>',
991 link: function (scope
, element
, attrs
, ctrls
) {
992 var crmUiWizardCtrl
= ctrls
[0], form
= ctrls
[1];
993 if (scope
.crmUiWizardStep
) {
994 scope
.crmUiWizardStep
= parseInt(scope
.crmUiWizardStep
);
996 scope
.crmUiWizardStep
= nextWeight
++;
998 scope
.isStepValid = function() {
1001 crmUiWizardCtrl
.add(scope
);
1002 scope
.$on('$destroy', function(){
1003 crmUiWizardCtrl
.remove(scope
);
1009 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
1010 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
1011 // Example: <button crm-confirm="{templateUrl: '~/path/to/view.html', export: {foo: bar}}" on-yes="frobnicate(123)">Frobincate</button>
1012 .directive('crmConfirm', function ($compile
, $rootScope
, $templateRequest
, $q
) {
1013 // Helpers to calculate default options for CRM.confirm()
1014 var defaultFuncs
= {
1015 'disable': function (options
) {
1017 message
: ts('Are you sure you want to disable this?'),
1018 options
: {no
: ts('Cancel'), yes
: ts('Disable')},
1020 title
: ts('Disable %1?', {
1021 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
1025 'revert': function (options
) {
1027 message
: ts('Are you sure you want to revert this?'),
1028 options
: {no
: ts('Cancel'), yes
: ts('Revert')},
1030 title
: ts('Revert %1?', {
1031 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
1035 'delete': function (options
) {
1037 message
: ts('Are you sure you want to delete this?'),
1038 options
: {no
: ts('Cancel'), yes
: ts('Delete')},
1040 title
: ts('Delete %1?', {
1041 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
1046 var confirmCount
= 0;
1048 link: function (scope
, element
, attrs
) {
1049 $(element
).click(function () {
1050 var options
= scope
.$eval(attrs
.crmConfirm
);
1051 if (attrs
.title
&& !options
.title
) {
1052 options
.title
= attrs
.title
;
1054 var defaults
= (options
.type
) ? defaultFuncs
[options
.type
](options
) : {};
1056 var tpl
= null, stubId
= null;
1057 if (!options
.message
) {
1058 if (options
.templateUrl
) {
1059 tpl
= $templateRequest(options
.templateUrl
);
1061 else if (options
.template
) {
1062 tpl
= options
.template
;
1065 stubId
= 'crmUiConfirm_' + (++confirmCount
);
1066 options
.message
= '<div id="' + stubId
+ '"></div>';
1070 CRM
.confirm(_
.extend(defaults
, options
))
1071 .on('crmConfirm:yes', function() { scope
.$apply(attrs
.onYes
); })
1072 .on('crmConfirm:no', function() { scope
.$apply(attrs
.onNo
); });
1074 if (tpl
&& stubId
) {
1075 $q
.when(tpl
, function(html
) {
1076 var scope
= options
.scope
|| $rootScope
.$new();
1077 if (options
.export) {
1078 angular
.extend(scope
, options
.export);
1080 var linker
= $compile(html
);
1081 $('#' + stubId
).append($(linker(scope
)));
1089 // Sets document title & page title; attempts to override CMS title markup for the latter
1090 // WARNING: Use only once per route!
1091 // WARNING: This directive works only if your AngularJS base page does not
1092 // set a custom title (i.e., it has an initial title of "CiviCRM"). See the
1093 // global variables pageTitle and documentTitle.
1094 // Example (same title for both): <h1 crm-page-title>{{ts('Hello')}}</h1>
1095 // Example (separate document title): <h1 crm-document-title="ts('Hello')" crm-page-title><i class="crm-i fa-flag" aria-hidden="true"></i>{{ts('Hello')}}</h1>
1096 .directive('crmPageTitle', function($timeout
) {
1099 crmDocumentTitle
: '='
1101 link: function(scope
, $el
, attrs
) {
1103 $timeout(function() {
1104 var newPageTitle
= _
.trim($el
.html()),
1105 newDocumentTitle
= scope
.crmDocumentTitle
|| $el
.text(),
1107 dialog
= $el
.closest('.ui-dialog-content');
1108 if (dialog
.length
) {
1109 dialog
.dialog('option', 'title', newDocumentTitle
);
1112 document
.title
= $('title').text().replace(documentTitle
, newDocumentTitle
);
1113 // If the CMS has already added title markup to the page, use it
1114 $('h1').not('.crm-container h1').each(function () {
1115 if ($(this).hasClass('crm-page-title') || _
.trim($(this).html()) === pageTitle
) {
1116 $(this).addClass('crm-page-title').html(newPageTitle
);
1124 pageTitle
= newPageTitle
;
1125 documentTitle
= newDocumentTitle
;
1130 scope
.$watch(function() {return scope
.crmDocumentTitle
+ $el
.html();}, update
);
1135 // Single-line editable text using ngModel & html5 contenteditable
1136 // Supports a `placeholder` attribute which shows up if empty and no `default-value`.
1137 // The `default-value` attribute will force a value if empty (mutually-exclusive with `placeholder`).
1138 // Usage: <span crm-ui-editable ng-model="model.text" placeholder="Enter text"></span>
1139 .directive("crmUiEditable", function() {
1146 link: function(scope
, element
, attrs
, ngModel
) {
1150 var htmlVal
= element
.html();
1152 htmlVal
= scope
.defaultValue
|| '';
1153 element
.text(htmlVal
);
1155 ngModel
.$setViewValue(htmlVal
);
1158 ngModel
.$render = function() {
1159 element
.text(ngModel
.$viewValue
|| scope
.defaultValue
|| '');
1162 // Special handling for enter and escape keys
1163 element
.on('keydown', function(e
) {
1164 // Enter: prevent line break and save
1165 if (e
.which
=== 13) {
1170 if (e
.which
=== 27) {
1171 element
.text(ngModel
.$viewValue
|| scope
.defaultValue
|| '');
1176 element
.on("blur change", function() {
1180 element
.attr('contenteditable', 'true');
1185 // Adds an icon picker widget
1186 // Example: `<input crm-ui-icon-picker ng-model="model.icon">`
1187 .directive('crmUiIconPicker', function($timeout
) {
1190 controller: function($element
) {
1191 CRM
.loadScript(CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmIconPicker.js').then(function() {
1192 $timeout(function() {
1193 $element
.crmIconPicker();
1200 .run(function($rootScope
, $location
) {
1201 /// Example: <button ng-click="goto('home')">Go home!</button>
1202 $rootScope
.goto = function(path
) {
1203 $location
.path(path
);
1205 // useful for debugging: $rootScope.log = console.log || function() {};
1208 })(angular
, CRM
.$, CRM
._
);