1 /// crmUi: Sundry UI helpers
2 (function (angular
, $, _
) {
6 angular
.module('crmUi', [])
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() {
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>',
17 link: function (scope
, element
, attrs
) {
19 'crm-accordion-wrapper': true,
20 collapsed
: scope
.crmUiAccordion
.collapsed
23 scope
.$watch('crmUiAccordion', function(crmUiAccordion
) {
24 if (crmUiAccordion
&& crmUiAccordion
.help
) {
25 scope
.help
= crmUiAccordion
.help
.clone({}, {
26 title
: crmUiAccordion
.title
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()});
39 .service('crmUiAlert', function($compile
, $rootScope
, $templateRequest
, $q
) {
41 return function crmUiAlert(params
) {
42 var id
= 'crmUiAlert_' + (++count
);
44 if (params
.templateUrl
) {
45 tpl
= $templateRequest(params
.templateUrl
);
47 else if (params
.template
) {
48 tpl
= params
.template
;
51 params
.text
= '<div id="' + id
+ '"></div>'; // temporary stub
53 var result
= CRM
.alert(params
.text
, params
.title
, params
.type
, params
.options
);
55 $q
.when(tpl
, function(html
) {
56 var scope
= params
.scope
|| $rootScope
.$new();
57 var linker
= $compile(html
);
58 $('#' + id
).append($(linker(scope
)));
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 () {
75 link: function (scope
, element
, attrs
, ngModel
) {
76 ngModel
.$render = function () {
77 element
.val(ngModel
.$viewValue
).change();
81 .crmDatepicker(scope
.crmUiDatepicker
)
82 .on('change', function() {
83 var requiredLength
= 19;
84 if (scope
.crmUiDatepicker
&& scope
.crmUiDatepicker
.time
=== false) {
87 if (scope
.crmUiDatepicker
&& scope
.crmUiDatepicker
.date
=== false) {
90 ngModel
.$setValidity('incompleteDateTime', !($(this).val().length
&& $(this).val().length
!== requiredLength
));
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
) {
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>' : '';
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
) {
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.
129 default: '~/crmUi/field.html',
130 checkbox
: '~/crmUi/field-cb.html'
134 require
: '^crmUiIdScope',
137 // {title, name, help, helpFile}
140 templateUrl: function(tElement
, tAttrs
){
141 var layout
= tAttrs
.crmLayout
? tAttrs
.crmLayout
: 'default';
142 return templateUrls
[layout
];
145 link: function (scope
, element
, attrs
, crmUiIdCtrl
) {
146 $(element
).addClass('crm-section');
148 scope
.$watch('crmUiField', function(crmUiField
) {
149 if (crmUiField
&& crmUiField
.help
) {
150 scope
.help
= crmUiField
.help
.clone({}, {
151 title
: crmUiField
.title
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 () {
162 require
: '^crmUiIdScope',
165 pre: function (scope
, element
, attrs
, crmUiIdCtrl
) {
166 var id
= crmUiIdCtrl
.get(attrs
.crmUiId
);
167 element
.attr('id', id
);
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
;
179 angular
.extend(FieldHelp
.prototype, {
181 return this.options
[n
];
183 open
: function open() {
184 CRM
.help(this.options
.title
, {id
: this.options
.id
, file
: this.options
.file
});
186 clone
: function clone(options
, defaults
) {
187 return new FieldHelp(angular
.extend({}, defaults
, this.options
, options
));
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
};
199 return new FieldHelp(angular
.extend({}, defaults
, options
));
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() {
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
);
221 .addClass('helpicon')
223 .on('click', function(e
) {
225 scope
.$eval(attrs
.crmUiHelp
).open();
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
) {
234 require
: '^crmUiIdScope',
236 template
: '<span ng-class="cssClasses"><span ng-transclude/><span crm-ui-visible="crmIsRequired" class="crm-marker" title="This field is required.">*</span></span>',
238 link: function (scope
, element
, attrs
, crmUiIdCtrl
) {
239 scope
.crmIsRequired
= false;
240 scope
.cssClasses
= {};
242 if (!attrs
.crmUiFor
) return;
244 var id
= crmUiIdCtrl
.get(attrs
.crmUiFor
);
245 element
.attr('for', id
);
248 var updateCss = function () {
249 scope
.cssClasses
['crm-error'] = !ngModel
.$valid
&& !ngModel
.$pristine
;
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) {
259 init(retries
-1, retryDelay
);
265 var tgtScope
= scope
;//.$parent;
266 if (attrs
.crmDepth
) {
267 for (var i
= attrs
.crmDepth
; i
> 0; i
--) {
268 tgtScope
= tgtScope
.$parent
;
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
;
279 scope
.crmIsRequired
= input
.prop('required');
282 ngModel
= $parse(attrs
.crmUiFor
)(tgtScope
);
284 ngModel
.$viewChangeListeners
.push(updateCss
);
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 () {
301 controllerAs
: 'crmUiIdCtrl',
302 controller: function($scope
) {
304 this.get = function(name
) {
306 ids
[name
] = "crmUiId_" + (++uidCount
);
311 link: function (scope
, element
, attrs
) {}
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
) {
321 crmUiIframeSrc
: '@', // expression which evaluates to a URL
322 crmUiIframe
: '@' // expression which evaluates to HTML content
324 link: function (scope
, elm
, attrs
) {
325 var iframe
= $(elm
)[0];
326 iframe
.setAttribute('width', '100%');
327 iframe
.setAttribute('frameborder', '0');
329 var refresh = function () {
330 if (attrs
.crmUiIframeSrc
) {
331 iframe
.setAttribute('src', scope
.$parent
.$eval(attrs
.crmUiIframeSrc
));
334 var iframeHtml
= scope
.$parent
.$eval(attrs
.crmUiIframe
);
336 var doc
= iframe
.document
;
337 if (iframe
.contentDocument
) {
338 doc
= iframe
.contentDocument
;
340 else if (iframe
.contentWindow
) {
341 doc
= iframe
.contentWindow
.document
;
345 doc
.writeln(iframeHtml
);
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');
356 scope
.$parent
.$watch(attrs
.crmUiIframe
, refresh
);
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() {
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
);
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
) {
381 link: function (scope
, elm
, attr
, ngModel
) {
383 var editor
= CRM
.wysiwyg
.create(elm
);
389 $(elm
).on('blur', function() {
390 $timeout(function() {
391 scope
.$eval(attr
.ngBlur
);
396 ngModel
.$render = function(value
) {
397 CRM
.wysiwyg
.setVal(elm
, ngModel
.$viewValue
);
403 // Display a lock icon (based on a boolean).
404 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
405 // example: <a crm-ui-lock
406 // binding="mymodel.boolfield"
407 // title-locked="ts('Boolfield is locked')"
408 // title-unlocked="ts('Boolfield is unlocked')"></a>
409 .directive('crmUiLock', function ($parse
, $rootScope
) {
410 var defaultVal = function (defaultValue
) {
411 var f = function (scope
) {
414 f
.assign = function (scope
, value
) {
420 // like $parse, but accepts a defaultValue in case expr is undefined
421 var parse = function (expr
, defaultValue
) {
422 return expr
? $parse(expr
) : defaultVal(defaultValue
);
427 link: function (scope
, element
, attrs
) {
428 var binding
= parse(attrs
.binding
, true);
429 var titleLocked
= parse(attrs
.titleLocked
, ts('Locked'));
430 var titleUnlocked
= parse(attrs
.titleUnlocked
, ts('Unlocked'));
432 $(element
).addClass('ui-icon lock-button');
433 var refresh = function () {
434 var locked
= binding(scope
);
437 .removeClass('ui-icon-unlocked')
438 .addClass('ui-icon-locked')
439 .prop('title', titleLocked(scope
))
444 .removeClass('ui-icon-locked')
445 .addClass('ui-icon-unlocked')
446 .prop('title', titleUnlocked(scope
))
451 $(element
).click(function () {
452 binding
.assign(scope
, !binding(scope
));
454 $rootScope
.$digest();
457 scope
.$watch(attrs
.binding
, refresh
);
458 scope
.$watch(attrs
.titleLocked
, refresh
);
459 scope
.$watch(attrs
.titleUnlocked
, refresh
);
466 // CrmUiOrderCtrl is a controller class which manages sort orderings.
468 // JS: $scope.myOrder = new CrmUiOrderCtrl(['+field1', '-field2]);
469 // $scope.myOrder.toggle('field1');
470 // $scope.myOrder.setDir('field2', '');
471 // HTML: <tr ng-repeat="... | order:myOrder.get()">...</tr>
472 .service('CrmUiOrderCtrl', function(){
474 function CrmUiOrderCtrl(defaults
){
475 this.values
= defaults
;
477 angular
.extend(CrmUiOrderCtrl
.prototype, {
478 get: function get() {
481 getDir
: function getDir(name
) {
482 if (this.values
.indexOf(name
) >= 0 || this.values
.indexOf('+' + name
) >= 0) {
485 if (this.values
.indexOf('-' + name
) >= 0) {
490 // @return bool TRUE if something is removed
491 remove
: function remove(name
) {
492 var idx
= this.values
.indexOf(name
);
494 this.values
.splice(idx
, 1);
501 setDir
: function setDir(name
, dir
) {
502 return this.toggle(name
, dir
);
504 // Toggle sort order on a field.
505 // To set a specific order, pass optional parameter 'next' ('+', '-', or '').
506 toggle
: function toggle(name
, next
) {
507 if (!next
&& next
!== '') {
509 if (this.remove(name
) || this.remove('+' + name
)) {
512 if (this.remove('-' + name
)) {
518 this.values
.unshift('+' + name
);
520 else if (next
== '-') {
521 this.values
.unshift('-' + name
);
525 return CrmUiOrderCtrl
;
528 // Define a controller which manages sort order. You may interact with the controller
529 // directly ("myOrder.toggle('fieldname')") order using the helper, crm-ui-order-by.
531 // <span crm-ui-order="{var: 'myOrder', defaults: {'-myField'}}"></span>
532 // <th><a crm-ui-order-by="[myOrder,'myField']">My Field</a></th>
533 // <tr ng-repeat="... | order:myOrder.get()">...</tr>
534 // <button ng-click="myOrder.toggle('myField')">
535 .directive('crmUiOrder', function(CrmUiOrderCtrl
) {
537 link: function(scope
, element
, attrs
){
538 var options
= angular
.extend({var: 'crmUiOrderBy'}, scope
.$eval(attrs
.crmUiOrder
));
539 scope
[options
.var] = new CrmUiOrderCtrl(options
.defaults
);
544 // For usage, see crmUiOrder (above)
545 .directive('crmUiOrderBy', function() {
547 link: function(scope
, element
, attrs
) {
548 function updateClass(crmUiOrderCtrl
, name
) {
549 var dir
= crmUiOrderCtrl
.getDir(name
);
551 .toggleClass('sorting_asc', dir
=== '+')
552 .toggleClass('sorting_desc', dir
=== '-')
553 .toggleClass('sorting', dir
=== '');
556 element
.on('click', function(e
){
557 var tgt
= scope
.$eval(attrs
.crmUiOrderBy
);
558 tgt
[0].toggle(tgt
[1]);
559 updateClass(tgt
[0], tgt
[1]);
564 var tgt
= scope
.$eval(attrs
.crmUiOrderBy
);
565 updateClass(tgt
[0], tgt
[1]);
570 // Display a fancy SELECT (based on select2).
571 // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
572 .directive('crmUiSelect', function ($parse
, $timeout
) {
578 link: function (scope
, element
, attrs
, ngModel
) {
579 // In cases where UI initiates update, there may be an extra
580 // call to refreshUI, but it doesn't create a cycle.
583 ngModel
.$render = function () {
584 $timeout(function () {
585 // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
586 // new item is added before selection is made
587 element
.select2('val', ngModel
.$viewValue
);
591 function refreshModel() {
592 var oldValue
= ngModel
.$viewValue
, newValue
= element
.select2('val');
593 if (oldValue
!= newValue
) {
594 scope
.$parent
.$apply(function () {
595 ngModel
.$setViewValue(newValue
);
601 // TODO watch select2-options
602 element
.select2(scope
.crmUiSelect
|| {});
604 element
.on('change', refreshModel
);
605 $timeout(ngModel
.$render
);
614 // Render a crmEntityRef widget
615 // usage: <input crm-entityref="{entity: 'Contact', select: {allowClear:true}}" ng-model="myobj.field" />
616 .directive('crmEntityref', function ($parse
, $timeout
) {
622 link: function (scope
, element
, attrs
, ngModel
) {
623 // In cases where UI initiates update, there may be an extra
624 // call to refreshUI, but it doesn't create a cycle.
626 ngModel
.$render = function () {
627 $timeout(function () {
628 // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
629 // new item is added before selection is made
630 element
.select2('val', ngModel
.$viewValue
);
633 function refreshModel() {
634 var oldValue
= ngModel
.$viewValue
, newValue
= element
.select2('val');
635 if (oldValue
!= newValue
) {
636 scope
.$parent
.$apply(function () {
637 ngModel
.$setViewValue(newValue
);
643 // TODO can we infer "entity" from model?
644 element
.crmEntityRef(scope
.crmEntityref
|| {});
645 element
.on('change', refreshModel
);
646 $timeout(ngModel
.$render
);
654 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
655 // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
656 .directive('crmUiTab', function($parse
) {
658 require
: '^crmUiTabSet',
664 template
: '<div ng-transclude></div>',
666 link: function (scope
, element
, attrs
, crmUiTabSetCtrl
) {
667 crmUiTabSetCtrl
.add(scope
);
672 // 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>
673 .directive('crmUiTabSet', function() {
679 templateUrl
: '~/crmUi/tabset.html',
681 controllerAs
: 'crmUiTabSetCtrl',
682 controller: function($scope
, $parse
) {
683 var tabs
= $scope
.tabs
= []; // array<$scope>
684 this.add = function(tab
) {
685 if (!tab
.id
) throw "Tab is missing 'id'";
689 link: function (scope
, element
, attrs
) {}
693 // Generic, field-independent form validator.
694 // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" />
695 // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" crm-ui-validate-name="myError" />
696 .directive('crmUiValidate', function() {
700 link: function(scope
, element
, attrs
, ngModel
) {
701 var validationKey
= attrs
.crmUiValidateName
? attrs
.crmUiValidateName
: 'crmUiValidate';
702 scope
.$watch(attrs
.crmUiValidate
, function(newValue
){
703 ngModel
.$setValidity(validationKey
, !!newValue
);
709 // like ng-show, but hides/displays elements using "visibility" which maintains positioning
710 // example <div crm-ui-visible="false">...content...</div>
711 .directive('crmUiVisible', function($parse
) {
717 link: function (scope
, element
, attrs
) {
718 var model
= $parse(attrs
.crmUiVisible
);
719 function updatecChildren() {
720 element
.css('visibility', model(scope
.$parent
) ? 'inherit' : 'hidden');
723 scope
.$parent
.$watch(attrs
.crmUiVisible
, updatecChildren
);
728 // 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>
729 // Note: "myWizardCtrl" has various actions/properties like next() and $first().
730 // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
731 // WISHLIST: Allow each step to enable/disable (show/hide) itself
732 .directive('crmUiWizard', function() {
738 templateUrl
: '~/crmUi/wizard.html',
740 controllerAs
: 'crmUiWizardCtrl',
741 controller: function($scope
, $parse
) {
742 var steps
= $scope
.steps
= []; // array<$scope>
743 var crmUiWizardCtrl
= this;
745 var selectedIndex
= null;
747 var findIndex = function() {
749 angular
.forEach(steps
, function(step
, stepKey
) {
750 if (step
.selected
) found
= stepKey
;
755 /// @return int the index of the current step
756 this.$index = function() { return selectedIndex
; };
757 /// @return bool whether the currentstep is first
758 this.$first = function() { return this.$index() === 0; };
759 /// @return bool whether the current step is last
760 this.$last = function() { return this.$index() === steps
.length
-1; };
761 this.$maxVisit = function() { return maxVisited
; };
762 this.$validStep = function() {
763 return steps
[selectedIndex
] && steps
[selectedIndex
].isStepValid();
765 this.iconFor = function(index
) {
766 if (index
< this.$index()) return 'โ';
767 if (index
=== this.$index()) return 'ยป';
770 this.isSelectable = function(step
) {
771 if (step
.selected
) return false;
773 angular
.forEach(steps
, function(otherStep
, otherKey
) {
774 if (step
=== otherStep
&& otherKey
<= maxVisited
) result
= true;
779 /*** @param Object step the $scope of the step */
780 this.select = function(step
) {
781 angular
.forEach(steps
, function(otherStep
, otherKey
) {
782 otherStep
.selected
= (otherStep
=== step
);
783 if (otherStep
=== step
&& maxVisited
< otherKey
) maxVisited
= otherKey
;
785 selectedIndex
= findIndex();
787 /*** @param Object step the $scope of the step */
788 this.add = function(step
) {
789 if (steps
.length
=== 0) {
790 step
.selected
= true;
794 steps
.sort(function(a
,b
){
795 return a
.crmUiWizardStep
- b
.crmUiWizardStep
;
797 selectedIndex
= findIndex();
799 this.remove = function(step
) {
801 angular
.forEach(steps
, function(otherStep
, otherKey
) {
802 if (otherStep
=== step
) key
= otherKey
;
805 steps
.splice(key
, 1);
808 this.goto = function(index
) {
809 if (index
< 0) index
= 0;
810 if (index
>= steps
.length
) index
= steps
.length
-1;
811 this.select(steps
[index
]);
813 this.previous = function() { this.goto(this.$index()-1); };
814 this.next = function() { this.goto(this.$index()+1); };
815 if ($scope
.crmUiWizard
) {
816 $parse($scope
.crmUiWizard
).assign($scope
.$parent
, this);
819 link: function (scope
, element
, attrs
) {
820 scope
.ts
= CRM
.ts(null);
825 // Use this to add extra markup to wizard
826 .directive('crmUiWizardButtons', function() {
828 require
: '^crmUiWizard',
831 template
: '<span ng-transclude></span>',
833 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
834 var realButtonsEl
= $(element
).closest('.crm-wizard').find('.crm-wizard-buttons');
835 $(element
).appendTo(realButtonsEl
);
840 // Example: <button crm-icon="check">Save</button>
841 .directive('crmIcon', function() {
845 link: function (scope
, element
, attrs
) {
846 $(element
).prepend('<span class="icon ui-icon-' + attrs
.crmIcon
+ '"></span> ');
847 if ($(element
).is('button')) {
848 $(element
).addClass('crm-button');
854 // example: <div crm-ui-wizard-step crm-title="ts('My Title')" ng-form="mySubForm">...content...</div>
855 // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering.
856 // example: <div crm-ui-wizard-step="100" crm-title="..." ng-if="...">...content...</div>
857 .directive('crmUiWizardStep', function() {
860 require
: ['^crmUiWizard', 'form'],
863 crmTitle
: '@', // expression, evaluates to a printable string
864 crmUiWizardStep
: '@' // int, a weight which determines the ordering of the steps
866 template
: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
868 link: function (scope
, element
, attrs
, ctrls
) {
869 var crmUiWizardCtrl
= ctrls
[0], form
= ctrls
[1];
870 if (scope
.crmUiWizardStep
) {
871 scope
.crmUiWizardStep
= parseInt(scope
.crmUiWizardStep
);
873 scope
.crmUiWizardStep
= nextWeight
++;
875 scope
.isStepValid = function() {
878 crmUiWizardCtrl
.add(scope
);
879 element
.on('$destroy', function(){
880 crmUiWizardCtrl
.remove(scope
);
886 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
887 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
888 // Example: <button crm-confirm="{templateUrl: '~/path/to/view.html', export: {foo: bar}}" on-yes="frobnicate(123)">Frobincate</button>
889 .directive('crmConfirm', function ($compile
, $rootScope
, $templateRequest
, $q
) {
890 // Helpers to calculate default options for CRM.confirm()
892 'disable': function (options
) {
894 message
: ts('Are you sure you want to disable this?'),
895 options
: {no
: ts('Cancel'), yes
: ts('Disable')},
897 title
: ts('Disable %1?', {
898 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
902 'revert': function (options
) {
904 message
: ts('Are you sure you want to revert this?'),
905 options
: {no
: ts('Cancel'), yes
: ts('Revert')},
907 title
: ts('Revert %1?', {
908 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
912 'delete': function (options
) {
914 message
: ts('Are you sure you want to delete this?'),
915 options
: {no
: ts('Cancel'), yes
: ts('Delete')},
917 title
: ts('Delete %1?', {
918 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
923 var confirmCount
= 0;
925 link: function (scope
, element
, attrs
) {
926 $(element
).click(function () {
927 var options
= scope
.$eval(attrs
.crmConfirm
);
928 if (attrs
.title
&& !options
.title
) {
929 options
.title
= attrs
.title
;
931 var defaults
= (options
.type
) ? defaultFuncs
[options
.type
](options
) : {};
933 var tpl
= null, stubId
= null;
934 if (!options
.message
) {
935 if (options
.templateUrl
) {
936 tpl
= $templateRequest(options
.templateUrl
);
938 else if (options
.template
) {
939 tpl
= options
.template
;
942 stubId
= 'crmUiConfirm_' + (++confirmCount
);
943 options
.message
= '<div id="' + stubId
+ '"></div>';
947 CRM
.confirm(_
.extend(defaults
, options
))
948 .on('crmConfirm:yes', function() { scope
.$apply(attrs
.onYes
); })
949 .on('crmConfirm:no', function() { scope
.$apply(attrs
.onNo
); });
952 $q
.when(tpl
, function(html
) {
953 var scope
= options
.scope
|| $rootScope
.$new();
954 if (options
.export) {
955 angular
.extend(scope
, options
.export);
957 var linker
= $compile(html
);
958 $('#' + stubId
).append($(linker(scope
)));
965 .run(function($rootScope
, $location
) {
966 /// Example: <button ng-click="goto('home')">Go home!</button>
967 $rootScope
.goto = function(path
) {
968 $location
.path(path
);
970 // useful for debugging: $rootScope.log = console.log || function() {};
974 })(angular
, CRM
.$, CRM
._
);