var uidCount = 0;
- var partialUrl = function (relPath) {
- return CRM.resourceUrls['civicrm'] + '/partials/crmUi/' + relPath;
- };
-
angular.module('crmUi', [])
// example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
};
})
- // example: <input crm-ui-date="myobj.datefield" />
- // example: <input crm-ui-date="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
+ // Display a date widget.
+ // example: <input crm-ui-date ng-model="myobj.datefield" />
+ // example: <input crm-ui-date ng-model="myobj.datefield" crm-ui-date-format="yy-mm-dd" />
.directive('crmUiDate', function ($parse, $timeout) {
return {
restrict: 'AE',
+ require: 'ngModel',
scope: {
- crmUiDate: '@', // expression, model binding
crmUiDateFormat: '@' // expression, date format (default: "yy-mm-dd")
},
- link: function (scope, element, attrs) {
+ link: function (scope, element, attrs, ngModel) {
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);
- });
+ ngModel.$render = function $render() {
+ $(element).datepicker('setDate', ngModel.$viewValue);
+ };
var updateParent = (function() {
$timeout(function () {
- model.assign(scope.$parent, $(element).val());
+ ngModel.$setViewValue(element.val());
});
});
- updateChildren();
- scope.$parent.$watch(attrs.crmUiDate, updateChildren);
element.on('change', updateParent);
}
};
})
- // example: <div crm-ui-date-time="myobj.mydatetimefield"></div>
+ // Display a date-time widget.
+ // example: <div crm-ui-date-time ng-model="myobj.mydatetimefield"></div>
.directive('crmUiDateTime', function ($parse) {
return {
restrict: 'AE',
+ require: 'ngModel',
scope: {
- crmUiDateTime: '@'
+ ngRequired: '@'
},
- 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);
+ templateUrl: '~/crmUi/datetime.html',
+ link: function (scope, element, attrs, ngModel) {
+ var ts = scope.ts = CRM.ts(null);
scope.dateLabel = ts('Date');
scope.timeLabel = ts('Time');
+ element.addClass('crm-ui-datetime');
- var updateChildren = (function () {
- var value = model(scope.$parent);
- if (value) {
- var dtparts = value.split(/ /);
+ ngModel.$render = function $render() {
+ if (!_.isEmpty(ngModel.$viewValue)) {
+ var dtparts = ngModel.$viewValue.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);
- });
+ };
+
+ function updateParent() {
+ var incompleteDateTime = _.isEmpty(scope.dtparts.date) ^ _.isEmpty(scope.dtparts.time);
+ ngModel.$setValidity('incompleteDateTime', !incompleteDateTime);
- updateChildren();
- scope.$parent.$watch(attrs.crmUiDateTime, updateChildren);
- scope.$watch('dtparts.date', updateParent),
- scope.$watch('dtparts.time', updateParent)
+ if (_.isEmpty(scope.dtparts.date) && _.isEmpty(scope.dtparts.time)) {
+ ngModel.$setViewValue(' ');
+ }
+ else {
+ //ngModel.$setViewValue(scope.dtparts.date + ' ' + scope.dtparts.time);
+ ngModel.$setViewValue((scope.dtparts.date ? scope.dtparts.date : '') + ' ' + (scope.dtparts.time ? scope.dtparts.time : ''));
+ }
+ }
+
+ scope.$watch('dtparts.date', updateParent);
+ scope.$watch('dtparts.time', updateParent);
+
+ function updateRequired() {
+ scope.required = scope.$parent.$eval(attrs.ngRequired);
+ }
+
+ if (attrs.ngRequired) {
+ updateRequired();
+ scope.$parent.$watch(attrs.ngRequired, updateRequired);
+ }
+
+ scope.reset = function reset() {
+ scope.dtparts = {date: '', time: ''};
+ ngModel.$setViewValue('');
+ };
}
};
})
.directive('crmUiField', function() {
// Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
var templateUrls = {
- default: partialUrl('field.html'),
- checkbox: partialUrl('field-cb.html')
+ default: '~/crmUi/field.html',
+ checkbox: '~/crmUi/field-cb.html'
};
return {
// immediately for initialization. Use retries/retryDelay to initialize such elements.
var init = function (retries, retryDelay) {
var input = $('#' + id);
- if (input.length == 0) {
+ if (input.length === 0) {
if (retries) {
$timeout(function(){
init(retries-1, retryDelay);
});
}
else {
- scope.crmIsRequired = input.prop('required')
+ scope.crmIsRequired = input.prop('required');
}
ngModel = $parse(attrs.crmUiFor)(tgtScope);
};
})
+ // Define a scope in which a name like "subform.foo" maps to a unique 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('crmUiIdScope', function () {
return {
ids[name] = "crmUiId_" + (++uidCount);
}
return ids[name];
- }
+ };
},
link: function (scope, element, attrs) {}
};
})
+ // Display an HTML blurb inside an IFRAME.
// 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);
}
};
})
+ // Define a rich text editor.
// example: <textarea crm-ui-id="myForm.body_html" crm-ui-richtext name="body_html" ng-model="mailing.body_html"></textarea>
+ // WISHLIST: use ngModel
.directive('crmUiRichtext', function ($timeout) {
return {
require: '?ngModel',
};
})
+ // Display a lock icon (based on a boolean).
// example: <a crm-ui-lock binding="mymodel.boolfield"></a>
// example: <a crm-ui-lock
// binding="mymodel.boolfield"
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 () {
};
})
+ // Display a fancy SELECT (based on select2).
// usage: <select crm-ui-select="{placeholder:'Something',allowClear:true,...}" ng-model="myobj.field"><option...></select>
.directive('crmUiSelect', function ($parse, $timeout) {
return {
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" />
+ // Display a time-entry field.
+ // example: <input crm-ui-time ng-model="myobj.mytimefield" />
.directive('crmUiTime', function ($parse, $timeout) {
return {
restrict: 'AE',
+ require: 'ngModel',
scope: {
- crmUiTime: '@'
},
- link: function (scope, element, attrs) {
- var model = $parse(attrs.crmUiTime);
-
+ link: function (scope, element, attrs, ngModel) {
element.addClass('crm-form-text six');
- $(element).timeEntry({show24Hours: true});
+ element.timeEntry({show24Hours: true});
+
+ ngModel.$render = function $render() {
+ element.timeEntry('setTime', ngModel.$viewValue);
+ };
- 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());
+ ngModel.$setViewValue(element.val());
});
});
-
- updateChildren();
- scope.$parent.$watch(attrs.crmUiTime, updateChildren);
element.on('change', updateParent);
}
- }
+ };
+ })
+
+ // Generic, field-independent form validator.
+ // 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" />
+ .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
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;
angular.forEach(steps, function(otherStep, otherKey) {
if (otherStep === step) key = otherKey;
});
- if (key != null) {
+ if (key !== null) {
steps.splice(key, 1);
}
};
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: '@', // expression, evaluates to a printable string
},
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); });
});
}
};