1044 lines
38 KiB
JavaScript
1044 lines
38 KiB
JavaScript
/// crmUi: Sundry UI helpers
|
|
(function (angular, $, _) {
|
|
|
|
var uidCount = 0,
|
|
pageTitle = 'CiviCRM',
|
|
documentTitle = 'CiviCRM';
|
|
|
|
angular.module('crmUi', CRM.angRequires('crmUi'))
|
|
|
|
// example <div crm-ui-accordion crm-title="ts('My Title')" crm-collapsed="true">...content...</div>
|
|
// WISHLIST: crmCollapsed should support two-way/continuous binding
|
|
.directive('crmUiAccordion', function() {
|
|
return {
|
|
scope: {
|
|
crmUiAccordion: '='
|
|
},
|
|
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 = {
|
|
'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
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
// Examples:
|
|
// crmUiAlert({text: 'My text', title: 'My title', type: 'error'});
|
|
// crmUiAlert({template: '<a ng-click="ok()">Hello</a>', scope: $scope.$new()});
|
|
// var h = crmUiAlert({templateUrl: '~/crmFoo/alert.html', scope: $scope.$new()});
|
|
// ... h.close(); ...
|
|
.service('crmUiAlert', function($compile, $rootScope, $templateRequest, $q) {
|
|
var count = 0;
|
|
return function crmUiAlert(params) {
|
|
var id = 'crmUiAlert_' + (++count);
|
|
var tpl = null;
|
|
if (params.templateUrl) {
|
|
tpl = $templateRequest(params.templateUrl);
|
|
}
|
|
else if (params.template) {
|
|
tpl = params.template;
|
|
}
|
|
if (tpl) {
|
|
params.text = '<div id="' + id + '"></div>'; // temporary stub
|
|
}
|
|
var result = CRM.alert(params.text, params.title, params.type, params.options);
|
|
if (tpl) {
|
|
$q.when(tpl, function(html) {
|
|
var scope = params.scope || $rootScope.$new();
|
|
var linker = $compile(html);
|
|
$('#' + id).append($(linker(scope)));
|
|
});
|
|
}
|
|
return result;
|
|
};
|
|
})
|
|
|
|
// 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="{date: 'm/d/y'}" ng-model="myobj.datefield"/>
|
|
.directive('crmUiDatepicker', function () {
|
|
return {
|
|
restrict: 'AE',
|
|
require: 'ngModel',
|
|
scope: {
|
|
crmUiDatepicker: '='
|
|
},
|
|
link: function (scope, element, attrs, ngModel) {
|
|
ngModel.$render = function () {
|
|
element.val(ngModel.$viewValue).change();
|
|
};
|
|
|
|
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 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',
|
|
scope: {
|
|
crmUiDebug: '@'
|
|
},
|
|
template: function() {
|
|
var args = $location.search();
|
|
return (args && args.angularDebug) ? '<div crm-ui-accordion=\'{title: ts("Debug (%1)", {1: crmUiDebug}), 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;
|
|
});
|
|
}
|
|
}
|
|
};
|
|
})
|
|
|
|
// Display a field/row in a field list
|
|
// 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'), required: true}"> {{mydata}} </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: {
|
|
// {title, name, help, helpFile}
|
|
crmUiField: '='
|
|
},
|
|
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.help = null;
|
|
scope.$watch('crmUiField', function(crmUiField) {
|
|
if (crmUiField && crmUiField.help) {
|
|
scope.help = crmUiField.help.clone({}, {
|
|
title: crmUiField.title
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
};
|
|
})
|
|
|
|
// 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 {
|
|
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 && !attrs.crmUiForceRequired) {
|
|
if (retries) {
|
|
$timeout(function(){
|
|
init(retries-1, retryDelay);
|
|
}, retryDelay);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (attrs.crmUiForceRequired) {
|
|
scope.crmIsRequired = true;
|
|
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);
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
// 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 {
|
|
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) {}
|
|
};
|
|
})
|
|
|
|
// Display an HTML blurb inside an IFRAME.
|
|
// example: <iframe crm-ui-iframe="getHtmlContent()"></iframe>
|
|
// example: <iframe crm-ui-iframe crm-ui-iframe-src="getUrl()"></iframe>
|
|
.directive('crmUiIframe', function ($parse) {
|
|
return {
|
|
scope: {
|
|
crmUiIframeSrc: '@', // expression which evaluates to a URL
|
|
crmUiIframe: '@' // expression which evaluates to HTML content
|
|
},
|
|
link: function (scope, elm, attrs) {
|
|
var iframe = $(elm)[0];
|
|
iframe.setAttribute('width', '100%');
|
|
iframe.setAttribute('height', '250px');
|
|
iframe.setAttribute('frameborder', '0');
|
|
|
|
var refresh = function () {
|
|
if (attrs.crmUiIframeSrc) {
|
|
iframe.setAttribute('src', scope.$parent.$eval(attrs.crmUiIframeSrc));
|
|
}
|
|
else {
|
|
var iframeHtml = scope.$parent.$eval(attrs.crmUiIframe);
|
|
|
|
var doc = iframe.document;
|
|
if (iframe.contentDocument) {
|
|
doc = iframe.contentDocument;
|
|
}
|
|
else if (iframe.contentWindow) {
|
|
doc = iframe.contentWindow.document;
|
|
}
|
|
|
|
doc.open();
|
|
doc.writeln(iframeHtml);
|
|
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');
|
|
});
|
|
|
|
$(elm).parent().on('dialogresize', function(e, ui) {
|
|
iframe.setAttribute('class', 'resized');
|
|
});
|
|
|
|
scope.$parent.$watch(attrs.crmUiIframe, refresh);
|
|
}
|
|
};
|
|
})
|
|
|
|
// Example:
|
|
// <a ng-click="$broadcast('my-insert-target', 'some new text')>Insert</a>
|
|
// <textarea crm-ui-insert-rx='my-insert-target'></textarea>
|
|
.directive('crmUiInsertRx', function() {
|
|
return {
|
|
link: function(scope, element, attrs) {
|
|
scope.$on(attrs.crmUiInsertRx, function(e, tokenName) {
|
|
CRM.wysiwyg.insert(element, tokenName);
|
|
$(element).select2('close').select2('val', '');
|
|
CRM.wysiwyg.focus(element);
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
// 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>
|
|
.directive('crmUiRichtext', function ($timeout) {
|
|
return {
|
|
require: '?ngModel',
|
|
link: function (scope, elm, attr, ngModel) {
|
|
|
|
var editor = CRM.wysiwyg.create(elm);
|
|
if (!ngModel) {
|
|
return;
|
|
}
|
|
|
|
if (attr.ngBlur) {
|
|
$(elm).on('blur', function() {
|
|
$timeout(function() {
|
|
scope.$eval(attr.ngBlur);
|
|
});
|
|
});
|
|
}
|
|
|
|
ngModel.$render = function(value) {
|
|
editor.done(function() {
|
|
CRM.wysiwyg.setVal(elm, ngModel.$viewValue || '');
|
|
});
|
|
};
|
|
}
|
|
};
|
|
})
|
|
|
|
// 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"
|
|
// title-locked="ts('Boolfield is locked')"
|
|
// title-unlocked="ts('Boolfield is unlocked')"></a>
|
|
.directive('crmUiLock', function ($parse, $rootScope) {
|
|
var defaultVal = function (defaultValue) {
|
|
var f = function (scope) {
|
|
return defaultValue;
|
|
};
|
|
f.assign = function (scope, value) {
|
|
// ignore changes
|
|
};
|
|
return f;
|
|
};
|
|
|
|
// like $parse, but accepts a defaultValue in case expr is undefined
|
|
var parse = function (expr, defaultValue) {
|
|
return expr ? $parse(expr) : defaultVal(defaultValue);
|
|
};
|
|
|
|
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'));
|
|
|
|
$(element).addClass('crm-i lock-button');
|
|
var refresh = function () {
|
|
var locked = binding(scope);
|
|
if (locked) {
|
|
$(element)
|
|
.removeClass('fa-unlock')
|
|
.addClass('fa-lock')
|
|
.prop('title', titleLocked(scope))
|
|
;
|
|
}
|
|
else {
|
|
$(element)
|
|
.removeClass('fa-lock')
|
|
.addClass('fa-unlock')
|
|
.prop('title', titleUnlocked(scope))
|
|
;
|
|
}
|
|
};
|
|
|
|
$(element).click(function () {
|
|
binding.assign(scope, !binding(scope));
|
|
//scope.$digest();
|
|
$rootScope.$digest();
|
|
});
|
|
|
|
scope.$watch(attrs.binding, refresh);
|
|
scope.$watch(attrs.titleLocked, refresh);
|
|
scope.$watch(attrs.titleUnlocked, refresh);
|
|
|
|
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',
|
|
priority: 1,
|
|
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.
|
|
|
|
if (ngModel) {
|
|
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
|
|
var newVal = _.cloneDeep(ngModel.$modelValue);
|
|
// Fix possible data-type mismatch
|
|
if (typeof newVal === 'string' && element.select2('container').hasClass('select2-container-multi')) {
|
|
newVal = newVal.length ? newVal.split(',') : [];
|
|
}
|
|
element.select2('val', newVal);
|
|
});
|
|
};
|
|
}
|
|
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
|
|
element.crmSelect2(scope.crmUiSelect || {});
|
|
if (ngModel) {
|
|
element.on('change', refreshModel);
|
|
}
|
|
}
|
|
|
|
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
|
|
var newVal = _.cloneDeep(ngModel.$modelValue);
|
|
// Fix possible data-type mismatch
|
|
if (typeof newVal === 'string' && element.select2('container').hasClass('select2-container-multi')) {
|
|
newVal = newVal.length ? newVal.split(',') : [];
|
|
}
|
|
element.select2('val', newVal);
|
|
});
|
|
};
|
|
function refreshModel() {
|
|
var oldValue = ngModel.$viewValue, newValue = element.select2('val');
|
|
if (oldValue != newValue) {
|
|
scope.$parent.$apply(function () {
|
|
ngModel.$setViewValue(newValue);
|
|
});
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
// TODO can we infer "entity" from model?
|
|
element.crmEntityRef(scope.crmEntityref || {});
|
|
element.on('change', refreshModel);
|
|
$timeout(ngModel.$render);
|
|
}
|
|
|
|
init();
|
|
}
|
|
};
|
|
})
|
|
|
|
// example <div crm-ui-tab id="tab-1" crm-title="ts('My Title')" count="3">...content...</div>
|
|
// WISHLIST: use a full Angular component instead of an incomplete jQuery wrapper
|
|
.directive('crmUiTab', function($parse) {
|
|
return {
|
|
require: '^crmUiTabSet',
|
|
restrict: 'EA',
|
|
scope: {
|
|
crmTitle: '@',
|
|
crmIcon: '@',
|
|
count: '@',
|
|
id: '@'
|
|
},
|
|
template: '<div ng-transclude></div>',
|
|
transclude: true,
|
|
link: function (scope, element, attrs, crmUiTabSetCtrl) {
|
|
crmUiTabSetCtrl.add(scope);
|
|
}
|
|
};
|
|
})
|
|
|
|
// 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>
|
|
.directive('crmUiTabSet', function() {
|
|
return {
|
|
restrict: 'EA',
|
|
scope: {
|
|
crmUiTabSet: '@',
|
|
tabSetOptions: '@'
|
|
},
|
|
templateUrl: '~/crmUi/tabset.html',
|
|
transclude: true,
|
|
controllerAs: 'crmUiTabSetCtrl',
|
|
controller: function($scope, $parse) {
|
|
var tabs = $scope.tabs = []; // array<$scope>
|
|
this.add = function(tab) {
|
|
if (!tab.id) throw "Tab is missing 'id'";
|
|
tabs.push(tab);
|
|
};
|
|
},
|
|
link: function (scope, element, attrs) {}
|
|
};
|
|
})
|
|
|
|
// 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
|
|
// 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>
|
|
// example with custom nav classes: <div crm-ui-wizard crm-ui-wizard-nav-class="ng-animate-out ...">...</div>
|
|
// Note: "myWizardCtrl" has various actions/properties like next() and $first().
|
|
// WISHLIST: Allow each step to determine if it is "complete" / "valid" / "selectable"
|
|
// WISHLIST: Allow each step to enable/disable (show/hide) itself
|
|
.directive('crmUiWizard', function() {
|
|
return {
|
|
restrict: 'EA',
|
|
scope: {
|
|
crmUiWizard: '@',
|
|
crmUiWizardNavClass: '@' // string, A list of classes that will be added to the nav items
|
|
},
|
|
templateUrl: '~/crmUi/wizard.html',
|
|
transclude: true,
|
|
controllerAs: 'crmUiWizardCtrl',
|
|
controller: function($scope, $parse) {
|
|
var steps = $scope.steps = []; // array<$scope>
|
|
var crmUiWizardCtrl = this;
|
|
var maxVisited = 0;
|
|
var selectedIndex = null;
|
|
|
|
var findIndex = function() {
|
|
var found = null;
|
|
angular.forEach(steps, function(step, stepKey) {
|
|
if (step.selected) found = stepKey;
|
|
});
|
|
return found;
|
|
};
|
|
|
|
/// @return int the index of the current step
|
|
this.$index = function() { return selectedIndex; };
|
|
/// @return bool whether the currentstep is first
|
|
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.$validStep = function() {
|
|
return steps[selectedIndex] && 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;
|
|
return this.$validStep();
|
|
};
|
|
|
|
/*** @param Object step the $scope of the step */
|
|
this.select = function(step) {
|
|
angular.forEach(steps, function(otherStep, otherKey) {
|
|
otherStep.selected = (otherStep === step);
|
|
if (otherStep === step && maxVisited < otherKey) maxVisited = otherKey;
|
|
});
|
|
selectedIndex = findIndex();
|
|
};
|
|
/*** @param Object step the $scope of the step */
|
|
this.add = function(step) {
|
|
if (steps.length === 0) {
|
|
step.selected = true;
|
|
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;
|
|
if (index >= steps.length) index = steps.length-1;
|
|
this.select(steps[index]);
|
|
};
|
|
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);
|
|
}
|
|
},
|
|
link: function (scope, element, attrs) {
|
|
scope.ts = CRM.ts(null);
|
|
}
|
|
};
|
|
})
|
|
|
|
// Use this to add extra markup to wizard
|
|
.directive('crmUiWizardButtons', function() {
|
|
return {
|
|
require: '^crmUiWizard',
|
|
restrict: 'EA',
|
|
scope: {},
|
|
template: '<span ng-transclude></span>',
|
|
transclude: true,
|
|
link: function (scope, element, attrs, crmUiWizardCtrl) {
|
|
var realButtonsEl = $(element).closest('.crm-wizard').find('.crm-wizard-buttons');
|
|
$(element).appendTo(realButtonsEl);
|
|
}
|
|
};
|
|
})
|
|
|
|
// Example for Font Awesome: <button crm-icon="fa-check">Save</button>
|
|
// Example for jQuery UI (deprecated): <button crm-icon="check">Save</button>
|
|
.directive('crmIcon', function() {
|
|
return {
|
|
restrict: 'EA',
|
|
link: function (scope, element, attrs) {
|
|
if (element.is('[crm-ui-tab]')) {
|
|
// handled in crmUiTab ctrl
|
|
return;
|
|
}
|
|
if (attrs.crmIcon.substring(0,3) == 'fa-') {
|
|
$(element).prepend('<i class="crm-i ' + attrs.crmIcon + '"></i> ');
|
|
}
|
|
else {
|
|
$(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 with custom classes: <div crm-ui-wizard-step="100" crm-ui-wizard-step-class="ng-animate-out ...">...content...</div>
|
|
.directive('crmUiWizardStep', function() {
|
|
var nextWeight = 1;
|
|
return {
|
|
require: ['^crmUiWizard', 'form'],
|
|
restrict: 'EA',
|
|
scope: {
|
|
crmTitle: '@', // expression, evaluates to a printable string
|
|
crmUiWizardStep: '@', // int, a weight which determines the ordering of the steps
|
|
crmUiWizardStepClass: '@' // string, A list of classes that will be added to the template
|
|
},
|
|
template: '<div class="crm-wizard-step {{crmUiWizardStepClass}}" ng-show="selected" ng-transclude/></div>',
|
|
transclude: true,
|
|
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);
|
|
scope.$on('$destroy', function(){
|
|
crmUiWizardCtrl.remove(scope);
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
// 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>
|
|
// 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) {
|
|
return {
|
|
message: ts('Are you sure you want to disable this?'),
|
|
options: {no: ts('Cancel'), yes: ts('Disable')},
|
|
width: 300,
|
|
title: ts('Disable %1?', {
|
|
1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
|
|
})
|
|
};
|
|
},
|
|
'revert': function (options) {
|
|
return {
|
|
message: ts('Are you sure you want to revert this?'),
|
|
options: {no: ts('Cancel'), yes: ts('Revert')},
|
|
width: 300,
|
|
title: ts('Revert %1?', {
|
|
1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
|
|
})
|
|
};
|
|
},
|
|
'delete': function (options) {
|
|
return {
|
|
message: ts('Are you sure you want to delete this?'),
|
|
options: {no: ts('Cancel'), yes: ts('Delete')},
|
|
width: 300,
|
|
title: ts('Delete %1?', {
|
|
1: options.obj.title || options.obj.label || options.obj.name || ts('the record')
|
|
})
|
|
};
|
|
}
|
|
};
|
|
var confirmCount = 0;
|
|
return {
|
|
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); });
|
|
|
|
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)));
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
// Sets document title & page title; attempts to override CMS title markup for the latter
|
|
// WARNING: Use only once per route!
|
|
// Example (same title for both): <h1 crm-page-title>{{ts('Hello')}}</h1>
|
|
// Example (separate document title): <h1 crm-document-title="ts('Hello')" crm-page-title><i class="crm-i fa-flag"></i>{{ts('Hello')}}</h1>
|
|
.directive('crmPageTitle', function($timeout) {
|
|
return {
|
|
scope: {
|
|
crmDocumentTitle: '='
|
|
},
|
|
link: function(scope, $el, attrs) {
|
|
function update() {
|
|
$timeout(function() {
|
|
var newPageTitle = _.trim($el.html()),
|
|
newDocumentTitle = scope.crmDocumentTitle || $el.text();
|
|
document.title = $('title').text().replace(documentTitle, newDocumentTitle);
|
|
// If the CMS has already added title markup to the page, use it
|
|
$('h1').not('.crm-container h1').each(function() {
|
|
if (_.trim($(this).html()) === pageTitle) {
|
|
$(this).addClass('crm-page-title').html(newPageTitle);
|
|
$el.hide();
|
|
}
|
|
});
|
|
pageTitle = newPageTitle;
|
|
documentTitle = newDocumentTitle;
|
|
});
|
|
}
|
|
|
|
scope.$watch(function() {return scope.crmDocumentTitle + $el.html();}, update);
|
|
}
|
|
};
|
|
})
|
|
|
|
.run(function($rootScope, $location) {
|
|
/// Example: <button ng-click="goto('home')">Go home!</button>
|
|
$rootScope.goto = function(path) {
|
|
$location.path(path);
|
|
};
|
|
// useful for debugging: $rootScope.log = console.log || function() {};
|
|
})
|
|
;
|
|
|
|
})(angular, CRM.$, CRM._);
|