1 /// crmUi: Sundry UI helpers
2 (function (angular
, $, _
) {
6 var partialUrl = function (relPath
) {
7 return CRM
.resourceUrls
['civicrm'] + '/partials/crmUi/' + relPath
;
10 angular
.module('crmUi', [])
12 .factory('crmUiId', function() {
14 // Get the HTML ID of an element. If none available, assign one.
15 return function crmUiId(el
){
16 var id
= el
.attr('id');
18 id
= 'crmUi_' + (++idCount
);
25 // example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
26 // WISHLIST: crmCollapsed should support two-way/continous binding
27 .directive('crmUiAccordion', function() {
33 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>',
35 link: function (scope
, element
, attrs
) {
37 collapsed
: scope
.$parent
.$eval(attrs
.crmCollapsed
)
43 // example: <input crm-ui-date="myobj.datefield" />
44 // example: <input crm-ui-date="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
45 .directive('crmUiDate', function ($parse
, $timeout
) {
49 crmUiDate
: '@', // expression, model binding
50 crmUiDateFormat
: '@' // expression, date format (default: "yy-mm-dd")
52 link: function (scope
, element
, attrs
) {
53 var fmt
= attrs
.crmUiDateFormat
? $parse(attrs
.crmUiDateFormat
)() : "yy-mm-dd";
54 var model
= $parse(attrs
.crmUiDate
);
56 element
.addClass('dateplugin');
57 $(element
).datepicker({
61 var updateChildren
= (function() {
62 element
.off('change', updateParent
);
63 $(element
).datepicker('setDate', model(scope
.$parent
));
64 element
.on('change', updateParent
);
66 var updateParent
= (function() {
67 $timeout(function () {
68 model
.assign(scope
.$parent
, $(element
).val());
73 scope
.$parent
.$watch(attrs
.crmUiDate
, updateChildren
);
74 element
.on('change', updateParent
);
79 // example: <div crm-ui-date-time="myobj.mydatetimefield"></div>
80 .directive('crmUiDateTime', function ($parse
) {
86 template
: '<input crm-ui-date="dtparts.date" placeholder="{{dateLabel}}"/> <input crm-ui-time="dtparts.time" placeholder="{{timeLabel}}"/>',
87 link: function (scope
, element
, attrs
) {
88 var model
= $parse(attrs
.crmUiDateTime
);
89 scope
.dateLabel
= ts('Date');
90 scope
.timeLabel
= ts('Time');
92 var updateChildren
= (function () {
93 var value
= model(scope
.$parent
);
95 var dtparts
= value
.split(/ /);
96 scope
.dtparts
= {date
: dtparts
[0], time
: dtparts
[1]};
99 scope
.dtparts
= {date
: '', time
: ''};
102 var updateParent
= (function () {
103 model
.assign(scope
.$parent
, scope
.dtparts
.date
+ " " + scope
.dtparts
.time
);
107 scope
.$parent
.$watch(attrs
.crmUiDateTime
, updateChildren
);
108 scope
.$watch('dtparts.date', updateParent
),
109 scope
.$watch('dtparts.time', updateParent
)
114 // Display a field/row in a field list
115 // example: <div crm-ui-field crm-title="My Field"> {{mydata}} </div>
116 // example: <div crm-ui-field="subform.myfield" crm-title="'My Field'"> <input crm-ui-id="subform.myfield" name="myfield" /> </div>
117 // example: <div crm-ui-field="subform.myfield" crm-title="'My Field'"> <input crm-ui-id="subform.myfield" name="myfield" required /> </div>
118 .directive('crmUiField', function(crmUiId
) {
119 // Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
121 default: partialUrl('field.html'),
122 checkbox
: partialUrl('field-cb.html')
126 require
: '^crmUiIdScope',
132 templateUrl: function(tElement
, tAttrs
){
133 var layout
= tAttrs
.crmLayout
? tAttrs
.crmLayout
: 'default';
134 return templateUrls
[layout
];
137 link: function (scope
, element
, attrs
, crmUiIdCtrl
) {
138 $(element
).addClass('crm-section');
139 scope
.crmUiField
= attrs
.crmUiField
;
140 scope
.crmTitle
= attrs
.crmTitle
;
145 // 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>
146 .directive('crmUiId', function () {
148 require
: '^crmUiIdScope',
150 link: function (scope
, element
, attrs
, crmUiIdCtrl
) {
151 var id
= crmUiIdCtrl
.get(attrs
.crmUiId
);
152 element
.attr('id', id
);
157 // 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>
158 .directive('crmUiFor', function ($parse
, $timeout
) {
160 require
: '^crmUiIdScope',
162 template
: '<span ng-class="cssClasses"><span ng-transclude/><span crm-ui-visible="crmIsRequired" class="crm-marker" title="This field is required.">*</span></span>',
164 link: function (scope
, element
, attrs
, crmUiIdCtrl
) {
165 scope
.crmIsRequired
= false;
166 scope
.cssClasses
= {};
168 if (!attrs
.crmUiFor
) return;
170 var id
= crmUiIdCtrl
.get(attrs
.crmUiFor
);
171 element
.attr('for', id
);
174 var updateCss = function () {
175 scope
.cssClasses
['crm-error'] = !ngModel
.$valid
&& !ngModel
.$pristine
;
178 // Note: if target element is dynamically generated (eg via ngInclude), then it may not be available
179 // immediately for initialization. Use retries/retryDelay to initialize such elements.
180 var init = function (retries
, retryDelay
) {
181 var input
= $('#' + id
);
182 if (input
.length
== 0) {
185 init(retries
-1, retryDelay
);
191 var tgtScope
= scope
;//.$parent;
192 if (attrs
.crmDepth
) {
193 for (var i
= attrs
.crmDepth
; i
> 0; i
--) {
194 tgtScope
= tgtScope
.$parent
;
198 if (input
.attr('ng-required')) {
199 scope
.crmIsRequired
= scope
.$parent
.$eval(input
.attr('ng-required'));
200 scope
.$parent
.$watch(input
.attr('ng-required'), function (isRequired
) {
201 scope
.crmIsRequired
= isRequired
;
205 scope
.crmIsRequired
= input
.prop('required')
208 ngModel
= $parse(attrs
.crmUiFor
)(tgtScope
);
210 ngModel
.$viewChangeListeners
.push(updateCss
);
221 // 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>
222 .directive('crmUiIdScope', function () {
226 controllerAs
: 'crmUiIdCtrl',
227 controller: function($scope
) {
229 this.get = function(name
) {
231 ids
[name
] = "crmUiId_" + (++uidCount
);
236 link: function (scope
, element
, attrs
) {}
240 // example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
241 .directive('crmUiIframe', function ($parse
) {
244 crmUiIframe
: '@' // expression which evalutes to HTML content
246 link: function (scope
, elm
, attrs
) {
247 var iframe
= $(elm
)[0];
248 iframe
.setAttribute('width', '100%');
249 iframe
.setAttribute('frameborder', '0');
251 var refresh = function () {
252 // 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>';
253 var iframeHtml
= scope
.$parent
.$eval(attrs
.crmUiIframe
);
255 var doc
= iframe
.document
;
256 if (iframe
.contentDocument
) {
257 doc
= iframe
.contentDocument
;
259 else if (iframe
.contentWindow
) {
260 doc
= iframe
.contentWindow
.document
;
264 doc
.writeln(iframeHtml
);
268 scope
.$parent
.$watch(attrs
.crmUiIframe
, refresh
);
269 //setTimeout(function () { refresh(); }, 50);
274 // example: <textarea crm-ui-richtext name="body_html" ng-model="mailing.body_html"></textarea>
275 .directive('crmUiRichtext', function (crmUiId
, $timeout
) {
278 link: function (scope
, elm
, attr
, ngModel
) {
280 var ck
= CKEDITOR
.replace(elm
[0]);
286 ck
.on('pasteState', function () {
287 scope
.$apply(function () {
288 ngModel
.$setViewValue(ck
.getData());
292 ck
.on('insertText', function () {
293 $timeout(function () {
294 ngModel
.$setViewValue(ck
.getData());
298 ngModel
.$render = function (value
) {
299 ck
.setData(ngModel
.$viewValue
);
305 // example: <a crm-ui-lock binding="mymodel.boolfield"></a>
306 // example: <a crm-ui-lock
307 // binding="mymodel.boolfield"
308 // title-locked="ts('Boolfield is locked')"
309 // title-unlocked="ts('Boolfield is unlocked')"></a>
310 .directive('crmUiLock', function ($parse
, $rootScope
) {
311 var defaultVal = function (defaultValue
) {
312 var f = function (scope
) {
315 f
.assign = function (scope
, value
) {
321 // like $parse, but accepts a defaultValue in case expr is undefined
322 var parse = function (expr
, defaultValue
) {
323 return expr
? $parse(expr
) : defaultVal(defaultValue
);
328 link: function (scope
, element
, attrs
) {
329 var binding
= parse(attrs
['binding'], true);
330 var titleLocked
= parse(attrs
['titleLocked'], ts('Locked'));
331 var titleUnlocked
= parse(attrs
['titleUnlocked'], ts('Unlocked'));
333 $(element
).addClass('ui-icon lock-button');
334 var refresh = function () {
335 var locked
= binding(scope
);
338 .removeClass('ui-icon-unlocked')
339 .addClass('ui-icon-locked')
340 .prop('title', titleLocked(scope
))
345 .removeClass('ui-icon-locked')
346 .addClass('ui-icon-unlocked')
347 .prop('title', titleUnlocked(scope
))
352 $(element
).click(function () {
353 binding
.assign(scope
, !binding(scope
));
355 $rootScope
.$digest();
358 scope
.$watch(attrs
.binding
, refresh
);
359 scope
.$watch(attrs
.titleLocked
, refresh
);
360 scope
.$watch(attrs
.titleUnlocked
, refresh
);
367 // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
368 .directive('crmUiSelect', function ($parse
, $timeout
) {
374 link: function (scope
, element
, attrs
, ngModel
) {
375 // In cases where UI initiates update, there may be an extra
376 // call to refreshUI, but it doesn't create a cycle.
378 ngModel
.$render = function () {
379 $timeout(function () {
380 // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
381 // new item is added before selection is made
382 $(element
).select2('val', ngModel
.$viewValue
);
385 function refreshModel() {
386 var oldValue
= ngModel
.$viewValue
, newValue
= $(element
).select2('val');
387 if (oldValue
!= newValue
) {
388 scope
.$parent
.$apply(function () {
389 ngModel
.$setViewValue(newValue
);
395 // TODO watch select2-options
396 var options
= attrs
.crmUiSelect
? scope
.$parent
.$eval(attrs
.crmUiSelect
) : {};
397 $(element
).select2(options
);
398 $(element
).on('change', refreshModel
);
399 $timeout(ngModel
.$render
);
407 // example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
408 // WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
409 .directive('crmUiTab', function($parse
) {
411 require
: '^crmUiTabSet',
417 template
: '<div ng-transclude></div>',
419 link: function (scope
, element
, attrs
, crmUiTabSetCtrl
) {
420 crmUiTabSetCtrl
.add(scope
);
425 // 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>
426 .directive('crmUiTabSet', function() {
432 templateUrl
: partialUrl('tabset.html'),
434 controllerAs
: 'crmUiTabSetCtrl',
435 controller: function($scope
, $parse
) {
436 var tabs
= $scope
.tabs
= []; // array<$scope>
437 this.add = function(tab
) {
438 if (!tab
.id
) throw "Tab is missing 'id'";
442 link: function (scope
, element
, attrs
) {}
446 // example: <input crm-ui-time="myobj.mytimefield" />
447 .directive('crmUiTime', function ($parse
, $timeout
) {
453 link: function (scope
, element
, attrs
) {
454 var model
= $parse(attrs
.crmUiTime
);
456 element
.addClass('crm-form-text six');
457 $(element
).timeEntry({show24Hours
: true});
459 var updateChildren
= (function() {
460 element
.off('change', updateParent
);
461 $(element
).timeEntry('setTime', model(scope
.$parent
));
462 element
.on('change', updateParent
);
464 var updateParent
= (function () {
465 $timeout(function () {
466 model
.assign(scope
.$parent
, element
.val());
471 scope
.$parent
.$watch(attrs
.crmUiTime
, updateChildren
);
472 element
.on('change', updateParent
);
477 // like ng-show, but hides/displays elements using "visibility" which maintains positioning
478 // example <div crm-ui-visible="false">...content...</div>
479 .directive('crmUiVisible', function($parse
) {
485 link: function (scope
, element
, attrs
) {
486 var model
= $parse(attrs
.crmUiVisible
);
487 function updatecChildren() {
488 element
.css('visibility', model(scope
.$parent
) ? 'inherit' : 'hidden');
491 scope
.$parent
.$watch(attrs
.crmUiVisible
, updatecChildren
);
496 // 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>
497 // Note: "myWizardCtrl" has various actions/properties like next() and $first().
498 // WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
499 // WISHLIST: Allow each step to enable/disable (show/hide) itself
500 .directive('crmUiWizard', function() {
506 templateUrl
: partialUrl('wizard.html'),
508 controllerAs
: 'crmUiWizardCtrl',
509 controller: function($scope
, $parse
) {
510 var steps
= $scope
.steps
= []; // array<$scope>
511 var crmUiWizardCtrl
= this;
513 var selectedIndex
= null;
515 var findIndex = function() {
517 angular
.forEach(steps
, function(step
, stepKey
) {
518 if (step
.selected
) found
= stepKey
;
523 /// @return int the index of the current step
524 this.$index = function() { return selectedIndex
; };
525 /// @return bool whether the currentstep is first
526 this.$first = function() { return this.$index() === 0; };
527 /// @return bool whether the current step is last
528 this.$last = function() { return this.$index() === steps
.length
-1; };
529 this.$maxVisit = function() { return maxVisited
; }
530 this.iconFor = function(index
) {
531 if (index
< this.$index()) return '√';
532 if (index
=== this.$index()) return '»';
535 this.isSelectable = function(step
) {
536 if (step
.selected
) return false;
538 angular
.forEach(steps
, function(otherStep
, otherKey
) {
539 if (step
=== otherStep
&& otherKey
<= maxVisited
) result
= true;
544 /*** @param Object step the $scope of the step */
545 this.select = function(step
) {
546 angular
.forEach(steps
, function(otherStep
, otherKey
) {
547 otherStep
.selected
= (otherStep
=== step
);
548 if (otherStep
=== step
&& maxVisited
< otherKey
) maxVisited
= otherKey
;
550 selectedIndex
= findIndex();
552 /*** @param Object step the $scope of the step */
553 this.add = function(step
) {
554 if (steps
.length
=== 0) {
555 step
.selected
= true;
559 steps
.sort(function(a
,b
){
560 return a
.crmUiWizardStep
- b
.crmUiWizardStep
;
562 selectedIndex
= findIndex();
564 this.remove = function(step
) {
566 angular
.forEach(steps
, function(otherStep
, otherKey
) {
567 if (otherStep
=== step
) key
= otherKey
;
570 steps
.splice(key
, 1);
573 this.goto = function(index
) {
574 if (index
< 0) index
= 0;
575 if (index
>= steps
.length
) index
= steps
.length
-1;
576 this.select(steps
[index
]);
578 this.previous = function() { this.goto(this.$index()-1); };
579 this.next = function() { this.goto(this.$index()+1); };
580 if ($scope
.crmUiWizard
) {
581 $parse($scope
.crmUiWizard
).assign($scope
.$parent
, this)
584 link: function (scope
, element
, attrs
) {}
588 // Use this to add extra markup to wizard
589 .directive('crmUiWizardButtons', function() {
591 require
: '^crmUiWizard',
594 template
: '<span ng-transclude></span>',
596 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
597 var realButtonsEl
= $(element
).closest('.crm-wizard').find('.crm-wizard-buttons');
598 $(element
).appendTo(realButtonsEl
);
603 // example: <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
604 // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering.
605 // example: <div crm-ui-wizard-step="100" crm-title="..." ng-if="...">...content...</div>
606 .directive('crmUiWizardStep', function() {
609 require
: '^crmUiWizard',
612 crmTitle
: '@', // expression, evaluates to a printable string
613 crmUiWizardStep
: '@' // int, a weight which determines the ordering of the steps
615 template
: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
617 link: function (scope
, element
, attrs
, crmUiWizardCtrl
) {
618 if (scope
.crmUiWizardStep
) {
619 scope
.crmUiWizardStep
= parseInt(scope
.crmUiWizardStep
);
621 scope
.crmUiWizardStep
= nextWeight
++;
623 crmUiWizardCtrl
.add(scope
);
624 element
.on('$destroy', function(){
625 crmUiWizardCtrl
.remove(scope
);
631 // Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
632 // Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
633 .directive('crmConfirm', function () {
634 // Helpers to calculate default options for CRM.confirm()
636 'disable': function (options
) {
638 message
: ts('Are you sure you want to disable this?'),
639 options
: {no
: ts('Cancel'), yes
: ts('Disable')},
641 title
: ts('Disable %1?', {
642 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
646 'revert': function (options
) {
648 message
: ts('Are you sure you want to revert this?'),
649 options
: {no
: ts('Cancel'), yes
: ts('Revert')},
651 title
: ts('Revert %1?', {
652 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
656 'delete': function (options
) {
658 message
: ts('Are you sure you want to delete this?'),
659 options
: {no
: ts('Cancel'), yes
: ts('Delete')},
661 title
: ts('Delete %1?', {
662 1: options
.obj
.title
|| options
.obj
.label
|| options
.obj
.name
|| ts('the record')
669 link: function (scope
, element
, attrs
) {
670 $(element
).click(function () {
671 var options
= scope
.$eval(attrs
['crmConfirm']);
672 var defaults
= (options
.type
) ? defaultFuncs
[options
.type
](options
) : {};
673 CRM
.confirm(_
.extend(defaults
, options
))
674 .on('crmConfirm:yes', function () { scope
.$apply(attrs
['onYes']); })
675 .on('crmConfirm:no', function () { scope
.$apply(attrs
['onNo']); });
680 .run(function($rootScope
, $location
) {
681 /// Example: <button ng-click="goto('home')">Go home!</button>
682 $rootScope
.goto = function(path
) {
683 $location
.path(path
);
685 // useful for debugging: $rootScope.log = console.log || function() {};
689 })(angular
, CRM
.$, CRM
._
);