From f8601d61fca9e9d7920e6d982bd037fde186f55f Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 12 Dec 2014 01:17:25 -0800 Subject: [PATCH] CRM-15578 - Refactor crmUiField This splits crmUiField into a few smaller pieces: crmUiField, crmUiId, crmUiIdScope, crmUiFor. This loosens the coupling and also makes it easier to create reusable components which combine them. --- js/angular-crm-ui.js | 139 ++++++++++++------ js/angular-crmMailing2-directives.js | 16 ++ partials/crmCaseType/caseTypeDetails.html | 14 +- partials/crmMailing2/edit.html | 4 +- .../crmMailing2/field/msg_template_id.html | 1 + partials/crmMailing2/field/recipients.html | 1 + partials/crmMailing2/headerFooter.html | 8 +- partials/crmMailing2/mailing.html | 16 +- partials/crmMailing2/publication.html | 9 +- partials/crmMailing2/responses.html | 24 +-- partials/crmMailing2/review.html | 2 +- partials/crmMailing2/summary.html | 8 +- partials/crmMailing2/tracking.html | 10 +- partials/crmUi/field-cb.html | 5 +- partials/crmUi/field.html | 9 +- 15 files changed, 168 insertions(+), 98 deletions(-) diff --git a/js/angular-crm-ui.js b/js/angular-crm-ui.js index 5e2dd818b9..72c57d33c6 100644 --- a/js/angular-crm-ui.js +++ b/js/angular-crm-ui.js @@ -1,6 +1,8 @@ /// crmUi: Sundry UI helpers (function (angular, $, _) { + var uidCount = 0; + var partialUrl = function (relPath) { return CRM.resourceUrls['civicrm'] + '/partials/crmUi/' + relPath; }; @@ -111,12 +113,9 @@ // Display a field/row in a field list // example:
{{mydata}}
- // example:
- // example:
+ // example:
+ // example:
.directive('crmUiField', function(crmUiId) { - 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'), @@ -124,68 +123,120 @@ }; 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); + scope.crmTitle = attrs.crmTitle; + } + }; + }) - // 0. Ensure that a target field has been specified + // example:
+ .directive('crmUiId', function () { + return { + require: '^crmUiIdScope', + restrict: 'EA', + link: function (scope, element, attrs, crmUiIdCtrl) { + var id = crmUiIdCtrl.get(attrs.crmUiId); + element.attr('id', id); + } + }; + }) - if (!attrs.crmUiField) return; - if (attrs.crmUiField == 'name') { - throw new Error('Validation monitoring does not work for field name "name"'); - } + // 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 = {}; - // 1. Figure out form and input elements + if (!attrs.crmUiFor) return; - 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; - } + var id = crmUiIdCtrl.get(attrs.crmUiFor); + element.attr('for', id); + var ngModel = null; - // 2. Make sure that inputs are well-defined (with name+id). + var updateCss = function () { + scope.cssClasses['crm-error'] = !ngModel.$valid && !ngModel.$pristine; + }; - crmUiId(input); - $(label).attr('for', input.attr('id')); + // 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; + } - // 3. Monitor is the "required" and "$valid" properties + var tgtScope = scope;//.$parent; + if (attrs.crmDepth) { + for (var i = attrs.crmDepth; i > 0; i--) { + tgtScope = tgtScope.$parent; + } + } - 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')); - } + 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') + } - 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'); + 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 { diff --git a/js/angular-crmMailing2-directives.js b/js/angular-crmMailing2-directives.js index 1de1e9eec1..93abce664d 100644 --- a/js/angular-crmMailing2-directives.js +++ b/js/angular-crmMailing2-directives.js @@ -5,6 +5,21 @@ var crmMailing2 = angular.module('crmMailing2'); + crmMailing2.directive('crmMailingBlockPublication', function ($parse) { + return { + scope: { + crmMailing: '@' + }, + templateUrl: partialUrl('publication.html'), + link: function (scope, elm, attr) { + var model = $parse(attr.crmMailing); + scope.mailing = model(scope.$parent); + scope.crmMailingConst = CRM.crmMailing; + scope.ts = CRM.ts('CiviMail'); + } + }; + }); + crmMailing2.directive('crmMailingReviewBool', function () { return { scope: { @@ -87,6 +102,7 @@ }); // example: + // FIXME: participate in ngModel's validation cycle crmMailing2.directive('crmMailingRecipients', function () { return { restrict: 'AE', diff --git a/partials/crmCaseType/caseTypeDetails.html b/partials/crmCaseType/caseTypeDetails.html index fb16da9ef0..9cb878981a 100644 --- a/partials/crmCaseType/caseTypeDetails.html +++ b/partials/crmCaseType/caseTypeDetails.html @@ -4,10 +4,11 @@ Required vars: caseType The original form used table layout; don't know if we have an alternative, CSS-based layout --> -
+
-
+
-
+
WARNING: If any external files or programs reference the old "Name", then they must be updated manually.
-
- +
+
-
+
diff --git a/partials/crmMailing2/edit.html b/partials/crmMailing2/edit.html index 3f27e41152..654d7b9b4c 100644 --- a/partials/crmMailing2/edit.html +++ b/partials/crmMailing2/edit.html @@ -2,7 +2,7 @@
{{mailing|json}}
-
+
@@ -30,7 +30,7 @@
-
+
diff --git a/partials/crmMailing2/field/msg_template_id.html b/partials/crmMailing2/field/msg_template_id.html index f665d0edd1..b512c1eef7 100644 --- a/partials/crmMailing2/field/msg_template_id.html +++ b/partials/crmMailing2/field/msg_template_id.html @@ -1,5 +1,6 @@ diff --git a/partials/crmMailing2/headerFooter.html b/partials/crmMailing2/headerFooter.html index b677c78531..bd1d1744ab 100644 --- a/partials/crmMailing2/headerFooter.html +++ b/partials/crmMailing2/headerFooter.html @@ -2,10 +2,11 @@ Controller: EditMailingCtrl Required vars: mailing, crmMailingConst --> -
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
{{ts('Recipients\' replies are sent to a CiviMail specific address instead of the sender\'s address so they can be stored within CiviCRM.')}}
-
- +
+
{{ts('If a recipient replies to this mailing, forward the reply to the FROM Email address specified for the mailing.')}}
-
+
@@ -28,10 +28,11 @@ Required vars: mailing, crmMailingConst
-
+
-
+
-
+
-
+
-
+
-
+
+
+
{{ts('Track the number of times recipients click each link in this mailing. NOTE: When this feature is enabled, all links in the message body will be automatically re-written to route through your CiviCRM server prior to redirecting to the target page.')}}
-
- +
+
{{ts('Track the number of times recipients open this mailing in their email software.')}} diff --git a/partials/crmUi/field-cb.html b/partials/crmUi/field-cb.html index 944fcd2ecb..860454e15e 100644 --- a/partials/crmUi/field-cb.html +++ b/partials/crmUi/field-cb.html @@ -1,8 +1,7 @@ -