Merge pull request #5314 from mlutfy/4.6-crm16059
[civicrm-core.git] / js / angular-crmMailing / directives.js
1 (function (angular, $, _) {
2
3 // The following directives have the same simple implementation -- load
4 // a template and export a "mailing" object into scope.
5 var simpleBlocks = {
6 crmMailingBlockApprove: '~/crmMailing/approve.html',
7 crmMailingBlockHeaderFooter: '~/crmMailing/headerFooter.html',
8 crmMailingBlockMailing: '~/crmMailing/mailing.html',
9 crmMailingBlockPublication: '~/crmMailing/publication.html',
10 crmMailingBlockResponses: '~/crmMailing/responses.html',
11 crmMailingBlockRecipients: '~/crmMailing/recipients.html',
12 crmMailingBlockSchedule: '~/crmMailing/schedule.html',
13 crmMailingBlockSummary: '~/crmMailing/summary.html',
14 crmMailingBlockTracking: '~/crmMailing/tracking.html',
15 crmMailingBodyHtml: '~/crmMailing/body_html.html',
16 crmMailingBodyText: '~/crmMailing/body_text.html'
17 };
18 _.each(simpleBlocks, function(templateUrl, directiveName){
19 angular.module('crmMailing').directive(directiveName, function ($q, crmMetadata) {
20 return {
21 scope: {
22 crmMailing: '@'
23 },
24 templateUrl: templateUrl,
25 link: function (scope, elm, attr) {
26 scope.$parent.$watch(attr.crmMailing, function(newValue){
27 scope.mailing = newValue;
28 });
29 scope.crmMailingConst = CRM.crmMailing;
30 scope.ts = CRM.ts(null);
31 scope[directiveName] = attr[directiveName] ? scope.$parent.$eval(attr[directiveName]) : {};
32 $q.when(crmMetadata.getFields('Mailing'), function(fields) {
33 scope.mailingFields = fields;
34 });
35 }
36 };
37 });
38 });
39
40 // example: <div crm-mailing-block-preview crm-mailing="myMailing" on-preview="openPreview(myMailing, preview.mode)" on-send="sendEmail(myMailing,preview.recipient)">
41 // note: the directive defines a variable called "preview" with any inputs supplied by the user (e.g. the target recipient for an example mailing)
42 angular.module('crmMailing').directive('crmMailingBlockPreview', function () {
43 return {
44 templateUrl: '~/crmMailing/preview.html',
45 link: function (scope, elm, attr) {
46 scope.$watch(attr.crmMailing, function(newValue){
47 scope.mailing = newValue;
48 });
49 scope.crmMailingConst = CRM.crmMailing;
50 scope.ts = CRM.ts(null);
51 scope.testContact = {email: CRM.crmMailing.defaultTestEmail};
52 scope.testGroup = {gid: null};
53
54 scope.doPreview = function(mode) {
55 scope.$eval(attr.onPreview, {
56 preview: {mode: mode}
57 });
58 };
59 scope.doSend = function doSend(recipient) {
60 scope.$eval(attr.onSend, {
61 preview: {recipient: recipient}
62 });
63 };
64 }
65 };
66 });
67
68 angular.module('crmMailing').directive('crmMailingBlockReview', function (crmMailingPreviewMgr) {
69 return {
70 scope: {
71 crmMailing: '@'
72 },
73 templateUrl: '~/crmMailing/review.html',
74 link: function (scope, elm, attr) {
75 scope.$parent.$watch(attr.crmMailing, function(newValue){
76 scope.mailing = newValue;
77 });
78 scope.crmMailingConst = CRM.crmMailing;
79 scope.ts = CRM.ts(null);
80 scope.previewMailing = function previewMailing(mailing, mode) {
81 return crmMailingPreviewMgr.preview(mailing, mode);
82 };
83 }
84 };
85 });
86
87 // Convert between a mailing "From Address" (mailing.from_name,mailing.from_email) and a unified label ("Name" <e@ma.il>)
88 // example: <span crm-mailing-from-address="myPlaceholder" crm-mailing="myMailing"><select ng-model="myPlaceholder.label"></select></span>
89 // NOTE: This really doesn't belong in a directive. I've tried (and failed) to make this work with a getterSetter binding, eg
90 // <select ng-model="mailing.convertFromAddress" ng-model-options="{getterSetter: true}">
91 angular.module('crmMailing').directive('crmMailingFromAddress', function (crmFromAddresses) {
92 return {
93 link: function (scope, element, attrs) {
94 var placeholder = attrs.crmMailingFromAddress;
95 var mailing = null;
96 scope.$watch(attrs.crmMailing, function(newValue){
97 mailing = newValue;
98 scope[placeholder] = {
99 label: crmFromAddresses.getByAuthorEmail(mailing.from_name, mailing.from_email, true).label
100 };
101 });
102 scope.$watch(placeholder + '.label', function (newValue) {
103 var addr = crmFromAddresses.getByLabel(newValue);
104 mailing.from_name = addr.author;
105 mailing.from_email = addr.email;
106 });
107 // FIXME: Shouldn't we also be watching mailing.from_name and mailing.from_email?
108 }
109 };
110 });
111
112 // Represent a datetime field as if it were a radio ('schedule.mode') and a datetime ('schedule.datetime').
113 // example: <div crm-mailing-radio-date="mySchedule" ng-model="mailing.scheduled_date">...</div>
114 angular.module('crmMailing').directive('crmMailingRadioDate', function () {
115 return {
116 require: 'ngModel',
117 link: function ($scope, element, attrs, ngModel) {
118
119 var schedule = $scope[attrs.crmMailingRadioDate] = {
120 mode: 'now',
121 datetime: ''
122 };
123
124 ngModel.$render = function $render() {
125 var sched = ngModel.$viewValue;
126 if (!_.isEmpty(sched)) {
127 schedule.mode = 'at';
128 schedule.datetime = sched;
129 }
130 else {
131 schedule.mode = 'now';
132 }
133 validate();
134 };
135
136 var updateParent = (function () {
137 switch (schedule.mode) {
138 case 'now':
139 ngModel.$setViewValue(null);
140 schedule.datetime = ' ';
141 break;
142 case 'at':
143 ngModel.$setViewValue(schedule.datetime);
144 break;
145 default:
146 throw 'Unrecognized schedule mode: ' + schedule.mode;
147 }
148 validate();
149 });
150
151 function validate() {
152 switch (schedule.mode) {
153 case 'now':
154 ngModel.$setValidity('empty', true);
155 break;
156 case 'at':
157 ngModel.$setValidity('empty', !_.isEmpty(schedule.datetime) && schedule.datetime !== ' ');
158 break;
159 default:
160 throw 'Unrecognized schedule mode: ' + schedule.mode;
161 }
162 }
163
164 $scope.$watch(attrs.crmMailingRadioDate + '.mode', updateParent);
165 $scope.$watch(attrs.crmMailingRadioDate + '.datetime', function (newValue, oldValue) {
166 // automatically switch mode based on datetime entry
167 if (oldValue != newValue) {
168 if (_.isEmpty(newValue) || newValue == " ") {
169 schedule.mode = 'now';
170 }
171 else {
172 schedule.mode = 'at';
173 }
174 }
175 updateParent();
176 });
177 }
178 };
179 });
180
181 angular.module('crmMailing').directive('crmMailingReviewBool', function () {
182 return {
183 scope: {
184 crmOn: '@',
185 crmTitle: '@'
186 },
187 template: '<span ng-class="spanClasses"><span class="icon" ng-class="iconClasses"></span>{{evalTitle}} </span>',
188 link: function (scope, element, attrs) {
189 function refresh() {
190 if (scope.$parent.$eval(attrs.crmOn)) {
191 scope.spanClasses = {'crmMailing-active': true};
192 scope.iconClasses = {'ui-icon-check': true};
193 }
194 else {
195 scope.spanClasses = {'crmMailing-inactive': true};
196 scope.iconClasses = {'ui-icon-close': true};
197 }
198 scope.evalTitle = scope.$parent.$eval(attrs.crmTitle);
199 }
200
201 refresh();
202 scope.$parent.$watch(attrs.crmOn, refresh);
203 scope.$parent.$watch(attrs.crmTitle, refresh);
204 }
205 };
206 });
207
208 // example: <input name="subject" /> <input crm-mailing-token on-select="doSomething(token.name)" />
209 // WISHLIST: Instead of global CRM.crmMailing.mailTokens, accept token list as an input
210 angular.module('crmMailing').directive('crmMailingToken', function () {
211 return {
212 require: '^crmUiIdScope',
213 scope: {
214 onSelect: '@'
215 },
216 template: '<input type="text" class="crmMailingToken" />',
217 link: function (scope, element, attrs, crmUiIdCtrl) {
218 $(element).addClass('crm-action-menu action-icon-token').select2({
219 width: "12em",
220 dropdownAutoWidth: true,
221 data: CRM.crmMailing.mailTokens,
222 placeholder: ts('Tokens')
223 });
224 $(element).on('select2-selecting', function (e) {
225 e.preventDefault();
226 $(element).select2('close').select2('val', '');
227 scope.$parent.$eval(attrs.onSelect, {
228 token: {name: e.val}
229 });
230 });
231 }
232 };
233 });
234
235 // example: <select multiple crm-mailing-recipients crm-mailing="mymailing" crm-avail-groups="myGroups" crm-avail-mailings="myMailings"></select>
236 // FIXME: participate in ngModel's validation cycle
237 angular.module('crmMailing').directive('crmMailingRecipients', function (crmUiAlert) {
238 return {
239 restrict: 'AE',
240 require: 'ngModel',
241 scope: {
242 crmAvailGroups: '@', // available groups
243 crmAvailMailings: '@', // available mailings
244 crmMandatoryGroups: '@', // hard-coded/mandatory groups
245 ngRequired: '@'
246 },
247 templateUrl: '~/crmMailing/directive/recipients.html',
248 link: function (scope, element, attrs, ngModel) {
249 scope.recips = ngModel.$viewValue;
250 scope.groups = scope.$parent.$eval(attrs.crmAvailGroups);
251 scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings);
252 refreshMandatory();
253
254 var ts = scope.ts = CRM.ts(null);
255
256 /// Convert MySQL date ("yyyy-mm-dd hh:mm:ss") to JS date object
257 scope.parseDate = function (date) {
258 if (!angular.isString(date)) {
259 return date;
260 }
261 var p = date.split(/[\- :]/);
262 return new Date(parseInt(p[0]), parseInt(p[1])-1, parseInt(p[2]), parseInt(p[3]), parseInt(p[4]), parseInt(p[5]));
263 };
264
265 /// Remove {value} from {array}
266 function arrayRemove(array, value) {
267 var idx = array.indexOf(value);
268 if (idx >= 0) {
269 array.splice(idx, 1);
270 }
271 }
272
273 // @param string id an encoded string like "4 civicrm_mailing include"
274 // @return Object keys: entity_id, entity_type, mode
275 function convertValueToObj(id) {
276 var a = id.split(" ");
277 return {entity_id: parseInt(a[0]), entity_type: a[1], mode: a[2]};
278 }
279
280 // @param Object mailing
281 // @return array list of values like "4 civicrm_mailing include"
282 function convertMailingToValues(recipients) {
283 var r = [];
284 angular.forEach(recipients.groups.include, function (v) {
285 r.push(v + " civicrm_group include");
286 });
287 angular.forEach(recipients.groups.exclude, function (v) {
288 r.push(v + " civicrm_group exclude");
289 });
290 angular.forEach(recipients.mailings.include, function (v) {
291 r.push(v + " civicrm_mailing include");
292 });
293 angular.forEach(recipients.mailings.exclude, function (v) {
294 r.push(v + " civicrm_mailing exclude");
295 });
296 return r;
297 }
298
299 function refreshMandatory() {
300 if (ngModel.$viewValue && ngModel.$viewValue.groups) {
301 scope.mandatoryGroups = _.filter(scope.$parent.$eval(attrs.crmMandatoryGroups), function(grp) {
302 return _.contains(ngModel.$viewValue.groups.include, parseInt(grp.id));
303 });
304 scope.mandatoryIds = _.map(_.pluck(scope.$parent.$eval(attrs.crmMandatoryGroups), 'id'), function(n) {
305 return parseInt(n);
306 });
307 }
308 else {
309 scope.mandatoryGroups = [];
310 scope.mandatoryIds = [];
311 }
312 }
313
314 function isMandatory(grpId) {
315 return _.contains(scope.mandatoryIds, parseInt(grpId));
316 }
317
318 var refreshUI = ngModel.$render = function refresuhUI() {
319 scope.recips = ngModel.$viewValue;
320 if (ngModel.$viewValue) {
321 $(element).select2('val', convertMailingToValues(ngModel.$viewValue));
322 validate();
323 refreshMandatory();
324 }
325 };
326
327 /// @return string HTML representingn an option
328 function formatItem(item) {
329 if (!item.id) {
330 // return `text` for optgroup
331 return item.text;
332 }
333 var option = convertValueToObj(item.id);
334 var icon = (option.entity_type === 'civicrm_mailing') ? 'EnvelopeIn.gif' : 'group.png';
335 var spanClass = (option.mode == 'exclude') ? 'crmMailing-exclude' : 'crmMailing-include';
336 if (option.entity_type != 'civicrm_mailing' && isMandatory(option.entity_id)) {
337 spanClass = 'crmMailing-mandatory';
338 }
339 return "<img src='../../sites/all/modules/civicrm/i/" + icon + "' height=12 width=12 /> <span class='" + spanClass + "'>" + item.text + "</span>";
340 }
341
342 function validate() {
343 if (scope.$parent.$eval(attrs.ngRequired)) {
344 var empty = (_.isEmpty(ngModel.$viewValue.groups.include) && _.isEmpty(ngModel.$viewValue.mailings.include));
345 ngModel.$setValidity('empty', !empty);
346 } else {
347 ngModel.$setValidity('empty', true);
348 }
349 }
350
351 $(element).select2({
352 dropdownAutoWidth: true,
353 placeholder: "Groups or Past Recipients",
354 formatResult: formatItem,
355 formatSelection: formatItem,
356 escapeMarkup: function (m) {
357 return m;
358 }
359 });
360
361 $(element).on('select2-selecting', function (e) {
362 var option = convertValueToObj(e.val);
363 var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups';
364 if (option.mode == 'exclude') {
365 ngModel.$viewValue[typeKey].exclude.push(option.entity_id);
366 arrayRemove(ngModel.$viewValue[typeKey].include, option.entity_id);
367 }
368 else {
369 ngModel.$viewValue[typeKey].include.push(option.entity_id);
370 arrayRemove(ngModel.$viewValue[typeKey].exclude, option.entity_id);
371 }
372 scope.$apply();
373 $(element).select2('close');
374 validate();
375 e.preventDefault();
376 });
377
378 $(element).on("select2-removing", function (e) {
379 var option = convertValueToObj(e.val);
380 var typeKey = option.entity_type == 'civicrm_mailing' ? 'mailings' : 'groups';
381 if (typeKey == 'groups' && isMandatory(option.entity_id)) {
382 crmUiAlert({
383 text: ts('This mailing was generated based on search results. The search results cannot be removed.'),
384 title: ts('Required')
385 });
386 e.preventDefault();
387 return;
388 }
389 scope.$parent.$apply(function () {
390 arrayRemove(ngModel.$viewValue[typeKey][option.mode], option.entity_id);
391 });
392 validate();
393 e.preventDefault();
394 });
395
396 scope.$watchCollection("recips.groups.include", refreshUI);
397 scope.$watchCollection("recips.groups.exclude", refreshUI);
398 scope.$watchCollection("recips.mailings.include", refreshUI);
399 scope.$watchCollection("recips.mailings.exclude", refreshUI);
400 setTimeout(refreshUI, 50);
401
402 scope.$watchCollection(attrs.crmAvailGroups, function() {
403 scope.groups = scope.$parent.$eval(attrs.crmAvailGroups);
404 });
405 scope.$watchCollection(attrs.crmAvailMailings, function() {
406 scope.mailings = scope.$parent.$eval(attrs.crmAvailMailings);
407 });
408 scope.$watchCollection(attrs.crmMandatoryGroups, function() {
409 refreshMandatory();
410 });
411 }
412 };
413 });
414
415 })(angular, CRM.$, CRM._);