Merge pull request #18324 from mattwire/payproc_configoptional
[civicrm-core.git] / templates / CRM / Contact / Form / Search / Builder.js
1 // http://civicrm.org/licensing
2 (function($, CRM, _) {
3 'use strict';
4
5 /* jshint validthis: true */
6 /**
7 * Handle user input - field or operator selection.
8 *
9 * Decide whether to display select drop down, regular text or date
10 * field for the given field and row.
11 */
12 function handleUserInputField() {
13 var row = $(this).closest('tr');
14 var field = $('select[id^=mapper][id$="_1"]', row).val();
15 field = (field === 'world_region') ? 'worldregion_id': field;
16 var operator = $('select[id^=operator]', row);
17 var op = operator.val();
18
19 var patt = /_1$/; // pattern to check if the change event came from field name
20 if (field !== null && patt.test(this.id)) {
21 // based on data type remove invalid operators e.g. IS EMPTY doesn't work with Boolean type column
22 var operators = CRM.searchBuilder.generalOperators;
23 if ((field in CRM.searchBuilder.fieldTypes) === true) {
24 if ($.inArray(CRM.searchBuilder.fieldTypes[field], ['Boolean', 'Int']) > -1) {
25 operators = _.omit(operators, ['IS NOT EMPTY', 'IS EMPTY']);
26 }
27 else if (CRM.searchBuilder.fieldTypes[field] == 'String') {
28 operators = _.omit(operators, ['>', '<', '>=', '<=']);
29 }
30 }
31 buildOperator(operator, operators);
32 }
33
34 // These Ops don't get any input field.
35 var noFieldOps = ['', 'IS EMPTY', 'IS NOT EMPTY', 'IS NULL', 'IS NOT NULL'];
36
37 if ($.inArray(op, noFieldOps) > -1) {
38 // Hide the fields and return.
39 $('.crm-search-value', row).hide().find('input, select').val('');
40 return;
41 }
42 $('.crm-search-value', row).show();
43
44 if (!CRM.searchBuilder.fieldOptions[field]) {
45 removeSelect(row);
46 }
47 else {
48 buildSelect(row, field, op, false);
49 }
50
51 if (CRM.searchBuilder.fieldTypes[field] === 'Date' || CRM.searchBuilder.fieldTypes[field] === 'Timestamp') {
52 buildDate(row, op, CRM.searchBuilder.fieldTypes[field] === 'Timestamp');
53 }
54 else {
55 removeDate(row);
56 }
57 }
58
59 /**
60 * Add appropriate operator to selected field
61 * @param operator: jQuery object
62 * @param options: array
63 */
64 function buildOperator(operator, options) {
65 var selected = operator.val();
66 operator.html('');
67 $.each(options, function(value, label) {
68 operator.append('<option value="' + value + '">' + label + '</option>');
69 });
70 operator.val(selected);
71 }
72
73 /**
74 * Add select list if appropriate for this operation
75 * @param row: jQuery object
76 * @param field: string
77 * @param skip_fetch: boolean
78 */
79 function buildSelect(row, field, op, skip_fetch) {
80 var multiSelect = '';
81 // Operators that will get a single drop down list of choices.
82 var dropDownSingleOps = ['=', '!='];
83 // Multiple select drop down list.
84 var dropDownMultipleOps = ['IN', 'NOT IN'];
85
86 if ($.inArray(op, dropDownMultipleOps) > -1) {
87 multiSelect = 'multiple="multiple"';
88 }
89 else if ($.inArray(op, dropDownSingleOps) < 0) {
90 // If this op is neither supported by single or multiple selects, then we should not render a select list.
91 removeSelect(row);
92 return;
93 }
94
95 $('.crm-search-value select', row).remove();
96 $('input[id^=value]', row)
97 .hide()
98 .after('<select class="crm-form-' + multiSelect.substr(0, 5) + 'select required" ' + multiSelect + '><option value="">' + ts('Loading') + '...</option></select>');
99
100 // Avoid reloading state/county options IF already built, identified by skip_fetch
101 if (skip_fetch) {
102 buildOptions(row, field);
103 }
104 else {
105 fetchOptions(row, field);
106 }
107 }
108
109 /**
110 * Retrieve option list for given row
111 * @param row: jQuery object
112 * @param field: string
113 */
114 function fetchOptions(row, field) {
115 if (CRM.searchBuilder.fieldOptions[field] === 'yesno') {
116 CRM.searchBuilder.fieldOptions[field] = [{key: 1, value: ts('Yes')}, {key: 0, value: ts('No')}];
117 }
118 if (typeof(CRM.searchBuilder.fieldOptions[field]) == 'string') {
119 CRM.api(CRM.searchBuilder.fieldOptions[field], 'getoptions', {field: field, sequential: 1}, {
120 success: function(result, settings) {
121 var field = settings.field;
122 if (result.count) {
123 CRM.searchBuilder.fieldOptions[field] = result.values;
124 buildOptions(settings.row, field);
125 }
126 else {
127 removeSelect(settings.row);
128 }
129 },
130 error: function(result, settings) {
131 removeSelect(settings.row);
132 },
133 row: row,
134 field: field
135 });
136 }
137 else {
138 buildOptions(row, field);
139 }
140 }
141
142 /**
143 * Populate option list for given row
144 * @param row: jQuery object
145 * @param field: string
146 */
147 function buildOptions(row, field) {
148 var select = $('.crm-search-value select', row);
149 var value = $('input[id^=value]', row).val();
150 if (value.length && value.charAt(0) == '(' && value.charAt(value.length - 1) == ')') {
151 value = value.slice(1, -1);
152 }
153 var options = value.split(',');
154 if (select.attr('multiple') == 'multiple') {
155 select.find('option').remove();
156 }
157 else {
158 select.find('option').text(ts('- select -'));
159 if (options.length > 1) {
160 options = [options[0]];
161 }
162 }
163 $.each(CRM.searchBuilder.fieldOptions[field], function(key, option) {
164 var optionKey = option.key;
165 if ($.inArray(field, CRM.searchBuilder.searchByLabelFields) >= 0) {
166 optionKey = option.value;
167 }
168 var selected = ($.inArray(''+optionKey, options) > -1) ? 'selected="selected"' : '';
169 select.append('<option value="' + optionKey + '"' + selected + '>' + option.value + '</option>');
170 });
171 select.change();
172 }
173
174 /**
175 * Remove select options and restore input to a plain textfield
176 * @param row: jQuery object
177 */
178 function removeSelect(row) {
179 $('.crm-search-value input', row).not('.crm-hidden-date').show();
180 $('.crm-search-value select', row).remove();
181 }
182
183 /**
184 * Add a datepicker if appropriate for this operation
185 * @param row: jQuery object
186 */
187 function buildDate(row, op, time) {
188 var input = $('.crm-search-value input', row);
189 // These are operations that should not get a datepicker
190 var datePickerOp = ($.inArray(op, ['IN', 'NOT IN', 'LIKE', 'RLIKE']) < 0);
191 if (!datePickerOp) {
192 removeDate(row);
193 }
194 else if (!$('input.crm-hidden-date', row).length) {
195 // Unfortunately the search builder form expects yyyymmdd and crmDatepicker gives yyyy-mm-dd so we have to fudge it
196 var val = input.val();
197 if (val && val.length === 8) {
198 input.val(val.substr(0, 4) + '-' + val.substr(4, 2) + '-' + val.substr(6, 2));
199 } else if (val && val.length === 14) {
200 input.val(val.substr(0, 4) + '-' + val.substr(4, 2) + '-' + val.substr(6, 2) + ' ' + val.substr(8, 2) + ':' + val.substr(10, 2) + ':' + val.substr(12, 2));
201 }
202 input
203 .on('change.searchBuilder', function() {
204 if ($(this).val()) {
205 $(this).val($(this).val().replace(/[: -]/g, ''));
206 }
207 })
208 .crmDatepicker({
209 time: time,
210 yearRange: '-100:+20'
211 })
212 .triggerHandler('change', ['userInput']);
213 }
214 }
215
216 /**
217 * Remove datepicker
218 * @param row: jQuery object
219 */
220 function removeDate(row) {
221 $('.crm-search-value input.crm-hidden-date', row).off('.searchBuilder').crmDatepicker('destroy');
222 }
223
224 /**
225 * Load and build select options for state IF country is chosen OR county options if state is chosen
226 * @param mapper: string
227 * @param value: integer
228 * @param location_type: integer
229 * @param section: section in which the country/state selection change occurred
230 */
231 function chainSelect(mapper, value, location_type, section) {
232 var apiParams = {
233 sequential: 1,
234 field: (mapper == 'country_id') ? 'state_province' : 'county',
235 };
236 apiParams[mapper] = value;
237 var fieldName = apiParams.field;
238 CRM.api3('address', 'getoptions', apiParams, {
239 success: function(result) {
240 if (result.count) {
241 CRM.searchBuilder.fieldOptions[fieldName] = result.values;
242 $('select[id^=mapper_' + section + '][id$="_1"]').each(function() {
243 var row = $(this).closest('tr');
244 var op = $('select[id^=operator]', row).val();
245 if ($(this).val() === fieldName && location_type === $('select[id^=mapper][id$="_2"]', row).val()) {
246 buildSelect(row, fieldName, op, true);
247 }
248 });
249 }
250 }
251 });
252 }
253
254 // Initialize display: Hide empty blocks & fields
255 var newBlock = CRM.searchBuilder && CRM.searchBuilder.newBlock || 0;
256 function initialize() {
257 $('.crm-search-block', '#Builder').each(function(blockNo) {
258 var block = $(this);
259 var empty = blockNo + 1 > newBlock;
260 var skippedRow = false;
261 $('tr:not(.crm-search-builder-add-row)', block).each(function(rowNo) {
262 var row = $(this);
263 if ($('select:first', row).val() === '') {
264 if (!skippedRow && (rowNo === 0 || blockNo + 1 == newBlock)) {
265 skippedRow = true;
266 }
267 else {
268 row.hide();
269 }
270 }
271 else {
272 empty = false;
273 }
274 });
275 if (empty) {
276 block.hide();
277 }
278 });
279 }
280
281 $(function($) {
282 $('#crm-main-content-wrapper')
283 // Reset and hide row
284 .on('click', '.crm-reset-builder-row', function() {
285 var row = $(this).closest('tr');
286 $('input, select', row).val('').change();
287 row.hide();
288 // Hide entire block if this is the only visible row
289 if (row.siblings(':visible').length < 2) {
290 row.closest('.crm-search-block').hide();
291 }
292 return false;
293 })
294 // Add new field - if there's a hidden one, show it
295 // Otherwise allow form to submit and fetch more from the server
296 .on('click', 'button[name^=addMore]', function() {
297 var table = $(this).closest('table');
298 if ($('tr:hidden', table).length) {
299 $('tr:hidden', table).first().show();
300 return false;
301 }
302 })
303 // Add new block - if there's a hidden one, show it
304 // Otherwise allow form to submit and fetch more from the server
305 .on('click', '#addBlock', function() {
306 if ($('.crm-search-block:hidden', '#Builder').length) {
307 var block = $('.crm-search-block:hidden', '#Builder').first();
308 block.show();
309 $('tr:first-child, tr.crm-search-builder-add-row', block).show();
310 return false;
311 }
312 })
313 // Handle field and operator selection
314 .on('change', 'select[id^=mapper][id$="_1"], select[id^=operator]', handleUserInputField)
315 // Handle option selection - update hidden value field
316 .on('change', '.crm-search-value select', function() {
317 var value = $(this).val() || '';
318 if ($(this).attr('multiple') == 'multiple' && value.length) {
319 value = value.join(',');
320 }
321 $(this).siblings('input').val(value);
322 if (value !== '') {
323 var mapper = $('#' + $(this).siblings('input').attr('id').replace('value_', 'mapper_') + '_1').val();
324 var location_type = $('#' + $(this).siblings('input').attr('id').replace('value_', 'mapper_') + '_2').val();
325 var section = $(this).siblings('input').attr('id').replace('value_', '').split('_')[0];
326 if ($.inArray(mapper, ['state_province', 'country']) > -1) {
327 chainSelect(mapper + '_id', value, location_type, section);
328 }
329 }
330 })
331 .on('crmLoad', function() {
332 initialize();
333 $('select[id^=mapper][id$="_1"]', '#Builder').each(handleUserInputField);
334 });
335
336 initialize();
337
338 // Fetch initial options during page refresh - it's more efficient to bundle them in a single ajax request
339 var initialFields = {}, fetchFields = false;
340 $('select[id^=mapper][id$="_1"] option:selected', '#Builder').each(function() {
341 var field = $(this).attr('value');
342 if (typeof(CRM.searchBuilder.fieldOptions[field]) == 'string' && CRM.searchBuilder.fieldOptions[field] !== 'yesno') {
343 initialFields[field] = [CRM.searchBuilder.fieldOptions[field], 'getoptions', {field: field, sequential: 1}];
344 fetchFields = true;
345 }
346 });
347 if (fetchFields) {
348 CRM.api3(initialFields).done(function(data) {
349 $.each(data, function(field, result) {
350 CRM.searchBuilder.fieldOptions[field] = result.values;
351 });
352 $('select[id^=mapper][id$="_1"]', '#Builder').each(handleUserInputField);
353 });
354 } else {
355 $('select[id^=mapper][id$="_1"]', '#Builder').each(handleUserInputField);
356 }
357 });
358 })(cj, CRM, CRM._);