Merge pull request #21646 from MegaphoneJon/core-2874
[civicrm-core.git] / ext / afform / core / ang / af / afField.component.js
1 (function(angular, $, _) {
2 var id = 0;
3 // Example usage: <div af-fieldset="myModel"><af-field name="do_not_email" /></div>
4 angular.module('af').component('afField', {
5 require: {
6 afFieldset: '^^afFieldset',
7 afJoin: '?^^afJoin',
8 afRepeatItem: '?^^afRepeatItem'
9 },
10 templateUrl: '~/af/afField.html',
11 bindings: {
12 fieldName: '@name',
13 defn: '='
14 },
15 controller: function($scope, $element, crmApi4, $timeout, $location) {
16 var ts = $scope.ts = CRM.ts('org.civicrm.afform'),
17 ctrl = this,
18 // Prefix used for SearchKit explicit joins
19 namePrefix = '',
20 boolOptions = [{id: true, label: ts('Yes')}, {id: false, label: ts('No')}],
21 // Used to store chain select options loaded on-the-fly
22 chainSelectOptions = null,
23 // Only used for is_primary radio button
24 noOptions = [{id: true, label: ''}];
25
26 // Attributes for each of the low & high date fields when using search_range
27 this.inputAttrs = [];
28
29 this.$onInit = function() {
30 var closestController = $($element).closest('[af-fieldset],[af-join],[af-repeat-item]');
31 $scope.dataProvider = closestController.is('[af-repeat-item]') ? ctrl.afRepeatItem : ctrl.afJoin || ctrl.afFieldset;
32 $scope.fieldId = ctrl.fieldName + '-' + id++;
33
34 $element.addClass('af-field-type-' + _.kebabCase(ctrl.defn.input_type));
35
36 if (this.defn.name !== this.fieldName) {
37 namePrefix = this.fieldName.substr(0, this.fieldName.length - this.defn.name.length);
38 }
39
40 if (ctrl.defn.search_range) {
41 // Initialize value as object unless using relative date select
42 var initialVal = $scope.dataProvider.getFieldData()[ctrl.fieldName];
43 if (!_.isArray($scope.dataProvider.getFieldData()[ctrl.fieldName]) &&
44 (ctrl.defn.input_type !== 'Select' || !ctrl.defn.is_date || initialVal !== '{}')
45 ) {
46 $scope.dataProvider.getFieldData()[ctrl.fieldName] = {};
47 }
48 // Initialize inputAttrs (only used for datePickers at the moment)
49 if (ctrl.defn.is_date) {
50 this.inputAttrs.push(ctrl.defn.input_attrs || {});
51 for (var i = 1; i <= 2; ++i) {
52 var attrs = _.cloneDeep(ctrl.defn.input_attrs || {});
53 attrs.placeholder = attrs['placeholder' + i];
54 attrs.timePlaceholder = attrs['timePlaceholder' + i];
55 ctrl.inputAttrs.push(attrs);
56 }
57 }
58 }
59
60 // is_primary field - watch others in this afRepeat block to ensure only one is selected
61 if (ctrl.fieldName === 'is_primary' && 'repeatIndex' in $scope.dataProvider) {
62 $scope.$watch('dataProvider.afRepeat.getEntityController().getData()', function (items, prev) {
63 var index = $scope.dataProvider.repeatIndex;
64 // Set first item to primary if there isn't a primary
65 if (items && !index && !_.find(items, 'is_primary')) {
66 $scope.dataProvider.getFieldData().is_primary = true;
67 }
68 // Set this item to not primary if another has been selected
69 if (items && prev && items.length === prev.length && items[index].is_primary && prev[index].is_primary &&
70 _.filter(items, 'is_primary').length > 1
71 ) {
72 $scope.dataProvider.getFieldData().is_primary = false;
73 }
74 }, true);
75 }
76
77 // ChainSelect - watch control field & reload options as needed
78 if (ctrl.defn.input_type === 'ChainSelect') {
79 var controlField = namePrefix + ctrl.defn.input_attrs.control_field;
80 $scope.$watch('dataProvider.getFieldData()["' + controlField + '"]', function(val) {
81 // After switching option list, remove invalid options
82 function validateValue() {
83 var options = $scope.getOptions(),
84 value = $scope.dataProvider.getFieldData()[ctrl.fieldName];
85 if (_.isArray(value)) {
86 _.remove(value, function(item) {
87 return !_.find(options, function(option) {return option.id == item;});
88 });
89 } else if (value && !_.find(options, function(option) {return option.id == value;})) {
90 $scope.dataProvider.getFieldData()[ctrl.fieldName] = '';
91 }
92 }
93 if (val) {
94 $('input[crm-ui-select]', $element).addClass('loading').prop('disabled', true);
95 var params = {
96 where: [['name', '=', ctrl.defn.name]],
97 select: ['options'],
98 loadOptions: ['id', 'label'],
99 values: {}
100 };
101 params.values[ctrl.defn.input_attrs.control_field] = val;
102 crmApi4(ctrl.defn.entity, 'getFields', params, 0)
103 .then(function(data) {
104 $('input[crm-ui-select]', $element).removeClass('loading').prop('disabled', false);
105 chainSelectOptions = data.options;
106 validateValue();
107 });
108 } else {
109 chainSelectOptions = null;
110 validateValue();
111 }
112 }, true);
113 }
114
115 // Wait for parent controllers to initialize
116 $timeout(function() {
117 // Unique field name = entity_name index . join . field_name
118 var entityName = ctrl.afFieldset.getName(),
119 joinEntity = ctrl.afJoin ? ctrl.afJoin.entity : null,
120 uniquePrefix = '',
121 urlArgs = $location.search();
122 if (entityName) {
123 var index = ctrl.getEntityIndex();
124 uniquePrefix = entityName + (index ? index + 1 : '') + (joinEntity ? '.' + joinEntity : '') + '.';
125 }
126 // Set default value from url with uniquePrefix + fieldName
127 if (urlArgs && urlArgs[uniquePrefix + ctrl.fieldName]) {
128 setValue(urlArgs[uniquePrefix + ctrl.fieldName]);
129 }
130 // Set default value from url with fieldName only
131 else if (urlArgs && urlArgs[ctrl.fieldName]) {
132 $scope.dataProvider.getFieldData()[ctrl.fieldName] = urlArgs[ctrl.fieldName];
133 }
134 // Set default value based on field defn
135 else if (ctrl.defn.afform_default) {
136 setValue(ctrl.defn.afform_default);
137 }
138 });
139 };
140
141 // Set default value; ensure data type matches input type
142 function setValue(value) {
143 if (ctrl.defn.input_type === 'Number' && ctrl.defn.search_range) {
144 if (!_.isPlainObject(value)) {
145 value = {
146 '>=': +(('' + value).split('-')[0] || 0),
147 '<=': +(('' + value).split('-')[1] || 0),
148 };
149 }
150 } else if (ctrl.defn.input_type === 'Number') {
151 value = +value;
152 } else if (ctrl.defn.search_range && !_.isPlainObject(value)) {
153 value = {
154 '>=': ('' + value).split('-')[0],
155 '<=': ('' + value).split('-')[1] || '',
156 };
157 }
158
159 $scope.dataProvider.getFieldData()[ctrl.fieldName] = value;
160 }
161
162 // Get the repeat index of the entity fieldset (not the join)
163 ctrl.getEntityIndex = function() {
164 // If already in a join repeat, look up the outer repeat
165 if ('repeatIndex' in $scope.dataProvider && $scope.dataProvider.afRepeat.getRepeatType() === 'join') {
166 return $scope.dataProvider.outerRepeatItem ? $scope.dataProvider.outerRepeatItem.repeatIndex : 0;
167 } else {
168 return ctrl.afRepeatItem ? ctrl.afRepeatItem.repeatIndex : 0;
169 }
170 };
171
172 // Params for the Afform.submitFile API when uploading a file field
173 ctrl.getFileUploadParams = function() {
174 return {
175 entityName: ctrl.afFieldset.modelName,
176 fieldName: ctrl.fieldName,
177 joinEntity: ctrl.afJoin ? ctrl.afJoin.entity : null,
178 entityIndex: ctrl.getEntityIndex(),
179 joinIndex: ctrl.afJoin && $scope.dataProvider.repeatIndex || null
180 };
181 };
182
183 $scope.getOptions = function () {
184 return chainSelectOptions || ctrl.defn.options || (ctrl.fieldName === 'is_primary' && ctrl.defn.input_type === 'Radio' ? noOptions : boolOptions);
185 };
186
187 $scope.select2Options = function() {
188 return {
189 results: _.transform($scope.getOptions(), function(result, opt) {
190 result.push({id: opt.id, text: opt.label});
191 }, [])
192 };
193 };
194
195 // Getter/Setter function for fields of type select or entityRef.
196 $scope.getSetSelect = function(val) {
197 var currentVal = $scope.dataProvider.getFieldData()[ctrl.fieldName];
198 // Setter
199 if (arguments.length) {
200 if (ctrl.defn.is_date) {
201 // The '{}' string is a placeholder for "choose date range"
202 if (val === '{}') {
203 val = !_.isPlainObject(currentVal) ? {} : currentVal;
204 }
205 }
206 // If search_range, this select is the "low" value (the high value uses ng-model without a getterSetter fn)
207 else if (ctrl.defn.search_range) {
208 return ($scope.dataProvider.getFieldData()[ctrl.fieldName]['>='] = val);
209 }
210 // A multi-select needs to split string value into an array
211 if (ctrl.defn.input_attrs && ctrl.defn.input_attrs.multiple) {
212 val = val ? val.split(',') : [];
213 }
214 return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val);
215 }
216 // Getter
217 if (_.isArray(currentVal)) {
218 return currentVal.join(',');
219 }
220 if (ctrl.defn.is_date) {
221 return _.isPlainObject(currentVal) ? '{}' : currentVal;
222 }
223 // If search_range, this select is the "low" value (the high value uses ng-model without a getterSetter fn)
224 else if (ctrl.defn.search_range) {
225 return currentVal['>='];
226 }
227 return currentVal;
228 };
229
230 }
231 });
232 })(angular, CRM.$, CRM._);