diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js |
first commit
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js')
-rw-r--r-- | www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js | 1092 |
1 files changed, 1092 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js new file mode 100644 index 00000000..0b09c60d --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmUi.js @@ -0,0 +1,1092 @@ +/// 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(); + } + }; + }) + + // validate multiple email text + // usage: <input crm-multiple-email type="text" ng-model="myobj.field" /> + .directive('crmMultipleEmail', function ($parse, $timeout) { + return { + require: 'ngModel', + link: function(scope, element, attrs, ctrl) { + ctrl.$parsers.unshift(function(viewValue) { + // if empty value provided simply bypass validation + if (_.isEmpty(viewValue)) { + ctrl.$setValidity('crmMultipleEmail', true); + return viewValue; + } + + // split email string on basis of comma + var emails = viewValue.split(','); + // regex pattern for single email + var emailRegex = /\S+@\S+\.\S+/; + + var validityArr = emails.map(function(str){ + return emailRegex.test(str.trim()); + }); + + if ($.inArray(false, validityArr) > -1) { + ctrl.$setValidity('crmMultipleEmail', false); + } else { + ctrl.$setValidity('crmMultipleEmail', true); + } + return viewValue; + }); + } + }; + }) + // 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); + + element.find('.crm-wizard-buttons button[ng-click^=crmUiWizardCtrl]').click(function () { + // These values are captured inside the click handler to ensure the + // positions/sizes of the elements are captured at the time of the + // click vs. at the time this directive is initialized. + var topOfWizard = element.offset().top; + var heightOfMenu = $('#civicrm-menu').height() || 0; + + $('html') + // stop any other animations that might be happening... + .stop() + // gracefully slide the user to the top of the wizard + .animate({scrollTop: topOfWizard - heightOfMenu}, 1000); + }); + } + }; + }) + + // 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! + // WARNING: This directive works only if your AngularJS base page does not + // set a custom title (i.e., it has an initial title of "CiviCRM"). See the + // global variables pageTitle and documentTitle. + // 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._); |