.directive('crmUiAccordion', function() {
return {
scope: {
- crmTitle: '@',
- crmCollapsed: '@'
+ crmUiAccordion: '='
},
- 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>',
+ template: '<div ng-class="cssClasses"><div class="crm-accordion-header">{{crmUiAccordion.title}} <a crm-ui-help="help" ng-if="help"></a></div><div class="crm-accordion-body" ng-transclude></div></div>',
transclude: true,
link: function (scope, element, attrs) {
scope.cssClasses = {
- collapsed: scope.$parent.$eval(attrs.crmCollapsed)
+ 'crm-accordion-wrapper': true,
+ collapsed: scope.crmUiAccordion.collapsed
};
+ scope.help = null;
+ scope.$watch('crmUiAccordion', function(crmUiAccordion) {
+ if (crmUiAccordion && crmUiAccordion.help) {
+ scope.help = crmUiAccordion.help.clone({}, {
+ title: crmUiAccordion.title
+ });
+ }
+ });
}
};
})
};
})
- // 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) {
+ // Simple wrapper around $.crmDatepicker.
+ // example with no time input: <input crm-ui-datepicker="{time: false}" ng-model="myobj.datefield"/>
+ // example with custom date format: <input crm-ui-datepicker="{dateFormat: 'm/d/y'}" ng-model="myobj.datefield"/>
+ .directive('crmUiDatepicker', function () {
return {
restrict: 'AE',
require: 'ngModel',
scope: {
- crmUiDateFormat: '@' // expression, date format (default: "yy-mm-dd")
+ crmUiDatepicker: '='
},
link: function (scope, element, attrs, ngModel) {
- var fmt = attrs.crmUiDateFormat ? $parse(attrs.crmUiDateFormat)() : "yy-mm-dd";
-
- element.addClass('dateplugin');
- $(element).datepicker({
- dateFormat: fmt
- });
-
- ngModel.$render = function $render() {
- $(element).datepicker('setDate', ngModel.$viewValue);
+ ngModel.$render = function () {
+ element.val(ngModel.$viewValue).change();
};
- var updateParent = (function() {
- $timeout(function () {
- ngModel.$setViewValue(element.val());
- });
- });
- element.on('change', updateParent);
+ element
+ .crmDatepicker(scope.crmUiDatepicker)
+ .on('change', function() {
+ var requiredLength = 19;
+ if (scope.crmUiDatepicker && scope.crmUiDatepicker.time === false) {
+ requiredLength = 10;
+ }
+ if (scope.crmUiDatepicker && scope.crmUiDatepicker.date === false) {
+ requiredLength = 8;
+ }
+ ngModel.$setValidity('incompleteDateTime', !($(this).val().length && $(this).val().length !== requiredLength));
+ });
}
};
})
- // Display a date-time widget.
- // example: <div crm-ui-date-time ng-model="myobj.mydatetimefield"></div>
- .directive('crmUiDateTime', function ($parse) {
+ // Display debug information (if available)
+ // For richer DX, checkout Batarang/ng-inspector (Chrome/Safari), or AngScope/ng-inspect (Firefox).
+ // example: <div crm-ui-debug="myobject" />
+ .directive('crmUiDebug', function ($location) {
return {
restrict: 'AE',
- require: 'ngModel',
scope: {
- ngRequired: '@'
+ crmUiDebug: '@'
},
- 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');
-
- 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: ''};
- }
- };
-
- function updateParent() {
- var incompleteDateTime = _.isEmpty(scope.dtparts.date) ^ _.isEmpty(scope.dtparts.time);
- ngModel.$setValidity('incompleteDateTime', !incompleteDateTime);
-
- 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);
+ template: function() {
+ var args = $location.search();
+ return (args && args.angularDebug) ? '<div crm-ui-accordion crm-title=\'ts("Debug (%1)", {1: crmUiDebug})\' crm-collapsed="true"><pre>{{data|json}}</pre></div>' : '';
+ },
+ link: function(scope, element, attrs) {
+ var args = $location.search();
+ if (args && args.angularDebug) {
+ scope.ts = CRM.ts(null);
+ scope.$parent.$watch(attrs.crmUiDebug, function(data) {
+ scope.data = data;
+ });
}
-
- scope.reset = function reset() {
- scope.dtparts = {date: '', time: ''};
- ngModel.$setViewValue('');
- };
}
};
})
// 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>
+ // example: <div crm-ui-field="{title: ts('My Field')}"> {{mydata}} </div>
+ // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field')}"> <input crm-ui-id="subform.myfield" name="myfield" /> </div>
+ // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field')}"> <input crm-ui-id="subform.myfield" name="myfield" required /> </div>
+ // example: <div crm-ui-field="{name: 'subform.myfield', title: ts('My Field'), help: hs('help_field_name')}"> {{mydata}} </div>
.directive('crmUiField', function() {
// Note: When writing new templates, the "label" position is particular. See/patch "var label" below.
var templateUrls = {
require: '^crmUiIdScope',
restrict: 'EA',
scope: {
- crmUiField: '@',
- crmTitle: '@'
+ // {title, name, help, helpFile}
+ crmUiField: '='
},
templateUrl: function(tElement, tAttrs){
var layout = tAttrs.crmLayout ? tAttrs.crmLayout : 'default';
transclude: true,
link: function (scope, element, attrs, crmUiIdCtrl) {
$(element).addClass('crm-section');
- scope.crmUiField = attrs.crmUiField;
- scope.crmTitle = attrs.crmTitle;
+ scope.help = null;
+ scope.$watch('crmUiField', function(crmUiField) {
+ if (crmUiField && crmUiField.help) {
+ scope.help = crmUiField.help.clone({}, {
+ title: crmUiField.title
+ });
+ }
+ });
}
};
})
};
})
+ // for example, see crmUiHelp
+ .service('crmUiHelp', function(){
+ // example: var h = new FieldHelp({id: 'foo'}); h.open();
+ function FieldHelp(options) {
+ this.options = options;
+ }
+ angular.extend(FieldHelp.prototype, {
+ get: function(n) {
+ return this.options[n];
+ },
+ open: function open() {
+ CRM.help(this.options.title, {id: this.options.id, file: this.options.file});
+ },
+ clone: function clone(options, defaults) {
+ return new FieldHelp(angular.extend({}, defaults, this.options, options));
+ }
+ });
+
+ // example: var hs = crmUiHelp({file: 'CRM/Foo/Bar'});
+ return function(defaults){
+ // example: hs('myfield')
+ // example: hs({id: 'myfield', title: 'Foo Bar', file: 'Whiz/Bang'})
+ return function(options) {
+ if (_.isString(options)) {
+ options = {id: options};
+ }
+ return new FieldHelp(angular.extend({}, defaults, options));
+ };
+ };
+ })
+
+ // Display a help icon
+ // Example: Use a default *.hlp file
+ // scope.hs = crmUiHelp({file: 'Path/To/Help/File'});
+ // HTML: <a crm-ui-help="hs({title:ts('My Field'), id:'my_field'})">
+ // Example: Use an explicit *.hlp file
+ // HTML: <a crm-ui-help="hs({title:ts('My Field'), id:'my_field', file:'CRM/Foo/Bar'})">
+ .directive('crmUiHelp', function() {
+ return {
+ restrict: 'EA',
+ link: function(scope, element, attrs) {
+ setTimeout(function() {
+ var crmUiHelp = scope.$eval(attrs.crmUiHelp);
+ var title = crmUiHelp && crmUiHelp.get('title') ? ts('%1 Help', {1: crmUiHelp.get('title')}) : ts('Help');
+ element.attr('title', title);
+ }, 50);
+
+ element
+ .addClass('helpicon')
+ .attr('href', '#')
+ .on('click', function(e) {
+ e.preventDefault();
+ scope.$eval(attrs.crmUiHelp).open();
+ });
+ }
+ };
+ })
+
// 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 {
doc.close();
};
+ // If the iframe is in a dialog, respond to resize events
+ $(elm).parent().on('dialogresize dialogopen', function(e, ui) {
+ $(this).css({padding: '0', margin: '0', overflow: 'hidden'});
+ iframe.setAttribute('height', '' + $(this).innerHeight() + 'px');
+ });
+
scope.$parent.$watch(attrs.crmUiIframe, refresh);
}
};
};
})
+ // CrmUiOrderCtrl is a controller class which manages sort orderings.
+ // Ex:
+ // JS: $scope.myOrder = new CrmUiOrderCtrl(['+field1', '-field2]);
+ // $scope.myOrder.toggle('field1');
+ // $scope.myOrder.setDir('field2', '');
+ // HTML: <tr ng-repeat="... | order:myOrder.get()">...</tr>
+ .service('CrmUiOrderCtrl', function(){
+ //
+ function CrmUiOrderCtrl(defaults){
+ this.values = defaults;
+ }
+ angular.extend(CrmUiOrderCtrl.prototype, {
+ get: function get() {
+ return this.values;
+ },
+ getDir: function getDir(name) {
+ if (this.values.indexOf(name) >= 0 || this.values.indexOf('+' + name) >= 0) {
+ return '+';
+ }
+ if (this.values.indexOf('-' + name) >= 0) {
+ return '-';
+ }
+ return '';
+ },
+ // @return bool TRUE if something is removed
+ remove: function remove(name) {
+ var idx = this.values.indexOf(name);
+ if (idx >= 0) {
+ this.values.splice(idx, 1);
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+ setDir: function setDir(name, dir) {
+ return this.toggle(name, dir);
+ },
+ // Toggle sort order on a field.
+ // To set a specific order, pass optional parameter 'next' ('+', '-', or '').
+ toggle: function toggle(name, next) {
+ if (!next && next !== '') {
+ next = '+';
+ if (this.remove(name) || this.remove('+' + name)) {
+ next = '-';
+ }
+ if (this.remove('-' + name)) {
+ next = '';
+ }
+ }
+
+ if (next == '+') {
+ this.values.unshift('+' + name);
+ }
+ else if (next == '-') {
+ this.values.unshift('-' + name);
+ }
+ }
+ });
+ return CrmUiOrderCtrl;
+ })
+
+ // Define a controller which manages sort order. You may interact with the controller
+ // directly ("myOrder.toggle('fieldname')") order using the helper, crm-ui-order-by.
+ // example:
+ // <span crm-ui-order="{var: 'myOrder', defaults: {'-myField'}}"></span>
+ // <th><a crm-ui-order-by="[myOrder,'myField']">My Field</a></th>
+ // <tr ng-repeat="... | order:myOrder.get()">...</tr>
+ // <button ng-click="myOrder.toggle('myField')">
+ .directive('crmUiOrder', function(CrmUiOrderCtrl) {
+ return {
+ link: function(scope, element, attrs){
+ var options = angular.extend({var: 'crmUiOrderBy'}, scope.$eval(attrs.crmUiOrder));
+ scope[options.var] = new CrmUiOrderCtrl(options.defaults);
+ }
+ };
+ })
+
+ // For usage, see crmUiOrder (above)
+ .directive('crmUiOrderBy', function() {
+ return {
+ link: function(scope, element, attrs) {
+ function updateClass(crmUiOrderCtrl, name) {
+ var dir = crmUiOrderCtrl.getDir(name);
+ element
+ .toggleClass('sorting_asc', dir === '+')
+ .toggleClass('sorting_desc', dir === '-')
+ .toggleClass('sorting', dir === '');
+ }
+
+ element.on('click', function(e){
+ var tgt = scope.$eval(attrs.crmUiOrderBy);
+ tgt[0].toggle(tgt[1]);
+ updateClass(tgt[0], tgt[1]);
+ e.preventDefault();
+ scope.$digest();
+ });
+
+ var tgt = scope.$eval(attrs.crmUiOrderBy);
+ updateClass(tgt[0], tgt[1]);
+ }
+ };
+ })
+
// 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 {
require: '?ngModel',
scope: {
- crmUiSelect: '@'
+ crmUiSelect: '='
},
link: function (scope, element, attrs, ngModel) {
// In cases where UI initiates update, there may be an extra
function init() {
// TODO watch select2-options
- var options = attrs.crmUiSelect ? scope.$parent.$eval(attrs.crmUiSelect) : {};
- element.select2(options);
+ element.select2(scope.crmUiSelect || {});
+ element.on('change', refreshModel);
+ $timeout(ngModel.$render);
+ }
+
+ init();
+ }
+ };
+ })
+
+ // Render a crmEntityRef widget
+ // usage: <input crm-entityref="{entity: 'Contact', select: {allowClear:true}}" ng-model="myobj.field" />
+ .directive('crmEntityref', function ($parse, $timeout) {
+ return {
+ require: '?ngModel',
+ scope: {
+ crmEntityref: '='
+ },
+ 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 options
+ // TODO can we infer "entity" from model?
+ element.crmEntityRef(scope.crmEntityref || {});
element.on('change', refreshModel);
$timeout(ngModel.$render);
}
};
})
- // 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: {
- },
- link: function (scope, element, attrs, ngModel) {
- element.addClass('crm-form-text six');
- element.timeEntry({show24Hours: true});
-
- ngModel.$render = function $render() {
- element.timeEntry('setTime', ngModel.$viewValue);
- };
-
- var updateParent = (function () {
- $timeout(function () {
- ngModel.$setViewValue(element.val());
- });
- });
- 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" />
this.$last = function() { return this.$index() === steps.length -1; };
this.$maxVisit = function() { return maxVisited; };
this.$validStep = function() {
- return steps[selectedIndex].isStepValid();
+ return steps[selectedIndex] && steps[selectedIndex].isStepValid();
};
this.iconFor = function(index) {
if (index < this.$index()) return '√';
$parse($scope.crmUiWizard).assign($scope.$parent, this);
}
},
- link: function (scope, element, attrs) {}
+ link: function (scope, element, attrs) {
+ scope.ts = CRM.ts(null);
+ }
};
})
};
})
+ // Example: <button crm-icon="check">Save</button>
+ .directive('crmIcon', function() {
+ return {
+ restrict: 'EA',
+ scope: {},
+ link: function (scope, element, attrs) {
+ $(element).prepend('<span class="icon ui-icon-' + attrs.crmIcon + '"></span> ');
+ if ($(element).is('button')) {
+ $(element).addClass('crm-button');
+ }
+ }
+ };
+ })
+
// 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>
// Example: <button crm-confirm="{message: ts('Are you sure you want to continue?')}" on-yes="frobnicate(123)">Frobincate</button>
// Example: <button crm-confirm="{type: 'disable', obj: myObject}" on-yes="myObject.is_active=0; myObject.save()">Disable</button>
- .directive('crmConfirm', function () {
+ // Example: <button crm-confirm="{templateUrl: '~/path/to/view.html', export: {foo: bar}}" on-yes="frobnicate(123)">Frobincate</button>
+ .directive('crmConfirm', function ($compile, $rootScope, $templateRequest, $q) {
// Helpers to calculate default options for CRM.confirm()
var defaultFuncs = {
'disable': function (options) {
};
}
};
+ var confirmCount = 0;
return {
- template: '',
link: function (scope, element, attrs) {
$(element).click(function () {
var options = scope.$eval(attrs.crmConfirm);
+ if (attrs.title && !options.title) {
+ options.title = attrs.title;
+ }
var defaults = (options.type) ? defaultFuncs[options.type](options) : {};
+
+ var tpl = null, stubId = null;
+ if (!options.message) {
+ if (options.templateUrl) {
+ tpl = $templateRequest(options.templateUrl);
+ }
+ else if (options.template) {
+ tpl = options.template;
+ }
+ if (tpl) {
+ stubId = 'crmUiConfirm_' + (++confirmCount);
+ options.message = '<div id="' + stubId + '"></div>';
+ }
+ }
+
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); });
+
+ if (tpl && stubId) {
+ $q.when(tpl, function(html) {
+ var scope = options.scope || $rootScope.$new();
+ if (options.export) {
+ angular.extend(scope, options.export);
+ }
+ var linker = $compile(html);
+ $('#' + stubId).append($(linker(scope)));
+ });
+ }
});
}
};