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