/// crmUi: Sundry UI helpers
(function (angular, $, _) {
- var idCount = 0;
-
- var partialUrl = function (relPath) {
- return CRM.resourceUrls['civicrm'] + '/partials/crmUi/' + relPath;
- };
+ var uidCount = 0;
angular.module('crmUi', [])
};
})
+ // example: <input crm-ui-date="myobj.datefield" />
+ // example: <input crm-ui-date="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
+ .directive('crmUiDate', function ($parse, $timeout) {
+ return {
+ restrict: 'AE',
+ scope: {
+ crmUiDate: '@', // expression, model binding
+ crmUiDateFormat: '@' // expression, date format (default: "yy-mm-dd")
+ },
+ link: function (scope, element, attrs) {
+ var fmt = attrs.crmUiDateFormat ? $parse(attrs.crmUiDateFormat)() : "yy-mm-dd";
+ var model = $parse(attrs.crmUiDate);
+
+ element.addClass('dateplugin');
+ $(element).datepicker({
+ dateFormat: fmt
+ });
+
+ var updateChildren = (function() {
+ element.off('change', updateParent);
+ $(element).datepicker('setDate', model(scope.$parent));
+ element.on('change', updateParent);
+ });
+ var updateParent = (function() {
+ $timeout(function () {
+ model.assign(scope.$parent, $(element).val());
+ });
+ });
+
+ updateChildren();
+ scope.$parent.$watch(attrs.crmUiDate, updateChildren);
+ element.on('change', updateParent);
+ }
+ };
+ })
+
+ // example: <div crm-ui-date-time="myobj.mydatetimefield"></div>
+ .directive('crmUiDateTime', function ($parse) {
+ return {
+ restrict: 'AE',
+ scope: {
+ crmUiDateTime: '@'
+ },
+ template: '<input crm-ui-date="dtparts.date" placeholder="{{dateLabel}}"/> <input crm-ui-time="dtparts.time" placeholder="{{timeLabel}}"/>',
+ link: function (scope, element, attrs) {
+ var model = $parse(attrs.crmUiDateTime);
+ scope.dateLabel = ts('Date');
+ scope.timeLabel = ts('Time');
+
+ var updateChildren = (function () {
+ var value = model(scope.$parent);
+ if (value) {
+ var dtparts = value.split(/ /);
+ scope.dtparts = {date: dtparts[0], time: dtparts[1]};
+ }
+ else {
+ scope.dtparts = {date: '', time: ''};
+ }
+ });
+ var updateParent = (function () {
+ model.assign(scope.$parent, scope.dtparts.date + " " + scope.dtparts.time);
+ });
+
+ updateChildren();
+ scope.$parent.$watch(attrs.crmUiDateTime, updateChildren);
+ scope.$watch('dtparts.date', updateParent);
+ scope.$watch('dtparts.time', updateParent);
+ }
+ };
+ })
+
+ // Display a field/row in a field list
+ // example: <div crm-ui-field crm-title="My Field"> {{mydata}} </div>
+ // example: <div crm-ui-field="subform.myfield" crm-title="'My Field'"> <input crm-ui-id="subform.myfield" name="myfield" /> </div>
+ // example: <div crm-ui-field="subform.myfield" crm-title="'My Field'"> <input crm-ui-id="subform.myfield" name="myfield" required /> </div>
+ .directive('crmUiField', function() {
+ // Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
+ var templateUrls = {
+ default: '~/crmUi/field.html',
+ checkbox: '~/crmUi/field-cb.html'
+ };
+
+ return {
+ require: '^crmUiIdScope',
+ restrict: 'EA',
+ scope: {
+ crmUiField: '@',
+ crmTitle: '@'
+ },
+ templateUrl: function(tElement, tAttrs){
+ var layout = tAttrs.crmLayout ? tAttrs.crmLayout : 'default';
+ return templateUrls[layout];
+ },
+ transclude: true,
+ link: function (scope, element, attrs, crmUiIdCtrl) {
+ $(element).addClass('crm-section');
+ scope.crmUiField = attrs.crmUiField;
+ scope.crmTitle = attrs.crmTitle;
+ }
+ };
+ })
+
+ // 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>
+ .directive('crmUiId', function () {
+ return {
+ require: '^crmUiIdScope',
+ restrict: 'EA',
+ link: {
+ pre: function (scope, element, attrs, crmUiIdCtrl) {
+ var id = crmUiIdCtrl.get(attrs.crmUiId);
+ element.attr('id', id);
+ }
+ }
+ };
+ })
+
+ // 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>
+ .directive('crmUiFor', function ($parse, $timeout) {
+ return {
+ require: '^crmUiIdScope',
+ restrict: 'EA',
+ template: '<span ng-class="cssClasses"><span ng-transclude/><span crm-ui-visible="crmIsRequired" class="crm-marker" title="This field is required.">*</span></span>',
+ transclude: true,
+ link: function (scope, element, attrs, crmUiIdCtrl) {
+ scope.crmIsRequired = false;
+ scope.cssClasses = {};
+
+ if (!attrs.crmUiFor) return;
+
+ var id = crmUiIdCtrl.get(attrs.crmUiFor);
+ element.attr('for', id);
+ var ngModel = null;
+
+ var updateCss = function () {
+ scope.cssClasses['crm-error'] = !ngModel.$valid && !ngModel.$pristine;
+ };
+
+ // Note: if target element is dynamically generated (eg via ngInclude), then it may not be available
+ // immediately for initialization. Use retries/retryDelay to initialize such elements.
+ var init = function (retries, retryDelay) {
+ var input = $('#' + id);
+ if (input.length === 0) {
+ if (retries) {
+ $timeout(function(){
+ init(retries-1, retryDelay);
+ }, retryDelay);
+ }
+ return;
+ }
+
+ var tgtScope = scope;//.$parent;
+ if (attrs.crmDepth) {
+ for (var i = attrs.crmDepth; i > 0; i--) {
+ tgtScope = tgtScope.$parent;
+ }
+ }
+
+ if (input.attr('ng-required')) {
+ scope.crmIsRequired = scope.$parent.$eval(input.attr('ng-required'));
+ scope.$parent.$watch(input.attr('ng-required'), function (isRequired) {
+ scope.crmIsRequired = isRequired;
+ });
+ }
+ else {
+ scope.crmIsRequired = input.prop('required');
+ }
+
+ ngModel = $parse(attrs.crmUiFor)(tgtScope);
+ if (ngModel) {
+ ngModel.$viewChangeListeners.push(updateCss);
+ }
+ };
+
+ $timeout(function(){
+ init(3, 100);
+ });
+ }
+ };
+ })
+
+ // 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>
+ .directive('crmUiIdScope', function () {
+ return {
+ restrict: 'EA',
+ scope: {},
+ controllerAs: 'crmUiIdCtrl',
+ controller: function($scope) {
+ var ids = {};
+ this.get = function(name) {
+ if (!ids[name]) {
+ ids[name] = "crmUiId_" + (++uidCount);
+ }
+ return ids[name];
+ };
+ },
+ link: function (scope, element, attrs) {}
+ };
+ })
+
// example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
.directive('crmUiIframe', function ($parse) {
return {
doc.open();
doc.writeln(iframeHtml);
doc.close();
- }
+ };
scope.$parent.$watch(attrs.crmUiIframe, refresh);
//setTimeout(function () { refresh(); }, 50);
};
})
- // example: <form name="myForm">...<label crm-ui-label crm-for="myField">My Field</span>...<input name="myField"/>...</form>
- //
- // Label adapts based on <input required>, <input ng-required>, or any other validation.
- //
- // Note: This should work in the normal case where <label> and <input> are in roughly the same scope,
- // but if the scopes are materially different then problems could arise.
- .directive('crmUiLabel', function($parse) {
+ // example: <textarea crm-ui-id="myForm.body_html" crm-ui-richtext name="body_html" ng-model="mailing.body_html"></textarea>
+ .directive('crmUiRichtext', function ($timeout) {
return {
- scope: {
- name: '@'
- },
- transclude: true,
- template: '<span ng-class="cssClasses"><span ng-transclude></span> <span ng-show="crmRequired" class="crm-marker" title="This field is required.">*</span></span>',
- link: function(scope, element, attrs) {
- if (attrs.crmFor == 'name') {
- throw new Error('Validation monitoring does not work for field name "name"');
- }
-
- // 1. Figure out form and input elements
+ require: '?ngModel',
+ link: function (scope, elm, attr, ngModel) {
+ var ck = CKEDITOR.replace(elm[0]);
- var form = $(element).closest('form');
- var formCtrl = scope.$parent.$eval(form.attr('name'));
- var input = $('input[name="' + attrs.crmFor + '"],select[name="' + attrs.crmFor + '"],textarea[name="' + attrs.crmFor + '"]', form);
- if (form.length != 1 || input.length != 1) {
- 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);
+ if (!ngModel) {
return;
}
- // 2. Make sure that inputs are well-defined (with name+id).
-
- if (!input.attr('id')) {
- input.attr('id', 'crmUi_' + (++idCount));
- }
- $(element).attr('for', input.attr('id'));
-
- // 3. Monitor is the "required" and "$valid" properties
-
- if (input.attr('ng-required')) {
- scope.crmRequired = scope.$parent.$eval(input.attr('ng-required'));
- scope.$parent.$watch(input.attr('ng-required'), function(isRequired) {
- scope.crmRequired = isRequired;
+ ck.on('pasteState', function () {
+ scope.$apply(function () {
+ ngModel.$setViewValue(ck.getData());
});
- } else {
- scope.crmRequired = input.prop('required');
- }
-
- var inputCtrl = form.attr('name') + '.' + input.attr('name');
- scope.cssClasses = {};
- scope.$parent.$watch(inputCtrl + '.$valid', function(newValue) {
- //scope.cssClasses['ng-valid'] = newValue;
- //scope.cssClasses['ng-invalid'] = !newValue;
- scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
});
- scope.$parent.$watch(inputCtrl + '.$pristine', function(newValue) {
- //scope.cssClasses['ng-pristine'] = newValue;
- //scope.cssClasses['ng-dirty'] = !newValue;
- scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine');
+
+ ck.on('insertText', function () {
+ $timeout(function () {
+ ngModel.$setViewValue(ck.getData());
+ });
});
+ ngModel.$render = function (value) {
+ ck.setData(ngModel.$viewValue);
+ };
}
};
})
var defaultVal = function (defaultValue) {
var f = function (scope) {
return defaultValue;
- }
+ };
f.assign = function (scope, value) {
// ignore changes
- }
+ };
return f;
};
return {
template: '',
link: function (scope, element, attrs) {
- var binding = parse(attrs['binding'], true);
- var titleLocked = parse(attrs['titleLocked'], ts('Locked'));
- var titleUnlocked = parse(attrs['titleUnlocked'], ts('Unlocked'));
+ var binding = parse(attrs.binding, true);
+ var titleLocked = parse(attrs.titleLocked, ts('Locked'));
+ var titleUnlocked = parse(attrs.titleUnlocked, ts('Unlocked'));
$(element).addClass('ui-icon lock-button');
var refresh = function () {
};
})
+ // usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
+ .directive('crmUiSelect', function ($parse, $timeout) {
+ return {
+ require: '?ngModel',
+ scope: {
+ crmUiSelect: '@'
+ },
+ link: function (scope, element, attrs, ngModel) {
+ // In cases where UI initiates update, there may be an extra
+ // call to refreshUI, but it doesn't create a cycle.
+
+ ngModel.$render = function () {
+ $timeout(function () {
+ // ex: msg_template_id adds new item then selects it; use $timeout to ensure that
+ // new item is added before selection is made
+ $(element).select2('val', ngModel.$viewValue);
+ });
+ };
+ function refreshModel() {
+ var oldValue = ngModel.$viewValue, newValue = $(element).select2('val');
+ if (oldValue != newValue) {
+ scope.$parent.$apply(function () {
+ ngModel.$setViewValue(newValue);
+ });
+ }
+ }
+
+ function init() {
+ // TODO watch select2-options
+ var options = attrs.crmUiSelect ? scope.$parent.$eval(attrs.crmUiSelect) : {};
+ $(element).select2(options);
+ $(element).on('change', refreshModel);
+ $timeout(ngModel.$render);
+ }
+
+ init();
+ }
+ };
+ })
+
// example <div crm-ui-tab crm-title="ts('My Title')">...content...</div>
// WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
.directive('crmUiTab', function($parse) {
scope: {
crmUiTabSet: '@'
},
- templateUrl: partialUrl('tabset.html'),
+ templateUrl: '~/crmUi/tabset.html',
transclude: true,
controllerAs: 'crmUiTabSetCtrl',
controller: function($scope, $parse) {
};
})
+ // example: <input crm-ui-time="myobj.mytimefield" />
+ .directive('crmUiTime', function ($parse, $timeout) {
+ return {
+ restrict: 'AE',
+ scope: {
+ crmUiTime: '@'
+ },
+ link: function (scope, element, attrs) {
+ var model = $parse(attrs.crmUiTime);
+
+ element.addClass('crm-form-text six');
+ $(element).timeEntry({show24Hours: true});
+
+ var updateChildren = (function() {
+ element.off('change', updateParent);
+ $(element).timeEntry('setTime', model(scope.$parent));
+ element.on('change', updateParent);
+ });
+ var updateParent = (function () {
+ $timeout(function () {
+ model.assign(scope.$parent, element.val());
+ });
+ });
+
+ updateChildren();
+ scope.$parent.$watch(attrs.crmUiTime, updateChildren);
+ element.on('change', updateParent);
+ }
+ };
+ })
+
+ // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" />
+ // example: <span ng-model="placeholder" crm-ui-validate="foo && bar || whiz" crm-ui-validate-name="myError" />
+ // Generic, field-independent validator.
+ .directive('crmUiValidate', function() {
+ return {
+ restrict: 'EA',
+ require: 'ngModel',
+ link: function(scope, element, attrs, ngModel) {
+ var validationKey = attrs.crmUiValidateName ? attrs.crmUiValidateName : 'crmUiValidate';
+ scope.$watch(attrs.crmUiValidate, function(newValue){
+ ngModel.$setValidity(validationKey, !!newValue);
+ });
+ }
+ };
+ })
+
+ // like ng-show, but hides/displays elements using "visibility" which maintains positioning
+ // example <div crm-ui-visible="false">...content...</div>
+ .directive('crmUiVisible', function($parse) {
+ return {
+ restrict: 'EA',
+ scope: {
+ crmUiVisible: '@'
+ },
+ link: function (scope, element, attrs) {
+ var model = $parse(attrs.crmUiVisible);
+ function updatecChildren() {
+ element.css('visibility', model(scope.$parent) ? 'inherit' : 'hidden');
+ }
+ updatecChildren();
+ scope.$parent.$watch(attrs.crmUiVisible, updatecChildren);
+ }
+ };
+ })
+
// 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>
// Note: "myWizardCtrl" has various actions/properties like next() and $first().
// WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
scope: {
crmUiWizard: '@'
},
- templateUrl: partialUrl('wizard.html'),
+ templateUrl: '~/crmUi/wizard.html',
transclude: true,
controllerAs: 'crmUiWizardCtrl',
controller: function($scope, $parse) {
this.$first = function() { return this.$index() === 0; };
/// @return bool whether the current step is last
this.$last = function() { return this.$index() === steps.length -1; };
- this.$maxVisit = function() { return maxVisited; }
+ this.$maxVisit = function() { return maxVisited; };
+ this.$validStep = function() {
+ return steps[selectedIndex].isStepValid();
+ };
this.iconFor = function(index) {
if (index < this.$index()) return '√';
if (index === this.$index()) return '»';
return ' ';
- }
+ };
this.isSelectable = function(step) {
if (step.selected) return false;
var result = false;
selectedIndex = 0;
}
steps.push(step);
+ steps.sort(function(a,b){
+ return a.crmUiWizardStep - b.crmUiWizardStep;
+ });
+ selectedIndex = findIndex();
+ };
+ this.remove = function(step) {
+ var key = null;
+ angular.forEach(steps, function(otherStep, otherKey) {
+ if (otherStep === step) key = otherKey;
+ });
+ if (key !== null) {
+ steps.splice(key, 1);
+ }
};
this.goto = function(index) {
if (index < 0) index = 0;
this.previous = function() { this.goto(this.$index()-1); };
this.next = function() { this.goto(this.$index()+1); };
if ($scope.crmUiWizard) {
- $parse($scope.crmUiWizard).assign($scope.$parent, this)
+ $parse($scope.crmUiWizard).assign($scope.$parent, this);
}
},
link: function (scope, element, attrs) {}
};
})
- // example <div crm-ui-wizard-step crm-title="ts('My Title')">...content...</div>
+ // example: <div crm-ui-wizard-step crm-title="ts('My Title')" ng-form="mySubForm">...content...</div>
+ // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering.
+ // example: <div crm-ui-wizard-step="100" crm-title="..." ng-if="...">...content...</div>
.directive('crmUiWizardStep', function() {
+ var nextWeight = 1;
return {
- require: '^crmUiWizard',
+ require: ['^crmUiWizard', 'form'],
restrict: 'EA',
scope: {
- crmTitle: '@'
+ crmTitle: '@', // expression, evaluates to a printable string
+ crmUiWizardStep: '@' // int, a weight which determines the ordering of the steps
},
template: '<div class="crm-wizard-step" ng-show="selected" ng-transclude/></div>',
transclude: true,
- link: function (scope, element, attrs, crmUiWizardCtrl) {
+ link: function (scope, element, attrs, ctrls) {
+ var crmUiWizardCtrl = ctrls[0], form = ctrls[1];
+ if (scope.crmUiWizardStep) {
+ scope.crmUiWizardStep = parseInt(scope.crmUiWizardStep);
+ } else {
+ scope.crmUiWizardStep = nextWeight++;
+ }
+ scope.isStepValid = function() {
+ return form.$valid;
+ };
crmUiWizardCtrl.add(scope);
+ element.on('$destroy', function(){
+ crmUiWizardCtrl.remove(scope);
+ });
}
};
})
template: '',
link: function (scope, element, attrs) {
$(element).click(function () {
- var options = scope.$eval(attrs['crmConfirm']);
+ var options = scope.$eval(attrs.crmConfirm);
var defaults = (options.type) ? defaultFuncs[options.type](options) : {};
CRM.confirm(_.extend(defaults, options))
- .on('crmConfirm:yes', function () { scope.$apply(attrs['onYes']); })
- .on('crmConfirm:no', function () { scope.$apply(attrs['onNo']); });
+ .on('crmConfirm:yes', function () { scope.$apply(attrs.onYes); })
+ .on('crmConfirm:no', function () { scope.$apply(attrs.onNo); });
});
}
};