1 // https://civicrm.org/licensing
2 (function(angular
, $, _
) {
5 angular
.module('afGuiEditor').component('afGuiField', {
6 templateUrl
: '~/afGuiEditor/elements/afGuiField.html',
12 editor
: '^^afGuiEditor',
13 container
: '^^afGuiContainer'
15 controller: function($scope
, afGui
, $timeout
) {
16 var ts
= $scope
.ts
= CRM
.ts('org.civicrm.afform_admin'),
18 entityRefOptions
= [],
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),
26 {id
: '1', label
: ts('Yes')},
27 {id
: '0', label
: ts('No')}
29 $scope
.editingOptions
= false;
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
});
42 if (entity
&& type
.name
=== 'Number') {
43 type
.label
= ts('%1 ID', {1: entity
.label
});
45 if (entity
&& type
.name
=== 'Select') {
46 type
.label
= ts('Select Form %1', {1: entity
.label
});
49 inputTypes
.push(type
);
54 this.getFkEntity = function() {
55 var fkEntity
= ctrl
.getDefn().fk_entity
;
56 return ctrl
.editor
.meta
.entities
[fkEntity
];
59 this.isSearch = function() {
60 return ctrl
.editor
.getFormType() === 'search';
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'))
76 this.canBeMultiple = function() {
77 return this.isSearch() &&
78 !_
.includes(['Date', 'Timestamp'], ctrl
.getDefn().data_type
) &&
79 _
.includes(['Select', 'EntityRef'], $scope
.getProp('input_type'));
82 this.getRangeElements = function(type
) {
83 if (!$scope
.getProp('search_range') || (type
=== 'Select' && ctrl
.getDefn().input_type
=== 'Date')) {
86 return type
=== 'Date' ? dateRangeElements
: rangeElements
;
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
);
93 label
: ts('Untitled'),
96 if (_
.isEmpty(defn
.input_attrs
)) {
97 defn
.input_attrs
= {};
102 $scope
.getOriginalLabel = function() {
103 if (ctrl
.container
.getEntityName()) {
104 return ctrl
.editor
.getEntity(ctrl
.container
.getEntityName()).label
+ ': ' + ctrl
.getDefn().label
;
106 return afGui
.getEntity(ctrl
.container
.getFieldEntityType(ctrl
.node
.name
)).label
+ ': ' + ctrl
.getDefn().label
;
109 $scope
.hasOptions = function() {
110 var inputType
= $scope
.getProp('input_type');
111 return _
.contains(['CheckBox', 'Radio', 'Select'], inputType
) && !(inputType
=== 'CheckBox' && !ctrl
.getDefn().options
);
114 this.getOptions = function() {
115 if (ctrl
.node
.defn
&& ctrl
.node
.defn
.options
) {
116 return ctrl
.node
.defn
.options
;
118 if (_
.includes(['Date', 'Timestamp'], $scope
.getProp('data_type'))) {
119 return $scope
.getProp('search_range') ? relativeDatesWithPickRange
: relativeDatesWithoutPickRange
;
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
};
126 // Store it in a stable variable for the sake of ng-repeat
127 if (!angular
.equals(newOptions
, entityRefOptions
)) {
128 entityRefOptions
= newOptions
;
130 return entityRefOptions
;
132 return ctrl
.getDefn().options
|| ($scope
.getProp('input_type') === 'CheckBox' ? null : yesNo
);
135 $scope
.resetOptions = function() {
136 delete ctrl
.node
.defn
.options
;
139 $scope
.editOptions = function() {
140 $scope
.editingOptions
= true;
141 $('#afGuiEditor').addClass('af-gui-editing-content');
144 function inputTypeCanBe(type
) {
145 var defn
= ctrl
.getDefn();
146 if (defn
.input_type
=== type
) {
152 return defn
.options
|| defn
.data_type
=== 'Boolean';
155 return defn
.options
|| defn
.data_type
=== 'Boolean' || defn
.input_type
=== 'EntityRef' || (defn
.input_type
=== 'Date' && ctrl
.isSearch());
158 return defn
.input_type
=== 'Date';
161 case 'RichTextEditor':
162 return (defn
.data_type
=== 'Text' || defn
.data_type
=== 'String');
165 return !(defn
.options
|| defn
.input_type
=== 'Date' || defn
.input_type
=== 'EntityRef' || defn
.data_type
=== 'Boolean');
168 return !(defn
.options
|| defn
.data_type
=== 'Boolean');
175 // Returns a value from either the local field defn or the base defn
176 $scope
.getProp = function(propName
) {
177 var path
= propName
.split('.'),
179 localDefn
= drillDown(ctrl
.node
.defn
|| {}, path
);
180 if (typeof localDefn
[item
] !== 'undefined') {
181 return localDefn
[item
];
183 return drillDown(ctrl
.getDefn(), path
)[item
];
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);
192 $scope
.toggleLabel = function() {
193 ctrl
.node
.defn
= ctrl
.node
.defn
|| {};
194 if (ctrl
.node
.defn
.label
=== false) {
195 delete ctrl
.node
.defn
.label
;
197 ctrl
.node
.defn
.label
= false;
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);
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);
215 $scope
.toggleRequired = function() {
216 getSet('required', !getSet('required'));
219 $scope
.toggleHelp = function(position
) {
220 getSet('help_' + position
, $scope
.propIsset('help_' + position
) ? null : (ctrl
.getDefn()['help_' + position
] || ts('Enter text')));
223 function defaultValueShouldBeArray() {
224 return ($scope
.getProp('data_type') !== 'Boolean' &&
225 ($scope
.getProp('input_type') === 'CheckBox' || $scope
.getProp('input_attrs.multiple')));
229 $scope
.toggleDefaultValue = function() {
230 if (ctrl
.hasDefaultValue
) {
231 getSet('afform_default', undefined);
232 ctrl
.hasDefaultValue
= false;
234 ctrl
.hasDefaultValue
= true;
238 $scope
.defaultValueContains = function(val
) {
239 var defaultVal
= getSet('afform_default');
240 return defaultVal
=== val
|| (_
.isArray(defaultVal
) && _
.includes(defaultVal
, val
));
243 $scope
.toggleDefaultValueItem = function(val
) {
244 if (defaultValueShouldBeArray()) {
245 if (!_
.isArray(getSet('afform_default'))) {
246 ctrl
.node
.defn
.afform_default
= [];
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
;
253 ctrl
.node
.defn
.afform_default
.push(val
);
254 ctrl
.hasDefaultValue
= true;
256 } else if (getSet('afform_default') === val
) {
257 getSet('afform_default', undefined);
258 ctrl
.hasDefaultValue
= false;
260 getSet('afform_default', val
);
261 ctrl
.hasDefaultValue
= true;
265 // Getter/setter for definition props
266 $scope
.getSet = function(propName
) {
267 return _
.wrap(propName
, getSet
);
270 // Getter/setter callback
271 function getSet(propName
, val
) {
272 if (arguments
.length
> 1) {
273 var path
= propName
.split('.'),
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
;
281 delete localDefn
[item
];
282 clearOut(ctrl
.node
, ['defn'].concat(path
));
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']);
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']);
295 ctrl
.fieldDefn
= angular
.extend({}, ctrl
.getDefn(), ctrl
.node
.defn
);
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(',');
305 $timeout(function() {
306 ctrl
.hasDefaultValue
= true;
311 return $scope
.getProp(propName
);
313 this.getSet
= getSet
;
315 this.setEditingOptions = function(val
) {
316 $scope
.editingOptions
= val
;
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
];
329 // Returns true only if value is [], {}, '', null, or undefined.
330 function isEmpty(val
) {
331 return typeof val
!== 'boolean' && typeof val
!== 'number' && _
.isEmpty(val
);
334 // Recursively clears out empty arrays and objects
335 function clearOut(parent
, path
) {
337 while (path
.length
&& _
.every(drillDown(parent
, path
), isEmpty
)) {
339 delete drillDown(parent
, path
)[item
];
345 })(angular
, CRM
.$, CRM
._
);