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/continous binding
10 .directive('crmUiAccordion', function() {
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>',
18 link: function (scope
, element
, attrs
) {
20 collapsed
: scope
.$parent
.$eval(attrs
.crmCollapsed
)
26 // Display a date widget.
27 // example: <input crm-ui-date ng-model="myobj.datefield" />
28 // example: <input crm-ui-date ng-model="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
29 .directive('crmUiDate', function ($parse
, $timeout
) {
34 crmUiDateFormat
: '@' // expression, date format (default: "yy-mm-dd")
36 link: function (scope
, element
, attrs
, ngModel
) {
37 var fmt
= attrs
.crmUiDateFormat
? $parse(attrs
.crmUiDateFormat
)() : "yy-mm-dd";
39 element
.addClass('dateplugin');
40 $(element
).datepicker({
44 ngModel
.$render
= function $render() {
45 $(element
).datepicker('setDate', ngModel
.$viewValue
);
47 var updateParent
= (function() {
48 $timeout(function () {
49 ngModel
.$setViewValue(element
.val());
53 element
.on('change', updateParent
);
58 // Display a date-time widget.
59 // example: <div crm-ui-date-time ng-model="myobj.mydatetimefield"></div>
60 .directive('crmUiDateTime', function ($parse
) {
67 templateUrl
: '~/crmUi/datetime.html',
68 link: function (scope
, element
, attrs
, ngModel
) {
69 var ts
= scope
.ts
= CRM
.ts(null);
70 scope
.dateLabel
= ts('Date');
71 scope
.timeLabel
= ts('Time');
72 element
.addClass('crm-ui-datetime');
74 ngModel
.$render
= function $render() {
75 if (!_
.isEmpty(ngModel
.$viewValue
)) {
76 var dtparts
= ngModel
.$viewValue
.split(/ /);
77 scope
.dtparts
= {date
: dtparts
[0], time
: dtparts
[1]};
80 scope
.dtparts
= {date
: '', time
: ''};
84 function updateParent() {
85 var incompleteDateTime
= _
.isEmpty(scope
.dtparts
.date
) ^ _
.isEmpty(scope
.dtparts
.time
);
86 ngModel
.$setValidity('incompleteDateTime', !incompleteDateTime
);
88 if (_
.isEmpty(scope
.dtparts
.date
) && _
.isEmpty(scope
.dtparts
.time
)) {
89 ngModel
.$setViewValue(' ');
92 //ngModel.$setViewValue(scope.dtparts.date + ' ' + scope.dtparts.time);
93 ngModel
.$setViewValue((scope
.dtparts
.date
? scope
.dtparts
.date
: '') + ' ' + (scope
.dtparts
.time
? scope
.dtparts
.time
: ''));
97 scope
.$watch('dtparts.date', updateParent
);
98 scope
.$watch('dtparts.time', updateParent
);
100 function updateRequired() {
101 scope
.required
= scope
.$parent
.$eval(attrs
.ngRequired
);
104 if (attrs
.ngRequired
) {
106 scope
.$parent
.$watch(attrs
.ngRequired
, updateRequired
);
109 scope
.reset
= function reset() {
110 scope
.dtparts
= {date
: '', time
: ''};
111 ngModel
.$setViewValue('');
117 // Display a field/row in a field list
118 // example: <div crm-ui-field crm-title="My Field"> {{mydata}} </div>
119 // example: <div crm-ui-field="subform.myfield" crm-title="'My Field'"> <input crm-ui-id="subform.myfield" name="myfield" /> </div>
120 // example: <div crm-ui-field="subform.myfield" crm-title="'My Field'"> <input crm-ui-id="subform.myfield" name="myfield" required /> </div>
121 .directive('crmUiField', function() {
122 // Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
124 default: '~/crmUi/field.html',
125 checkbox
: '~/crmUi/field-cb.html'
129 require
: '^crmUiIdScope',
135 templateUrl: function(tElement
, tAttrs
){
136 var layout
= tAttrs
.crmLayout
? tAttrs
.crmLayout
: 'default';
137 return templateUrls
[layout
];
140 link: function (scope
, element
, attrs
, crmUiIdCtrl
) {
141 $(element
).addClass('crm-section');
142 scope
.crmUiField
= attrs
.crmUiField
;
143 scope
.crmTitle
= attrs
.crmTitle
;
148 // 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>
149 .directive('crmUiId', function () {
151 require
: '^crmUiIdScope',
154 pre: function (scope
, element
, attrs
, crmUiIdCtrl
) {
155 var id
= crmUiIdCtrl
.get(attrs
.crmUiId
);
156 element
.attr('id', id
);
162 // 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>
163 .directive('crmUiFor', function ($parse
, $timeout
) {
165 require
: '^crmUiIdScope',
167 template
: '<span ng-class="cssClasses"><span ng-transclude/><span crm-ui-visible="crmIsRequired" class="crm-marker" title="This field is required.">*</span></span>',
169 link: function (scope
, element
, attrs
, crmUiIdCtrl
) {
170 scope
.crmIsRequired
= false;
171 scope
.cssClasses
= {};
173 if (!attrs
.crmUiFor
) return;
175 var id
= crmUiIdCtrl
.get(attrs
.crmUiFor
);
176 element
.attr('for', id
);
179 var updateCss = function () {
180 scope
.cssClasses
['crm-error'] = !ngModel
.$valid
&& !ngModel
.$pristine
;
183 // Note: if target element is dynamically generated (eg via ngInclude), then it may not be available
184 // immediately for initialization. Use retries/retryDelay to initialize such elements.
185 var init = function (retries
, retryDelay
) {
186 var input
= $('#' + id
);
187 if (input
.length
=== 0) {
190 init(retries
-1, retryDelay
);
196 var tgtScope
= scope
;//.$parent;
197 if (attrs
.crmDepth
) {
198 for (var i
= attrs
.crmDepth
; i
> 0; i
--) {
199 tgtScope
= tgtScope
.$parent
;
203 if (input
.attr('ng-required')) {
204 scope
.crmIsRequired
= scope
.$parent
.$eval(input
.attr('ng-required'));
205 scope
.$parent
.$watch(input
.attr('ng-required'), function (isRequired
) {
206 scope
.crmIsRequired
= isRequired
;
210 scope
.crmIsRequired
= input
.prop('required');
213 ngModel
= $parse(attrs
.crmUiFor
)(tgtScope
);
215 ngModel
.$viewChangeListeners
.push(updateCss
);
226 // Define a scope in which a name like "subform.foo" maps to a unique ID.
227 // 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>
228 .directive('crmUiIdScope', function () {
232 controllerAs
: 'crmUiIdCtrl',
233 controller: function($scope
) {
235 this.get = function(name
) {
237 ids
[name
] = "crmUiId_" + (++uidCount
);
242 link: function (scope
, element
, attrs
) {}
246 // Display an HTML blurb inside an IFRAME.
247 // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
248 .directive('crmUiIframe', function ($parse
) {
251 crmUiIframe
: '@' // expression which evalutes to HTML content
253 link: function (scope
, elm
, attrs
) {
254 var iframe
= $(elm
)[0];
255 iframe
.setAttribute('width', '100%');
256 iframe
.setAttribute('frameborder', '0');
258 var refresh = function () {
259 // 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>';
260 var iframeHtml
= scope
.$parent
.$eval(attrs
.crmUiIframe
);
262 var doc
= iframe
.document
;
263 if (iframe
.contentDocument
) {
264 doc
= iframe
.contentDocument
;
266 else if (iframe
.contentWindow
) {
267 doc
= iframe
.contentWindow
.document
;
271 doc
.writeln(iframeHtml
);
275 scope
.$parent
.$watch(attrs
.crmUiIframe
, refresh
);
280 // Define a rich text editor.
281 // example: <textarea crm-ui-id="myForm.body_html" crm-ui-richtext name="body_html" ng-model="mailing.body_html"></textarea>
282 // WISHLIST: use ngModel
283 .directive('crmUiRichtext', function ($timeout
) {
286 link: function (scope
, elm
, attr
, ngModel
) {
287 var ck
= CKEDITOR
.replace(elm
[0]);
293 ck
.on('pasteState', function () {
294 scope
.$apply(function () {
295 ngModel
.$setViewValue(ck
.getData());
299 ck
.on('insertText', function () {
300 $timeout(function () {
301 ngModel
.$setViewValue(ck
.getData());
305 ngModel
.$render = function (value
) {
306 ck
.setData(ngModel
.$viewValue
);
312 // Display a lock icon (based on a boolean).
313 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
314 // example: <a crm-ui-lock
315 // binding="mymodel.boolfield"
316 // title-locked="ts('Boolfield is locked')"
317 // title-unlocked="ts('Boolfield is unlocked')"></a>
318 .directive('crmUiLock', function ($parse
, $rootScope
) {
319 var defaultVal = function (defaultValue
) {
320 var f = function (scope
) {
323 f
.assign = function (scope
, value
) {
329 // like $parse, but accepts a defaultValue in case expr is undefined
330 var parse = function (expr
, defaultValue
) {
331 return expr
? $parse(expr
) : defaultVal(defaultValue
);
336 link: function (scope
, element
, attrs
) {
337 var binding
= parse(attrs
.binding
, true);
338 var titleLocked
= parse(attrs
.titleLocked
, ts('Locked'));
339 var titleUnlocked
= parse(attrs
.titleUnlocked
, ts('Unlocked'));
341 $(element
).addClass('ui-icon lock-button');
342 var refresh = function () {
343 var locked
= binding(scope
);
346 .removeClass('ui-icon-unlocked')
347 .addClass('ui-icon-locked')
348 .prop('title', titleLocked(scope
))
353 .removeClass('ui-icon-locked')
354 .addClass('ui-icon-unlocked')
355 .prop('title', titleUnlocked(scope
))
360 $(element
).click(function () {
361 binding
.assign(scope
, !binding(scope
));
363 $rootScope
.$digest();
366 scope
.$watch(attrs
.binding
, refresh
);
367 scope
.$watch(attrs
.titleLocked
, refresh
);
368 scope
.$watch(attrs
.titleUnlocked
, refresh
);
375 // Display a fancy SELECT (based on select2).
376 // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
377 .directive('crmUiSelect', function ($parse
, $timeout
) {
383 link: function (scope
, element
, attrs
, ngModel
) {
384 // In cases where UI initiates update, there may be an extra
385 // call to refreshUI, but it doesn't create a cycle.
387 ngModel
.$render = function () {
388 $timeout(function () {
389 // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
390 // new item is added before selection is made
391 $(element
).select2('val', ngModel
.$viewValue
);
394 function refreshModel() {
395 var oldValue
= ngModel
.$viewValue
, newValue
= $(element
).select2('val');
396 if (oldValue
!= newValue
) {
397 scope
.$parent
.$apply(function () {
398 ngModel
.$setViewValue(newValue
);
404 // TODO watch select2-options
405 var options
= attrs
.crmUiSelect
? scope
.$parent
.$eval(attrs
.crmUiSelect
) : {};
406 $(element
).select2(options
);
407 $(element
).on('change', refreshModel
);
408 $timeout(ngModel
.$render
);
416 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
417 // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
418 .directive('crmUiTab', function($parse
) {
420 require
: '^crmUiTabSet',
426 template
: '<div ng-transclude></div>',
428 link: function (scope
, element
, attrs
, crmUiTabSetCtrl
) {
429 crmUiTabSetCtrl
.add(scope
);
434 // 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>
435 .directive('crmUiTabSet', function() {
441 templateUrl
: '~/crmUi/tabset.html',
443 controllerAs
: 'crmUiTabSetCtrl',
444 controller: function($scope
, $parse
) {
445 var tabs
= $scope
.tabs
= []; // array<$scope>
446 this.add = function(tab
) {
447 if (!tab
.id
) throw "Tab is missing 'id'";
451 link: function (scope
, element
, attrs
) {}
455 // Display a time-entry field.
456 // example: <input crm-ui-time ng-model="myobj.mytimefield" />
457 .directive('crmUiTime', function ($parse
, $timeout
) {
463 link: function (scope
, element
, attrs
, ngModel
) {
464 element
.addClass('crm-form-text six');
465 element
.timeEntry({show24Hours
: true});
467 ngModel
.$render
= function $render() {
468 element
.timeEntry('setTime', ngModel
.$viewValue
);
471 var updateParent
= (function () {
472 $timeout(function () {
473 ngModel
.$setViewValue(element
.val());
476 element
.on('change', updateParent
);
481 // Generic, field-independent form validator.
482 // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" />
483 // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" crm-ui-validate-name="myError" />
484 .directive('crmUiValidate', function() {
488 link: function(scope
, element
, attrs
, ngModel
) {
489 var validationKey
= attrs
.crmUiValidateName
? attrs
.crmUiValidateName
: 'crmUiValidate';
490 scope
.$watch(attrs
.crmUiValidate
, function(newValue
){
491 ngModel
.$setValidity(validationKey
, !!newValue
);
497 // like ng-show, but hides/displays elements using "visibility" which maintains positioning
498 // example <div crm-ui-visible="false">...content...</div>
499 .directive('crmUiVisible', function($parse
) {
505 link: function (scope
, element
, attrs
) {
506 var model
= $parse(attrs
.crmUiVisible
);
507 function updatecChildren() {
508 element
.css('visibility', model(scope
.$parent
) ? 'inherit' : 'hidden');
511 scope
.$parent
.$watch(attrs
.crmUiVisible
, updatecChildren
);
516 // 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>
517 // Note: "myWizardCtrl" has various actions/properties like next() and $first().
518 // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
519 // WISHLIST: Allow each step to enable/disable (show/hide) itself
520 .directive('crmUiWizard', function() {
526 templateUrl
: '~/crmUi/wizard.html',
528 controllerAs
: 'crmUiWizardCtrl',
529 controller: function($scope
, $parse
) {
530 var steps
= $scope
.steps
= []; // array<$scope>
531 var crmUiWizardCtrl
= this;
533 var selectedIndex
= null;
535 var findIndex = function() {
537 angular
.forEach(steps
, function(step
, stepKey
) {
538 if (step
.selected
) found
= stepKey
;
543 /// @return int the index of the current step
544 this.$index = function() { return selectedIndex
; };
545 /// @return bool whether the currentstep is first
546 this.$first = function() { return this.$index() === 0; };
547 /// @return bool whether the current step is last
548 this.$last = function() { return this.$index() === steps
.length
-1; };
549 this.$maxVisit = function() { return maxVisited
; };
550 this.$validStep = function() {
551 return steps
[selectedIndex
].isStepValid();
553 this.iconFor = function(index
) {
554 if (index
< this.$index()) return '√';
555 if (index
=== this.$index()) return '»';
558 this.isSelectable = function(step
) {
559 if (step
.selected
) return false;
561 angular
.forEach(steps
, function(otherStep
, otherKey
) {
562 if (step
=== otherStep
&& otherKey
<= maxVisited
) result
= true;
567 /*** @param Object step the $scope of the step */
568 this.select = function(step
) {
569 angular
.forEach(steps
, function(otherStep
, otherKey
) {
570 otherStep
.selected
= (otherStep
=== step
);
571 if (otherStep
=== step
&& maxVisited
< otherKey
) maxVisited
= otherKey
;
573 selectedIndex
= findIndex();
575 /*** @param Object step the $scope of the step */
576 this.add = function(step
) {
577 if (steps
.length
=== 0) {
578 step
.selected
= true;
582 steps
.sort(function(a
,b
){
583 return a
.crmUiWizardStep
- b
.crmUiWizardStep
;
585 selectedIndex
= findIndex();
587 this.remove = function(step
) {
589 angular
.forEach(steps
, function(otherStep
, otherKey
) {
590 if (otherStep
=== step
) key
= otherKey
;
593 steps
.splice(key
, 1);
596 this.goto = function(index
) {
597 if (index
< 0) index
= 0;
598 if (index
>= steps
.length
) index
= steps
.length
-1;
599 this.select(steps
[index
]);
601 this.previous = function() { this.goto(this.$index()-1); };
602 this.next = function() { this.goto(this.$index()+1); };
603 if ($scope
.crmUiWizard
) {
604 $parse($scope
.crmUiWizard
).assign($scope
.$parent
, this);
607 link: function (scope
, element
, attrs
) {}
611 // Use this to add extra markup to wizard
612 .directive('crmUiWizardButtons', function() {
614 require
: '^crmUiWizard',
617 template
: '<span ng-transclude></span>',
619 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
620 var realButtonsEl
= $(element
).closest('.crm-wizard').find('.crm-wizard-buttons');
621 $(element
).appendTo(realButtonsEl
);
626 // example: <div crm-ui-wizard-step crm-title="ts('My Title')" ng-form="mySubForm">...content...</div>
627 // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering.
628 // example: <div crm-ui-wizard-step="100" crm-title="..." ng-if="...">...content...</div>
629 .directive('crmUiWizardStep', function() {
632 require
: ['^crmUiWizard', 'form'],
635 crmTitle
: '@', // expression, evaluates to a printable string
636 crmUiWizardStep
: '@' // int, a weight which determines the ordering of the steps
638 template
: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
640 link: function (scope
, element
, attrs
, ctrls
) {
641 var crmUiWizardCtrl
= ctrls
[0], form
= ctrls
[1];
642 if (scope
.crmUiWizardStep
) {
643 scope
.crmUiWizardStep
= parseInt(scope
.crmUiWizardStep
);
645 scope
.crmUiWizardStep
= nextWeight
++;
647 scope
.isStepValid = function() {
650 crmUiWizardCtrl
.add(scope
);
651 element
.on('$destroy', function(){
652 crmUiWizardCtrl
.remove(scope
);
658 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
659 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
660 .directive('crmConfirm', function () {
661 // Helpers to calculate default options for CRM.confirm()
663 'disable': function (options
) {
665 message
: ts('Are you sure you want to disable this?'),
666 options
: {no
: ts('Cancel'), yes
: ts('Disable')},
668 title
: ts('Disable %1?', {
669 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
673 'revert': function (options
) {
675 message
: ts('Are you sure you want to revert this?'),
676 options
: {no
: ts('Cancel'), yes
: ts('Revert')},
678 title
: ts('Revert %1?', {
679 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
683 'delete': function (options
) {
685 message
: ts('Are you sure you want to delete this?'),
686 options
: {no
: ts('Cancel'), yes
: ts('Delete')},
688 title
: ts('Delete %1?', {
689 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
696 link: function (scope
, element
, attrs
) {
697 $(element
).click(function () {
698 var options
= scope
.$eval(attrs
.crmConfirm
);
699 var defaults
= (options
.type
) ? defaultFuncs
[options
.type
](options
) : {};
700 CRM
.confirm(_
.extend(defaults
, options
))
701 .on('crmConfirm:yes', function () { scope
.$apply(attrs
.onYes
); })
702 .on('crmConfirm:no', function () { scope
.$apply(attrs
.onNo
); });
707 .run(function($rootScope
, $location
) {
708 /// Example: <button ng-click="goto('home')">Go home!</button>
709 $rootScope
.goto = function(path
) {
710 $location
.path(path
);
712 // useful for debugging: $rootScope.log = console.log || function() {};
716 })(angular
, CRM
.$, CRM
._
);