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