1 /// crmUi: Sundry UI helpers
2 (function (angular
, $, _
) {
5 var partialUrl = function (relPath
) {
6 return CRM
.resourceUrls
['civicrm'] + '/partials/crmUi/' + relPath
;
10 angular
.module('crmUi', [])
12 // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
13 // WISHLIST: crmCollapsed should support two-way/continous binding
14 .directive('crmUiAccordion', function() {
20 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>',
22 link: function (scope
, element
, attrs
) {
24 collapsed
: scope
.$parent
.$eval(attrs
.crmCollapsed
)
30 // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
31 .directive('crmUiIframe', function ($parse
) {
34 crmUiIframe
: '@' // expression which evalutes to HTML content
36 link: function (scope
, elm
, attrs
) {
37 var iframe
= $(elm
)[0];
38 iframe
.setAttribute('width', '100%');
39 iframe
.setAttribute('frameborder', '0');
41 var refresh = function () {
42 // 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>';
43 var iframeHtml
= scope
.$parent
.$eval(attrs
.crmUiIframe
);
45 var doc
= iframe
.document
;
46 if (iframe
.contentDocument
) {
47 doc
= iframe
.contentDocument
;
49 else if (iframe
.contentWindow
) {
50 doc
= iframe
.contentWindow
.document
;
54 doc
.writeln(iframeHtml
);
58 scope
.$parent
.$watch(attrs
.crmUiIframe
, refresh
);
59 //setTimeout(function () { refresh(); }, 50);
64 // example: <form name="myForm">...<label crm-ui-label crm-for="myField">My Field</span>...<input name="myField"/>...</form>
66 // Label adapts based on <input required>, <input ng-required>, or any other validation.
68 // Note: This should work in the normal case where <label> and <input> are in roughly the same scope,
69 // but if the scopes are materially different then problems could arise.
70 .directive('crmUiLabel', function($parse
) {
76 template
: '<span ng-class="cssClasses"><span ng-transclude></span> <span ng-show="crmRequired" class="crm-marker" title="This field is required.">*</span></span>',
77 link: function(scope
, element
, attrs
) {
78 if (attrs
.crmFor
== 'name') {
79 throw new Error('Validation monitoring does not work for field name "name"');
82 // 1. Figure out form and input elements
84 var form
= $(element
).closest('form');
85 var formCtrl
= scope
.$parent
.$eval(form
.attr('name'));
86 var input
= $('input[name="' + attrs
.crmFor
+ '"],select[name="' + attrs
.crmFor
+ '"],textarea[name="' + attrs
.crmFor
+ '"]', form
);
87 if (form
.length
!= 1 || input
.length
!= 1) {
88 if (console
.log
) console
.log('Label cannot be matched to input element. Expected to find one form and one input[name='+attrs
.crmFor
+'].', form
.length
, input
.length
);
92 // 2. Make sure that inputs are well-defined (with name+id).
94 if (!input
.attr('id')) {
95 input
.attr('id', 'crmUi_' + (++idCount
));
97 $(element
).attr('for', input
.attr('id'));
99 // 3. Monitor is the "required" and "$valid" properties
101 if (input
.attr('ng-required')) {
102 scope
.crmRequired
= scope
.$parent
.$eval(input
.attr('ng-required'));
103 scope
.$parent
.$watch(input
.attr('ng-required'), function(isRequired
) {
104 scope
.crmRequired
= isRequired
;
107 scope
.crmRequired
= input
.prop('required');
110 var inputCtrl
= form
.attr('name') + '.' + input
.attr('name');
111 scope
.cssClasses
= {};
112 scope
.$parent
.$watch(inputCtrl
+ '.$valid', function(newValue
) {
113 //scope.cssClasses['ng-valid'] = newValue;
114 //scope.cssClasses['ng-invalid'] = !newValue;
115 scope
.cssClasses
['crm-error'] = !scope
.$parent
.$eval(inputCtrl
+ '.$valid') && !scope
.$parent
.$eval(inputCtrl
+ '.$pristine');
117 scope
.$parent
.$watch(inputCtrl
+ '.$pristine', function(newValue
) {
118 //scope.cssClasses['ng-pristine'] = newValue;
119 //scope.cssClasses['ng-dirty'] = !newValue;
120 scope
.cssClasses
['crm-error'] = !scope
.$parent
.$eval(inputCtrl
+ '.$valid') && !scope
.$parent
.$eval(inputCtrl
+ '.$pristine');
127 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
128 // example: <a crm-ui-lock
129 // binding="mymodel.boolfield"
130 // title-locked="ts('Boolfield is locked')"
131 // title-unlocked="ts('Boolfield is unlocked')"></a>
132 .directive('crmUiLock', function ($parse
, $rootScope
) {
133 var defaultVal = function (defaultValue
) {
134 var f = function (scope
) {
137 f
.assign = function (scope
, value
) {
143 // like $parse, but accepts a defaultValue in case expr is undefined
144 var parse = function (expr
, defaultValue
) {
145 return expr
? $parse(expr
) : defaultVal(defaultValue
);
150 link: function (scope
, element
, attrs
) {
151 var binding
= parse(attrs
['binding'], true);
152 var titleLocked
= parse(attrs
['titleLocked'], ts('Locked'));
153 var titleUnlocked
= parse(attrs
['titleUnlocked'], ts('Unlocked'));
155 $(element
).addClass('ui-icon lock-button');
156 var refresh = function () {
157 var locked
= binding(scope
);
160 .removeClass('ui-icon-unlocked')
161 .addClass('ui-icon-locked')
162 .prop('title', titleLocked(scope
))
167 .removeClass('ui-icon-locked')
168 .addClass('ui-icon-unlocked')
169 .prop('title', titleUnlocked(scope
))
174 $(element
).click(function () {
175 binding
.assign(scope
, !binding(scope
));
177 $rootScope
.$digest();
180 scope
.$watch(attrs
.binding
, refresh
);
181 scope
.$watch(attrs
.titleLocked
, refresh
);
182 scope
.$watch(attrs
.titleUnlocked
, refresh
);
189 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
190 // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
191 .directive('crmUiTab', function($parse
) {
193 require
: '^crmUiTabSet',
199 template
: '<div ng-transclude></div>',
201 link: function (scope
, element
, attrs
, crmUiTabSetCtrl
) {
202 crmUiTabSetCtrl
.add(scope
);
207 // 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>
208 .directive('crmUiTabSet', function() {
214 templateUrl
: partialUrl('tabset.html'),
216 controllerAs
: 'crmUiTabSetCtrl',
217 controller: function($scope
, $parse
) {
218 var tabs
= $scope
.tabs
= []; // array<$scope>
219 this.add = function(tab
) {
220 if (!tab
.id
) throw "Tab is missing 'id'";
224 link: function (scope
, element
, attrs
) {}
228 // 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>
229 // Note: "myWizardCtrl" has various actions/properties like next() and $first().
230 // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
231 // WISHLIST: Allow each step to enable/disable (show/hide) itself
232 .directive('crmUiWizard', function() {
238 templateUrl
: partialUrl('wizard.html'),
240 controllerAs
: 'crmUiWizardCtrl',
241 controller: function($scope
, $parse
) {
242 var steps
= $scope
.steps
= []; // array<$scope>
243 var crmUiWizardCtrl
= this;
245 var selectedIndex
= null;
247 var findIndex = function() {
249 angular
.forEach(steps
, function(step
, stepKey
) {
250 if (step
.selected
) found
= stepKey
;
255 /// @return int the index of the current step
256 this.$index = function() { return selectedIndex
; };
257 /// @return bool whether the currentstep is first
258 this.$first = function() { return this.$index() === 0; };
259 /// @return bool whether the current step is last
260 this.$last = function() { return this.$index() === steps
.length
-1; };
261 this.$maxVisit = function() { return maxVisited
; }
262 this.iconFor = function(index
) {
263 if (index
< this.$index()) return '√';
264 if (index
=== this.$index()) return '»';
267 this.isSelectable = function(step
) {
268 if (step
.selected
) return false;
270 angular
.forEach(steps
, function(otherStep
, otherKey
) {
271 if (step
=== otherStep
&& otherKey
<= maxVisited
) result
= true;
276 /*** @param Object step the $scope of the step */
277 this.select = function(step
) {
278 angular
.forEach(steps
, function(otherStep
, otherKey
) {
279 otherStep
.selected
= (otherStep
=== step
);
280 if (otherStep
=== step
&& maxVisited
< otherKey
) maxVisited
= otherKey
;
282 selectedIndex
= findIndex();
284 /*** @param Object step the $scope of the step */
285 this.add = function(step
) {
286 if (steps
.length
=== 0) {
287 step
.selected
= true;
292 this.goto = function(index
) {
293 if (index
< 0) index
= 0;
294 if (index
>= steps
.length
) index
= steps
.length
-1;
295 this.select(steps
[index
]);
297 this.previous = function() { this.goto(this.$index()-1); };
298 this.next = function() { this.goto(this.$index()+1); };
299 if ($scope
.crmUiWizard
) {
300 $parse($scope
.crmUiWizard
).assign($scope
.$parent
, this)
303 link: function (scope
, element
, attrs
) {}
307 // Use this to add extra markup to wizard
308 .directive('crmUiWizardButtons', function() {
310 require
: '^crmUiWizard',
313 template
: '<span ng-transclude></span>',
315 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
316 var realButtonsEl
= $(element
).closest('.crm-wizard').find('.crm-wizard-buttons');
317 $(element
).appendTo(realButtonsEl
);
322 // example <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
323 .directive('crmUiWizardStep', function() {
325 require
: '^crmUiWizard',
330 template
: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
332 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
333 crmUiWizardCtrl
.add(scope
);
338 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
339 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
340 .directive('crmConfirm', function () {
341 // Helpers to calculate default options for CRM.confirm()
343 'disable': function (options
) {
345 message
: ts('Are you sure you want to disable this?'),
346 options
: {no
: ts('Cancel'), yes
: ts('Disable')},
348 title
: ts('Disable %1?', {
349 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
353 'revert': function (options
) {
355 message
: ts('Are you sure you want to revert this?'),
356 options
: {no
: ts('Cancel'), yes
: ts('Revert')},
358 title
: ts('Revert %1?', {
359 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
363 'delete': function (options
) {
365 message
: ts('Are you sure you want to delete this?'),
366 options
: {no
: ts('Cancel'), yes
: ts('Delete')},
368 title
: ts('Delete %1?', {
369 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
376 link: function (scope
, element
, attrs
) {
377 $(element
).click(function () {
378 var options
= scope
.$eval(attrs
['crmConfirm']);
379 var defaults
= (options
.type
) ? defaultFuncs
[options
.type
](options
) : {};
380 CRM
.confirm(_
.extend(defaults
, options
))
381 .on('crmConfirm:yes', function () { scope
.$apply(attrs
['onYes']); })
382 .on('crmConfirm:no', function () { scope
.$apply(attrs
['onNo']); });
387 .run(function($rootScope
, $location
) {
388 /// Example: <button ng-click="goto('home')">Go home!</button>
389 $rootScope
.goto = function(path
) {
390 $location
.path(path
);
392 // useful for debugging: $rootScope.log = console.log || function() {};
396 })(angular
, CRM
.$, CRM
._
);