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: <input crm-ui-date="myobj.datefield" />
31 // example: <input crm-ui-date="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
32 .directive('crmUiDate', function ($parse
, $timeout
) {
36 crmUiDate
: '@', // expression, model binding
37 crmUiDateFormat
: '@' // expression, date format (default: "yy-mm-dd")
39 link: function (scope
, element
, attrs
) {
40 var fmt
= attrs
.crmUiDateFormat
? $parse(attrs
.crmUiDateFormat
)() : "yy-mm-dd";
41 var model
= $parse(attrs
.crmUiDate
);
43 element
.addClass('dateplugin');
44 $(element
).datepicker({
48 var updateChildren
= (function() {
49 element
.off('change', updateParent
);
50 $(element
).datepicker('setDate', model(scope
.$parent
));
51 element
.on('change', updateParent
);
53 var updateParent
= (function() {
54 $timeout(function () {
55 model
.assign(scope
.$parent
, $(element
).val());
60 scope
.$parent
.$watch(attrs
.crmUiDate
, updateChildren
);
61 element
.on('change', updateParent
);
66 // example: <div crm-ui-date-time="myobj.mydatetimefield"></div>
67 .directive('crmUiDateTime', function ($parse
) {
73 template
: '<input crm-ui-date="dtparts.date" placeholder="{{dateLabel}}"/> <input crm-ui-time="dtparts.time" placeholder="{{timeLabel}}"/>',
74 link: function (scope
, element
, attrs
) {
75 var model
= $parse(attrs
.crmUiDateTime
);
76 scope
.dateLabel
= ts('Date');
77 scope
.timeLabel
= ts('Time');
79 var updateChildren
= (function () {
80 var value
= model(scope
.$parent
);
82 var dtparts
= value
.split(/ /);
83 scope
.dtparts
= {date
: dtparts
[0], time
: dtparts
[1]};
86 scope
.dtparts
= {date
: '', time
: ''};
89 var updateParent
= (function () {
90 model
.assign(scope
.$parent
, scope
.dtparts
.date
+ " " + scope
.dtparts
.time
);
94 scope
.$parent
.$watch(attrs
.crmUiDateTime
, updateChildren
);
95 scope
.$watch('dtparts.date', updateParent
),
96 scope
.$watch('dtparts.time', updateParent
)
101 // Display a field/row in a field list
102 // example: <div crm-ui-field crm-title="My Field"> {{mydata}} </div>
103 // example: <div crm-ui-field="myfield" crm-title="My Field"> <input name="myfield" /> </div>
104 // example: <div crm-ui-field="myfield" crm-title="My Field"> <input name="myfield" required /> </div>
105 .directive('crmUiField', function() {
106 function createReqStyle(req
) {
107 return {visibility
: req
? 'inherit' : 'hidden'};
109 // Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
111 default: partialUrl('field.html'),
112 checkbox
: partialUrl('field-cb.html')
117 crmUiField
: '@', // string, name of an HTML form element
118 crmLayout
: '@', // string, "default" or "checkbox"
119 crmTitle
: '@' // expression, printable title for the field
121 templateUrl: function(tElement
, tAttrs
){
122 var layout
= tAttrs
.crmLayout
? tAttrs
.crmLayout
: 'default';
123 return templateUrls
[layout
];
126 link: function (scope
, element
, attrs
) {
127 $(element
).addClass('crm-section');
128 scope
.crmTitle
= attrs
.crmTitle
;
129 scope
.crmUiField
= attrs
.crmUiField
;
130 scope
.cssClasses
= {};
131 scope
.crmRequiredStyle
= createReqStyle(false);
133 // 0. Ensure that a target field has been specified
135 if (!attrs
.crmUiField
) return;
136 if (attrs
.crmUiField
== 'name') {
137 throw new Error('Validation monitoring does not work for field name "name"');
140 // 1. Figure out form and input elements
142 var form
= $(element
).closest('form');
143 var formCtrl
= scope
.$parent
.$eval(form
.attr('name'));
144 var input
= $('input[name="' + attrs
.crmUiField
+ '"],select[name="' + attrs
.crmUiField
+ '"],textarea[name="' + attrs
.crmUiField
+ '"]', form
);
145 var label
= $('>div.label >label, >label', element
);
146 if (form
.length
!= 1 || input
.length
!= 1 || label
.length
!= 1) {
147 if (console
.log
) console
.log('Label cannot be matched to input element. Expected to find one form and one input[name='+attrs
.crmUiField
+'].', form
.length
, input
.length
, label
.length
);
151 // 2. Make sure that inputs are well-defined (with name+id).
153 if (!input
.attr('id')) {
154 input
.attr('id', 'crmUi_' + (++idCount
));
156 $(label
).attr('for', input
.attr('id'));
158 // 3. Monitor is the "required" and "$valid" properties
160 if (input
.attr('ng-required')) {
161 scope
.crmRequiredStyle
= createReqStyle(scope
.$parent
.$eval(input
.attr('ng-required')));
162 scope
.$parent
.$watch(input
.attr('ng-required'), function(isRequired
) {
163 scope
.crmRequiredStyle
= createReqStyle(isRequired
);
166 scope
.crmRequiredStyle
= createReqStyle(input
.prop('required'));
169 var inputCtrl
= form
.attr('name') + '.' + input
.attr('name');
170 scope
.$parent
.$watch(inputCtrl
+ '.$valid', function(newValue
) {
171 scope
.cssClasses
['crm-error'] = !scope
.$parent
.$eval(inputCtrl
+ '.$valid') && !scope
.$parent
.$eval(inputCtrl
+ '.$pristine');
173 scope
.$parent
.$watch(inputCtrl
+ '.$pristine', function(newValue
) {
174 scope
.cssClasses
['crm-error'] = !scope
.$parent
.$eval(inputCtrl
+ '.$valid') && !scope
.$parent
.$eval(inputCtrl
+ '.$pristine');
180 // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
181 .directive('crmUiIframe', function ($parse
) {
184 crmUiIframe
: '@' // expression which evalutes to HTML content
186 link: function (scope
, elm
, attrs
) {
187 var iframe
= $(elm
)[0];
188 iframe
.setAttribute('width', '100%');
189 iframe
.setAttribute('frameborder', '0');
191 var refresh = function () {
192 // 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>';
193 var iframeHtml
= scope
.$parent
.$eval(attrs
.crmUiIframe
);
195 var doc
= iframe
.document
;
196 if (iframe
.contentDocument
) {
197 doc
= iframe
.contentDocument
;
199 else if (iframe
.contentWindow
) {
200 doc
= iframe
.contentWindow
.document
;
204 doc
.writeln(iframeHtml
);
208 scope
.$parent
.$watch(attrs
.crmUiIframe
, refresh
);
209 //setTimeout(function () { refresh(); }, 50);
214 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
215 // example: <a crm-ui-lock
216 // binding="mymodel.boolfield"
217 // title-locked="ts('Boolfield is locked')"
218 // title-unlocked="ts('Boolfield is unlocked')"></a>
219 .directive('crmUiLock', function ($parse
, $rootScope
) {
220 var defaultVal = function (defaultValue
) {
221 var f = function (scope
) {
224 f
.assign = function (scope
, value
) {
230 // like $parse, but accepts a defaultValue in case expr is undefined
231 var parse = function (expr
, defaultValue
) {
232 return expr
? $parse(expr
) : defaultVal(defaultValue
);
237 link: function (scope
, element
, attrs
) {
238 var binding
= parse(attrs
['binding'], true);
239 var titleLocked
= parse(attrs
['titleLocked'], ts('Locked'));
240 var titleUnlocked
= parse(attrs
['titleUnlocked'], ts('Unlocked'));
242 $(element
).addClass('ui-icon lock-button');
243 var refresh = function () {
244 var locked
= binding(scope
);
247 .removeClass('ui-icon-unlocked')
248 .addClass('ui-icon-locked')
249 .prop('title', titleLocked(scope
))
254 .removeClass('ui-icon-locked')
255 .addClass('ui-icon-unlocked')
256 .prop('title', titleUnlocked(scope
))
261 $(element
).click(function () {
262 binding
.assign(scope
, !binding(scope
));
264 $rootScope
.$digest();
267 scope
.$watch(attrs
.binding
, refresh
);
268 scope
.$watch(attrs
.titleLocked
, refresh
);
269 scope
.$watch(attrs
.titleUnlocked
, refresh
);
276 // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" crm-ui-select-model="myobj.field"><option...></select>
277 .directive('crmUiSelect', function ($parse
) {
281 crmUiSelectModel
: '@',
282 crmUiSelectChange
: '@'
284 link: function (scope
, element
, attrs
) {
285 var model
= $parse(attrs
.crmUiSelectModel
);
287 // In cases where UI initiates update, there may be an extra
288 // call to refreshUI, but it doesn't create a cycle.
290 function refreshUI() {
291 $(element
).select2('val', model(scope
.$parent
));
293 function refreshModel() {
294 var oldValue
= model(scope
.$parent
), newValue
= $(element
).select2('val');
295 if (oldValue
!= newValue
) {
296 scope
.$parent
.$apply(function(){
297 model
.assign(scope
.$parent
, newValue
);
299 if (attrs
.crmUiSelectChange
) {
300 scope
.$parent
.$eval(attrs
.crmUiSelectChange
);
305 // TODO watch select2-options
306 var options
= attrs
.crmUiSelect
? scope
.$parent
.$eval(attrs
.crmUiSelect
) : {};
307 $(element
).select2(options
);
308 $(element
).on('change', refreshModel
);
309 setTimeout(refreshUI
, 0);
310 scope
.$parent
.$watch(attrs
.crmUiSelectModel
, refreshUI
);
317 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
318 // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
319 .directive('crmUiTab', function($parse
) {
321 require
: '^crmUiTabSet',
327 template
: '<div ng-transclude></div>',
329 link: function (scope
, element
, attrs
, crmUiTabSetCtrl
) {
330 crmUiTabSetCtrl
.add(scope
);
335 // 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>
336 .directive('crmUiTabSet', function() {
342 templateUrl
: partialUrl('tabset.html'),
344 controllerAs
: 'crmUiTabSetCtrl',
345 controller: function($scope
, $parse
) {
346 var tabs
= $scope
.tabs
= []; // array<$scope>
347 this.add = function(tab
) {
348 if (!tab
.id
) throw "Tab is missing 'id'";
352 link: function (scope
, element
, attrs
) {}
356 // example: <input crm-ui-time="myobj.mytimefield" />
357 .directive('crmUiTime', function ($parse
, $timeout
) {
363 link: function (scope
, element
, attrs
) {
364 var model
= $parse(attrs
.crmUiTime
);
366 element
.addClass('crm-form-text six');
367 $(element
).timeEntry({show24Hours
: true});
369 var updateChildren
= (function() {
370 element
.off('change', updateParent
);
371 $(element
).timeEntry('setTime', model(scope
.$parent
));
372 element
.on('change', updateParent
);
374 var updateParent
= (function () {
375 $timeout(function () {
376 model
.assign(scope
.$parent
, element
.val());
381 scope
.$parent
.$watch(attrs
.crmUiTime
, updateChildren
);
382 element
.on('change', updateParent
);
387 // 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>
388 // Note: "myWizardCtrl" has various actions/properties like next() and $first().
389 // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
390 // WISHLIST: Allow each step to enable/disable (show/hide) itself
391 .directive('crmUiWizard', function() {
397 templateUrl
: partialUrl('wizard.html'),
399 controllerAs
: 'crmUiWizardCtrl',
400 controller: function($scope
, $parse
) {
401 var steps
= $scope
.steps
= []; // array<$scope>
402 var crmUiWizardCtrl
= this;
404 var selectedIndex
= null;
406 var findIndex = function() {
408 angular
.forEach(steps
, function(step
, stepKey
) {
409 if (step
.selected
) found
= stepKey
;
414 /// @return int the index of the current step
415 this.$index = function() { return selectedIndex
; };
416 /// @return bool whether the currentstep is first
417 this.$first = function() { return this.$index() === 0; };
418 /// @return bool whether the current step is last
419 this.$last = function() { return this.$index() === steps
.length
-1; };
420 this.$maxVisit = function() { return maxVisited
; }
421 this.iconFor = function(index
) {
422 if (index
< this.$index()) return '√';
423 if (index
=== this.$index()) return '»';
426 this.isSelectable = function(step
) {
427 if (step
.selected
) return false;
429 angular
.forEach(steps
, function(otherStep
, otherKey
) {
430 if (step
=== otherStep
&& otherKey
<= maxVisited
) result
= true;
435 /*** @param Object step the $scope of the step */
436 this.select = function(step
) {
437 angular
.forEach(steps
, function(otherStep
, otherKey
) {
438 otherStep
.selected
= (otherStep
=== step
);
439 if (otherStep
=== step
&& maxVisited
< otherKey
) maxVisited
= otherKey
;
441 selectedIndex
= findIndex();
443 /*** @param Object step the $scope of the step */
444 this.add = function(step
) {
445 if (steps
.length
=== 0) {
446 step
.selected
= true;
451 this.goto = function(index
) {
452 if (index
< 0) index
= 0;
453 if (index
>= steps
.length
) index
= steps
.length
-1;
454 this.select(steps
[index
]);
456 this.previous = function() { this.goto(this.$index()-1); };
457 this.next = function() { this.goto(this.$index()+1); };
458 if ($scope
.crmUiWizard
) {
459 $parse($scope
.crmUiWizard
).assign($scope
.$parent
, this)
462 link: function (scope
, element
, attrs
) {}
466 // Use this to add extra markup to wizard
467 .directive('crmUiWizardButtons', function() {
469 require
: '^crmUiWizard',
472 template
: '<span ng-transclude></span>',
474 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
475 var realButtonsEl
= $(element
).closest('.crm-wizard').find('.crm-wizard-buttons');
476 $(element
).appendTo(realButtonsEl
);
481 // example <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
482 .directive('crmUiWizardStep', function() {
484 require
: '^crmUiWizard',
489 template
: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
491 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
492 crmUiWizardCtrl
.add(scope
);
497 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
498 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
499 .directive('crmConfirm', function () {
500 // Helpers to calculate default options for CRM.confirm()
502 'disable': function (options
) {
504 message
: ts('Are you sure you want to disable this?'),
505 options
: {no
: ts('Cancel'), yes
: ts('Disable')},
507 title
: ts('Disable %1?', {
508 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
512 'revert': function (options
) {
514 message
: ts('Are you sure you want to revert this?'),
515 options
: {no
: ts('Cancel'), yes
: ts('Revert')},
517 title
: ts('Revert %1?', {
518 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
522 'delete': function (options
) {
524 message
: ts('Are you sure you want to delete this?'),
525 options
: {no
: ts('Cancel'), yes
: ts('Delete')},
527 title
: ts('Delete %1?', {
528 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
535 link: function (scope
, element
, attrs
) {
536 $(element
).click(function () {
537 var options
= scope
.$eval(attrs
['crmConfirm']);
538 var defaults
= (options
.type
) ? defaultFuncs
[options
.type
](options
) : {};
539 CRM
.confirm(_
.extend(defaults
, options
))
540 .on('crmConfirm:yes', function () { scope
.$apply(attrs
['onYes']); })
541 .on('crmConfirm:no', function () { scope
.$apply(attrs
['onNo']); });
546 .run(function($rootScope
, $location
) {
547 /// Example: <button ng-click="goto('home')">Go home!</button>
548 $rootScope
.goto = function(path
) {
549 $location
.path(path
);
551 // useful for debugging: $rootScope.log = console.log || function() {};
555 })(angular
, CRM
.$, CRM
._
);