Afform - fix contact source field
[civicrm-core.git] / ext / afform / admin / ang / afGuiEditor / elements / afGuiField.component.js
1 // https://civicrm.org/licensing
2 (function(angular, $, _) {
3 "use strict";
4
5 angular.module('afGuiEditor').component('afGuiField', {
6 templateUrl: '~/afGuiEditor/elements/afGuiField.html',
7 bindings: {
8 node: '=',
9 deleteThis: '&'
10 },
11 require: {
12 editor: '^^afGuiEditor',
13 container: '^^afGuiContainer'
14 },
15 controller: function($scope, afGui, $timeout) {
16 var ts = $scope.ts = CRM.ts('org.civicrm.afform_admin'),
17 ctrl = this,
18 entityRefOptions = [],
19 singleElement = [''],
20 // When search-by-range is enabled the second element gets a suffix for some properties like "placeholder2"
21 rangeElements = ['', '2'],
22 dateRangeElements = ['1', '2'],
23 relativeDatesWithPickRange = CRM.afGuiEditor.dateRanges,
24 relativeDatesWithoutPickRange = relativeDatesWithPickRange.slice(1),
25 yesNo = [
26 {id: '1', label: ts('Yes')},
27 {id: '0', label: ts('No')}
28 ];
29 $scope.editingOptions = false;
30
31 this.$onInit = function() {
32 ctrl.hasDefaultValue = !!getSet('afform_default');
33 ctrl.fieldDefn = angular.extend({}, ctrl.getDefn(), ctrl.node.defn);
34 ctrl.inputTypes = _.transform(_.cloneDeep(afGui.meta.inputType), function(inputTypes, type) {
35 if (inputTypeCanBe(type.name)) {
36 // Change labels for EntityRef fields
37 if (ctrl.getDefn().input_type === 'EntityRef') {
38 var entity = ctrl.getFkEntity();
39 if (entity && type.name === 'EntityRef') {
40 type.label = ts('Autocomplete %1', {1: entity.label});
41 }
42 if (entity && type.name === 'Number') {
43 type.label = ts('%1 ID', {1: entity.label});
44 }
45 if (entity && type.name === 'Select') {
46 type.label = ts('Select Form %1', {1: entity.label});
47 }
48 }
49 inputTypes.push(type);
50 }
51 });
52 };
53
54 this.getFkEntity = function() {
55 var fkEntity = ctrl.getDefn().fk_entity;
56 return ctrl.editor.meta.entities[fkEntity];
57 };
58
59 this.isSearch = function() {
60 return ctrl.editor.getFormType() === 'search';
61 };
62
63 this.canBeRange = function() {
64 // Range search only makes sense for search display forms
65 return this.isSearch() &&
66 // Hack for postal code which is not stored as a number but can act like one
67 (ctrl.node.name.substr(-11) === 'postal_code' || (
68 // Multiselects cannot use range search
69 !ctrl.getDefn().input_attrs.multiple &&
70 // DataType & inputType must make sense for a range
71 _.includes(['Date', 'Timestamp', 'Integer', 'Float', 'Money'], ctrl.getDefn().data_type) &&
72 _.includes(['Date', 'Number', 'Select'], $scope.getProp('input_type'))
73 ));
74 };
75
76 this.canBeMultiple = function() {
77 return this.isSearch() &&
78 !_.includes(['Date', 'Timestamp'], ctrl.getDefn().data_type) &&
79 _.includes(['Select', 'EntityRef'], $scope.getProp('input_type'));
80 };
81
82 this.getRangeElements = function(type) {
83 if (!$scope.getProp('search_range') || (type === 'Select' && ctrl.getDefn().input_type === 'Date')) {
84 return singleElement;
85 }
86 return type === 'Date' ? dateRangeElements : rangeElements;
87 };
88
89 // Returns the original field definition from metadata
90 this.getDefn = function() {
91 var defn = afGui.getField(ctrl.container.getFieldEntityType(ctrl.node.name), ctrl.node.name);
92 defn = defn || {
93 label: ts('Untitled'),
94 required: false
95 };
96 if (_.isEmpty(defn.input_attrs)) {
97 defn.input_attrs = {};
98 }
99 return defn;
100 };
101
102 $scope.getOriginalLabel = function() {
103 if (ctrl.container.getEntityName()) {
104 return ctrl.editor.getEntity(ctrl.container.getEntityName()).label + ': ' + ctrl.getDefn().label;
105 }
106 return afGui.getEntity(ctrl.container.getFieldEntityType(ctrl.node.name)).label + ': ' + ctrl.getDefn().label;
107 };
108
109 $scope.hasOptions = function() {
110 var inputType = $scope.getProp('input_type');
111 return _.contains(['CheckBox', 'Radio', 'Select'], inputType) && !(inputType === 'CheckBox' && !ctrl.getDefn().options);
112 };
113
114 this.getOptions = function() {
115 if (ctrl.node.defn && ctrl.node.defn.options) {
116 return ctrl.node.defn.options;
117 }
118 if (_.includes(['Date', 'Timestamp'], $scope.getProp('data_type'))) {
119 return $scope.getProp('search_range') ? relativeDatesWithPickRange : relativeDatesWithoutPickRange;
120 }
121 if (ctrl.getDefn().input_type === 'EntityRef') {
122 // Build a list of all entities in this form that can be referenced by this field.
123 var newOptions = _.map(ctrl.editor.getEntities({type: ctrl.getDefn().fk_entity}), function(entity) {
124 return {id: entity.name, label: entity.label};
125 }, []);
126 // Store it in a stable variable for the sake of ng-repeat
127 if (!angular.equals(newOptions, entityRefOptions)) {
128 entityRefOptions = newOptions;
129 }
130 return entityRefOptions;
131 }
132 return ctrl.getDefn().options || ($scope.getProp('input_type') === 'CheckBox' ? null : yesNo);
133 };
134
135 $scope.resetOptions = function() {
136 delete ctrl.node.defn.options;
137 };
138
139 $scope.editOptions = function() {
140 $scope.editingOptions = true;
141 $('#afGuiEditor').addClass('af-gui-editing-content');
142 };
143
144 function inputTypeCanBe(type) {
145 var defn = ctrl.getDefn();
146 if (defn.input_type === type) {
147 return true;
148 }
149 switch (type) {
150 case 'CheckBox':
151 case 'Radio':
152 return defn.options || defn.data_type === 'Boolean';
153
154 case 'Select':
155 return defn.options || defn.data_type === 'Boolean' || defn.input_type === 'EntityRef' || (defn.input_type === 'Date' && ctrl.isSearch());
156
157 case 'Date':
158 return defn.input_type === 'Date';
159
160 case 'TextArea':
161 case 'RichTextEditor':
162 return (defn.data_type === 'Text' || defn.data_type === 'String');
163
164 case 'Text':
165 return !(defn.options || defn.input_type === 'Date' || defn.input_type === 'EntityRef' || defn.data_type === 'Boolean');
166
167 case 'Number':
168 return !(defn.options || defn.data_type === 'Boolean');
169
170 default:
171 return false;
172 }
173 }
174
175 // Returns a value from either the local field defn or the base defn
176 $scope.getProp = function(propName) {
177 var path = propName.split('.'),
178 item = path.pop(),
179 localDefn = drillDown(ctrl.node.defn || {}, path);
180 if (typeof localDefn[item] !== 'undefined') {
181 return localDefn[item];
182 }
183 return drillDown(ctrl.getDefn(), path)[item];
184 };
185
186 // Checks for a value in either the local field defn or the base defn
187 $scope.propIsset = function(propName) {
188 var val = $scope.getProp(propName);
189 return !(typeof val === 'undefined' || val === null);
190 };
191
192 $scope.toggleLabel = function() {
193 ctrl.node.defn = ctrl.node.defn || {};
194 if (ctrl.node.defn.label === false) {
195 delete ctrl.node.defn.label;
196 } else {
197 ctrl.node.defn.label = false;
198 }
199 };
200
201 $scope.toggleMultiple = function() {
202 var newVal = getSet('input_attrs.multiple', !getSet('input_attrs.multiple'));
203 if (newVal && getSet('search_range')) {
204 getSet('search_range', false);
205 }
206 };
207
208 $scope.toggleSearchRange = function() {
209 var newVal = getSet('search_range', !getSet('search_range'));
210 if (newVal && getSet('input_attrs.multiple')) {
211 getSet('input_attrs.multiple', false);
212 }
213 };
214
215 $scope.toggleRequired = function() {
216 getSet('required', !getSet('required'));
217 };
218
219 $scope.toggleHelp = function(position) {
220 getSet('help_' + position, $scope.propIsset('help_' + position) ? null : (ctrl.getDefn()['help_' + position] || ts('Enter text')));
221 };
222
223 function defaultValueShouldBeArray() {
224 return ($scope.getProp('data_type') !== 'Boolean' &&
225 ($scope.getProp('input_type') === 'CheckBox' || $scope.getProp('input_attrs.multiple')));
226 }
227
228
229 $scope.toggleDefaultValue = function() {
230 if (ctrl.hasDefaultValue) {
231 getSet('afform_default', undefined);
232 ctrl.hasDefaultValue = false;
233 } else {
234 ctrl.hasDefaultValue = true;
235 }
236 };
237
238 $scope.defaultValueContains = function(val) {
239 var defaultVal = getSet('afform_default');
240 return defaultVal === val || (_.isArray(defaultVal) && _.includes(defaultVal, val));
241 };
242
243 $scope.toggleDefaultValueItem = function(val) {
244 if (defaultValueShouldBeArray()) {
245 if (!_.isArray(getSet('afform_default'))) {
246 ctrl.node.defn.afform_default = [];
247 }
248 if (_.includes(ctrl.node.defn.afform_default, val)) {
249 var newVal = _.without(ctrl.node.defn.afform_default, val);
250 getSet('afform_default', newVal.length ? newVal : undefined);
251 ctrl.hasDefaultValue = !!newVal.length;
252 } else {
253 ctrl.node.defn.afform_default.push(val);
254 ctrl.hasDefaultValue = true;
255 }
256 } else if (getSet('afform_default') === val) {
257 getSet('afform_default', undefined);
258 ctrl.hasDefaultValue = false;
259 } else {
260 getSet('afform_default', val);
261 ctrl.hasDefaultValue = true;
262 }
263 };
264
265 // Getter/setter for definition props
266 $scope.getSet = function(propName) {
267 return _.wrap(propName, getSet);
268 };
269
270 // Getter/setter callback
271 function getSet(propName, val) {
272 if (arguments.length > 1) {
273 var path = propName.split('.'),
274 item = path.pop(),
275 localDefn = drillDown(ctrl.node, ['defn'].concat(path)),
276 fieldDefn = drillDown(ctrl.getDefn(), path);
277 // Set the value if different than the field defn, otherwise unset it
278 if (typeof val !== 'undefined' && (val !== fieldDefn[item] && !(!val && !fieldDefn[item]))) {
279 localDefn[item] = val;
280 } else {
281 delete localDefn[item];
282 clearOut(ctrl.node, ['defn'].concat(path));
283 }
284 // When changing input_type
285 if (propName === 'input_type') {
286 if (ctrl.node.defn && ctrl.node.defn.search_range && !ctrl.canBeRange()) {
287 delete ctrl.node.defn.search_range;
288 clearOut(ctrl.node, ['defn']);
289 }
290 if (ctrl.node.defn && ctrl.node.defn.input_attrs && 'multiple' in ctrl.node.defn.input_attrs && !ctrl.canBeMultiple()) {
291 delete ctrl.node.defn.input_attrs.multiple;
292 clearOut(ctrl.node, ['defn', 'input_attrs']);
293 }
294 }
295 ctrl.fieldDefn = angular.extend({}, ctrl.getDefn(), ctrl.node.defn);
296
297 // When changing the multiple property, force-reset the default value widget
298 if (ctrl.hasDefaultValue && _.includes(['input_type', 'input_attrs.multiple'], propName)) {
299 ctrl.hasDefaultValue = false;
300 if (!defaultValueShouldBeArray() && _.isArray(getSet('afform_default'))) {
301 ctrl.node.defn.afform_default = ctrl.node.defn.afform_default[0];
302 } else if (defaultValueShouldBeArray() && _.isString(getSet('afform_default')) && ctrl.node.defn.afform_default.length) {
303 ctrl.node.defn.afform_default = ctrl.node.defn.afform_default.split(',');
304 }
305 $timeout(function() {
306 ctrl.hasDefaultValue = true;
307 });
308 }
309 return val;
310 }
311 return $scope.getProp(propName);
312 }
313 this.getSet = getSet;
314
315 this.setEditingOptions = function(val) {
316 $scope.editingOptions = val;
317 };
318
319 // Returns a reference to a path n-levels deep within an object
320 function drillDown(parent, path) {
321 var container = parent;
322 _.each(path, function(level) {
323 container[level] = container[level] || {};
324 container = container[level];
325 });
326 return container;
327 }
328
329 // Returns true only if value is [], {}, '', null, or undefined.
330 function isEmpty(val) {
331 return typeof val !== 'boolean' && typeof val !== 'number' && _.isEmpty(val);
332 }
333
334 // Recursively clears out empty arrays and objects
335 function clearOut(parent, path) {
336 var item;
337 while (path.length && _.every(drillDown(parent, path), isEmpty)) {
338 item = path.pop();
339 delete drillDown(parent, path)[item];
340 }
341 }
342 }
343 });
344
345 })(angular, CRM.$, CRM._);