X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=js%2Fangular-crm-ui.js;h=44b19bdb280e104578148adaf1efc952ad5a764e;hb=f3337aaeab808a06348db58aa6c09d492efb1ff8;hp=76ec351ec583e913bde386cd20c311714f902fe8;hpb=3074caa9d9fe45832ac0997b6c46c3e698a6415e;p=civicrm-core.git diff --git a/js/angular-crm-ui.js b/js/angular-crm-ui.js index 76ec351ec5..44b19bdb28 100644 --- a/js/angular-crm-ui.js +++ b/js/angular-crm-ui.js @@ -1,11 +1,7 @@ /// crmUi: Sundry UI helpers (function (angular, $, _) { - var idCount = 0; - - var partialUrl = function (relPath) { - return CRM.resourceUrls['civicrm'] + '/partials/crmUi/' + relPath; - }; + var uidCount = 0; angular.module('crmUi', []) @@ -92,91 +88,140 @@ updateChildren(); scope.$parent.$watch(attrs.crmUiDateTime, updateChildren); - scope.$watch('dtparts.date', updateParent), - scope.$watch('dtparts.time', updateParent) + scope.$watch('dtparts.date', updateParent); + scope.$watch('dtparts.time', updateParent); } }; }) // Display a field/row in a field list // example:
{{mydata}}
- // example:
- // example:
+ // example:
+ // example:
.directive('crmUiField', function() { - function createReqStyle(req) { - return {visibility: req ? 'inherit' : 'hidden'}; - } // 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 { + require: '^crmUiIdScope', + restrict: 'EA', scope: { - crmUiField: '@', // string, name of an HTML form element - crmLayout: '@', // string, "default" or "checkbox" - crmTitle: '@' // expression, printable title for the field + crmUiField: '@', + crmTitle: '@' }, templateUrl: function(tElement, tAttrs){ var layout = tAttrs.crmLayout ? tAttrs.crmLayout : 'default'; return templateUrls[layout]; }, transclude: true, - link: function (scope, element, attrs) { + link: function (scope, element, attrs, crmUiIdCtrl) { $(element).addClass('crm-section'); - scope.crmTitle = attrs.crmTitle; scope.crmUiField = attrs.crmUiField; - scope.cssClasses = {}; - scope.crmRequiredStyle = createReqStyle(false); - - // 0. Ensure that a target field has been specified + scope.crmTitle = attrs.crmTitle; + } + }; + }) - if (!attrs.crmUiField) return; - if (attrs.crmUiField == 'name') { - throw new Error('Validation monitoring does not work for field name "name"'); + // example:
+ .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); } + } + }; + }) - // 1. Figure out form and input elements + // example:
+ .directive('crmUiFor', function ($parse, $timeout) { + return { + require: '^crmUiIdScope', + restrict: 'EA', + template: '*', + transclude: true, + link: function (scope, element, attrs, crmUiIdCtrl) { + scope.crmIsRequired = false; + scope.cssClasses = {}; - var form = $(element).closest('form'); - var formCtrl = scope.$parent.$eval(form.attr('name')); - var input = $('input[name="' + attrs.crmUiField + '"],select[name="' + attrs.crmUiField + '"],textarea[name="' + attrs.crmUiField + '"]', form); - var label = $('>div.label >label, >label', element); - if (form.length != 1 || input.length != 1 || label.length != 1) { - if (console.log) console.log('Label cannot be matched to input element. Expected to find one form and one input[name='+attrs.crmUiField+'].', form.length, input.length, label.length); - return; - } + if (!attrs.crmUiFor) return; - // 2. Make sure that inputs are well-defined (with name+id). + var id = crmUiIdCtrl.get(attrs.crmUiFor); + element.attr('for', id); + var ngModel = null; - if (!input.attr('id')) { - input.attr('id', 'crmUi_' + (++idCount)); - } - $(label).attr('for', input.attr('id')); + var updateCss = function () { + scope.cssClasses['crm-error'] = !ngModel.$valid && !ngModel.$pristine; + }; - // 3. Monitor is the "required" and "$valid" properties + // Note: if target element is dynamically generated (eg via ngInclude), then it may not be available + // immediately for initialization. Use retries/retryDelay to initialize such elements. + var init = function (retries, retryDelay) { + var input = $('#' + id); + if (input.length === 0) { + if (retries) { + $timeout(function(){ + init(retries-1, retryDelay); + }, retryDelay); + } + return; + } - if (input.attr('ng-required')) { - scope.crmRequiredStyle = createReqStyle(scope.$parent.$eval(input.attr('ng-required'))); - scope.$parent.$watch(input.attr('ng-required'), function(isRequired) { - scope.crmRequiredStyle = createReqStyle(isRequired); - }); - } else { - scope.crmRequiredStyle = createReqStyle(input.prop('required')); - } + var tgtScope = scope;//.$parent; + if (attrs.crmDepth) { + for (var i = attrs.crmDepth; i > 0; i--) { + tgtScope = tgtScope.$parent; + } + } - var inputCtrl = form.attr('name') + '.' + input.attr('name'); - scope.$parent.$watch(inputCtrl + '.$valid', function(newValue) { - scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine'); - }); - scope.$parent.$watch(inputCtrl + '.$pristine', function(newValue) { - scope.cssClasses['crm-error'] = !scope.$parent.$eval(inputCtrl + '.$valid') && !scope.$parent.$eval(inputCtrl + '.$pristine'); + if (input.attr('ng-required')) { + scope.crmIsRequired = scope.$parent.$eval(input.attr('ng-required')); + scope.$parent.$watch(input.attr('ng-required'), function (isRequired) { + scope.crmIsRequired = isRequired; + }); + } + else { + scope.crmIsRequired = input.prop('required'); + } + + ngModel = $parse(attrs.crmUiFor)(tgtScope); + if (ngModel) { + ngModel.$viewChangeListeners.push(updateCss); + } + }; + + $timeout(function(){ + init(3, 100); }); } }; }) + // example:
+ .directive('crmUiIdScope', function () { + return { + restrict: 'EA', + scope: {}, + controllerAs: 'crmUiIdCtrl', + controller: function($scope) { + var ids = {}; + this.get = function(name) { + if (!ids[name]) { + ids[name] = "crmUiId_" + (++uidCount); + } + return ids[name]; + }; + }, + link: function (scope, element, attrs) {} + }; + }) + // example: .directive('crmUiIframe', function ($parse) { return { @@ -203,7 +248,7 @@ doc.open(); doc.writeln(iframeHtml); doc.close(); - } + }; scope.$parent.$watch(attrs.crmUiIframe, refresh); //setTimeout(function () { refresh(); }, 50); @@ -211,6 +256,36 @@ }; }) + // example: + .directive('crmUiRichtext', function ($timeout) { + return { + require: '?ngModel', + link: function (scope, elm, attr, ngModel) { + var ck = CKEDITOR.replace(elm[0]); + + if (!ngModel) { + return; + } + + ck.on('pasteState', function () { + scope.$apply(function () { + ngModel.$setViewValue(ck.getData()); + }); + }); + + ck.on('insertText', function () { + $timeout(function () { + ngModel.$setViewValue(ck.getData()); + }); + }); + + ngModel.$render = function (value) { + ck.setData(ngModel.$viewValue); + }; + } + }; + }) + // example: // example: - .directive('crmUiSelect', function ($parse) { + // usage: + .directive('crmUiSelect', function ($parse, $timeout) { return { + require: '?ngModel', scope: { - crmUiSelect: '@', - crmUiSelectModel: '@', - crmUiSelectChange: '@' + crmUiSelect: '@' }, - link: function (scope, element, attrs) { - var model = $parse(attrs.crmUiSelectModel); - + 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. - function refreshUI() { - $(element).select2('val', model(scope.$parent)); - } + 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 = model(scope.$parent), newValue = $(element).select2('val'); + var oldValue = ngModel.$viewValue, newValue = $(element).select2('val'); if (oldValue != newValue) { - scope.$parent.$apply(function(){ - model.assign(scope.$parent, newValue); + scope.$parent.$apply(function () { + ngModel.$setViewValue(newValue); }); - if (attrs.crmUiSelectChange) { - scope.$parent.$eval(attrs.crmUiSelectChange); - } } } + function init() { // TODO watch select2-options var options = attrs.crmUiSelect ? scope.$parent.$eval(attrs.crmUiSelect) : {}; $(element).select2(options); $(element).on('change', refreshModel); - setTimeout(refreshUI, 0); - scope.$parent.$watch(attrs.crmUiSelectModel, refreshUI); + $timeout(ngModel.$render); } + init(); } }; @@ -339,7 +413,7 @@ scope: { crmUiTabSet: '@' }, - templateUrl: partialUrl('tabset.html'), + templateUrl: '~/crmUi/tabset.html', transclude: true, controllerAs: 'crmUiTabSetCtrl', controller: function($scope, $parse) { @@ -381,7 +455,42 @@ scope.$parent.$watch(attrs.crmUiTime, updateChildren); element.on('change', updateParent); } - } + }; + }) + + // example: + // example: + // Generic, field-independent validator. + .directive('crmUiValidate', function() { + return { + restrict: 'EA', + require: 'ngModel', + link: function(scope, element, attrs, ngModel) { + var validationKey = attrs.crmUiValidateName ? attrs.crmUiValidateName : 'crmUiValidate'; + scope.$watch(attrs.crmUiValidate, function(newValue){ + ngModel.$setValidity(validationKey, !!newValue); + }); + } + }; + }) + + // like ng-show, but hides/displays elements using "visibility" which maintains positioning + // example
...content...
+ .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:
...
...
@@ -394,7 +503,7 @@ scope: { crmUiWizard: '@' }, - templateUrl: partialUrl('wizard.html'), + templateUrl: '~/crmUi/wizard.html', transclude: true, controllerAs: 'crmUiWizardCtrl', controller: function($scope, $parse) { @@ -417,12 +526,15 @@ 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; @@ -447,6 +559,19 @@ 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; @@ -456,7 +581,7 @@ 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) {} @@ -478,18 +603,34 @@ }; }) - // example
...content...
+ // example:
...content...
+ // If there are any conditional steps, then be sure to set a weight explicitly on *all* steps to maintain ordering. + // example:
...content...
.directive('crmUiWizardStep', function() { + var nextWeight = 1; return { - require: '^crmUiWizard', + require: ['^crmUiWizard', 'form'], restrict: 'EA', scope: { - crmTitle: '@' + crmTitle: '@', // expression, evaluates to a printable string + crmUiWizardStep: '@' // int, a weight which determines the ordering of the steps }, template: '
', 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); + }); } }; }) @@ -534,11 +675,11 @@ 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); }); }); } };