Merge pull request #22087 from agileware/CIVICRM-1887
[civicrm-core.git] / js / Common.js
CommitLineData
353ea873 1// https://civicrm.org/licensing
2b3ddf6e 2/* global CRM:true */
b7f93874 3var CRM = CRM || {};
4b513f23
CW
4var cj = CRM.$ = jQuery;
5CRM._ = _;
6a488035
TO
6
7/**
8 * Short-named function for string translation, defined in global scope so it's available everywhere.
9 *
353ea873
CW
10 * @param text string for translating
11 * @param params object key:value of additional parameters
6a488035 12 *
353ea873 13 * @return string
6a488035
TO
14 */
15function ts(text, params) {
7553cf23 16 "use strict";
3f3bba82
TO
17 var d = (params && params.domain) ? ('strings::' + params.domain) : null;
18 if (d && CRM[d] && CRM[d][text]) {
19 text = CRM[d][text];
20 }
0d75c29c
CW
21 else if (CRM.strings[text]) {
22 text = CRM.strings[text];
3f3bba82 23 }
2788147f 24 if (typeof(params) === 'object') {
6a488035 25 for (var i in params) {
32155ad6 26 if (typeof(params[i]) === 'string' || typeof(params[i]) === 'number') {
2788147f 27 // sprintf emulation: escape % characters in the replacements to avoid conflicts
32155ad6 28 text = text.replace(new RegExp('%' + i, 'g'), String(params[i]).replace(/%/g, '%-crmescaped-'));
2788147f 29 }
6a488035
TO
30 }
31 return text.replace(/%-crmescaped-/g, '%');
32 }
33 return text;
34}
35
2b3ddf6e
CW
36// Legacy code - ignore warnings
37/* jshint ignore:start */
38
6a488035
TO
39/**
40 * This function is called by default at the bottom of template files which have forms that have
41 * conditionally displayed/hidden sections and elements. The PHP is responsible for generating
42 * a list of 'blocks to show' and 'blocks to hide' and the template passes these parameters to
43 * this function.
44 *
353ea873 45 * @deprecated
6a488035
TO
46 * @param showBlocks Array of element Id's to be displayed
47 * @param hideBlocks Array of element Id's to be hidden
48 * @param elementType Value to set display style to for showBlocks (e.g. 'block' or 'table-row' or ...)
6a488035 49 */
0f5816a6 50function on_load_init_blocks(showBlocks, hideBlocks, elementType) {
2b3ddf6e 51 if (elementType == null) {
f54254d8 52 elementType = 'block';
0f5816a6
KJ
53 }
54
f54254d8
TO
55 var myElement, i;
56
0f5816a6 57 /* This loop is used to display the blocks whose IDs are present within the showBlocks array */
f54254d8
TO
58 for (i = 0; i < showBlocks.length; i++) {
59 myElement = document.getElementById(showBlocks[i]);
0f5816a6 60 /* getElementById returns null if element id doesn't exist in the document */
2b3ddf6e 61 if (myElement != null) {
0f5816a6 62 myElement.style.display = elementType;
6a488035 63 }
0f5816a6
KJ
64 else {
65 alert('showBlocks array item not in .tpl = ' + showBlocks[i]);
66 }
67 }
6a488035 68
0f5816a6 69 /* This loop is used to hide the blocks whose IDs are present within the hideBlocks array */
f54254d8
TO
70 for (i = 0; i < hideBlocks.length; i++) {
71 myElement = document.getElementById(hideBlocks[i]);
0f5816a6 72 /* getElementById returns null if element id doesn't exist in the document */
2b3ddf6e 73 if (myElement != null) {
0f5816a6
KJ
74 myElement.style.display = 'none';
75 }
76 else {
77 alert('showBlocks array item not in .tpl = ' + hideBlocks[i]);
6a488035 78 }
0f5816a6 79 }
6a488035
TO
80}
81
82/**
83 * This function is called when we need to show or hide a related form element (target_element)
84 * based on the value (trigger_value) of another form field (trigger_field).
85 *
353ea873 86 * @deprecated
6a488035
TO
87 * @param trigger_field_id HTML id of field whose onchange is the trigger
88 * @param trigger_value List of integers - option value(s) which trigger show-element action for target_field
89 * @param target_element_id HTML id of element to be shown or hidden
90 * @param target_element_type Type of element to be shown or hidden ('block' or 'table-row')
91 * @param field_type Type of element radio/select
92 * @param invert Boolean - if true, we HIDE target on value match; if false, we SHOW target on value match
0f5816a6
KJ
93 */
94function showHideByValue(trigger_field_id, trigger_value, target_element_id, target_element_type, field_type, invert) {
f54254d8 95 var target, j;
0f5816a6
KJ
96
97 if (field_type == 'select') {
98 var trigger = trigger_value.split("|");
24cc4545 99 var selectedOptionValue = cj('#' + trigger_field_id).val();
0f5816a6 100
f54254d8
TO
101 target = target_element_id.split("|");
102 for (j = 0; j < target.length; j++) {
0f5816a6
KJ
103 if (invert) {
104 cj('#' + target[j]).show();
105 }
106 else {
107 cj('#' + target[j]).hide();
108 }
109 for (var i = 0; i < trigger.length; i++) {
110 if (selectedOptionValue == trigger[i]) {
111 if (invert) {
112 cj('#' + target[j]).hide();
113 }
114 else {
115 cj('#' + target[j]).show();
116 }
6a488035 117 }
0f5816a6
KJ
118 }
119 }
6a488035 120
0f5816a6
KJ
121 }
122 else {
123 if (field_type == 'radio') {
f54254d8
TO
124 target = target_element_id.split("|");
125 for (j = 0; j < target.length; j++) {
5fb3af09 126 if (cj('[name="' + trigger_field_id + '"]:first').is(':checked')) {
0f5816a6
KJ
127 if (invert) {
128 cj('#' + target[j]).hide();
129 }
130 else {
131 cj('#' + target[j]).show();
132 }
6a488035 133 }
0f5816a6
KJ
134 else {
135 if (invert) {
136 cj('#' + target[j]).show();
137 }
138 else {
139 cj('#' + target[j]).hide();
140 }
141 }
142 }
6a488035 143 }
0f5816a6 144 }
6a488035
TO
145}
146
423616fa 147var submitcount = 0;
6a488035 148
6a488035
TO
149/**
150 * Function to show / hide the row in optionFields
353ea873
CW
151 * @deprecated
152 * @param index string, element whose innerHTML is to hide else will show the hidden row.
6a488035 153 */
0f5816a6
KJ
154function showHideRow(index) {
155 if (index) {
156 cj('tr#optionField_' + index).hide();
157 if (cj('table#optionField tr:hidden:first').length) {
158 cj('div#optionFieldLink').show();
6a488035 159 }
0f5816a6
KJ
160 }
161 else {
162 cj('table#optionField tr:hidden:first').show();
163 if (!cj('table#optionField tr:hidden:last').length) {
164 cj('div#optionFieldLink').hide();
165 }
166 }
167 return false;
6a488035
TO
168}
169
2b3ddf6e
CW
170/* jshint ignore:end */
171
1711f01c
CW
172if (!CRM.utils) CRM.utils = {};
173if (!CRM.strings) CRM.strings = {};
174if (!CRM.vars) CRM.vars = {};
6a488035 175
4b513f23 176(function ($, _, undefined) {
7553cf23 177 "use strict";
0d75c29c 178 /* jshint validthis: true */
d664f648 179
3f586963
CW
180 // Theme classes for unattached elements
181 $.fn.select2.defaults.dropdownCssClass = $.ui.dialog.prototype.options.dialogClass = 'crm-container';
182
47f21f3a
CW
183 // https://github.com/ivaynberg/select2/pull/2090
184 $.fn.select2.defaults.width = 'resolve';
185
e20523a8
CW
186 // Workaround for https://github.com/ivaynberg/select2/issues/1246
187 $.ui.dialog.prototype._allowInteraction = function(e) {
dbf67e1e 188 return !!$(e.target).closest('.ui-dialog, .ui-datepicker, .select2-drop, .cke_dialog, .ck-balloon-panel, #civicrm-menu').length;
e20523a8
CW
189 };
190
9f8862e1
CW
191 // Implements jQuery hook.prop
192 $.propHooks.disabled = {
193 set: function (el, value, name) {
194 // Sync button enabled status with wrapper css
15ef6755 195 if ($(el).is('.crm-button.crm-form-submit')) {
9f8862e1
CW
196 $(el).parent().toggleClass('crm-button-disabled', !!value);
197 }
198 // Sync button enabled status with dialog button
199 if ($(el).is('.ui-dialog input.crm-form-submit')) {
9bce560a
CW
200 $(el).closest('.ui-dialog').find('.ui-dialog-buttonset button[data-identifier='+ $(el).attr('name') +']').prop('disabled', value);
201 }
202 if ($(el).is('.crm-form-date-wrapper .crm-hidden-date')) {
203 $(el).siblings().prop('disabled', value);
9f8862e1
CW
204 }
205 }
206 };
207
286a7e5a 208 var scriptsLoaded = {};
2bdce12c 209 CRM.loadScript = function(url, appendCacheCode) {
286a7e5a 210 if (!scriptsLoaded[url]) {
2bdce12c
CW
211 var script = document.createElement('script'),
212 src = url;
213 if (appendCacheCode !== false) {
214 src += (_.includes(url, '?') ? '&r=' : '?r=') + CRM.config.resourceCacheCode;
215 }
286a7e5a
CW
216 scriptsLoaded[url] = $.Deferred();
217 script.onload = function () {
0d730261
CW
218 // Give the script time to execute
219 window.setTimeout(function () {
220 if (window.jQuery === CRM.$ && CRM.CMSjQuery) {
221 window.jQuery = CRM.CMSjQuery;
222 }
223 scriptsLoaded[url].resolve();
224 }, 100);
286a7e5a 225 };
30b7642c
CW
226 // Make jQuery global available while script is loading
227 if (window.jQuery !== CRM.$) {
228 CRM.CMSjQuery = window.jQuery;
229 window.jQuery = CRM.$;
230 }
2bdce12c 231 script.src = src;
286a7e5a
CW
232 document.getElementsByTagName("head")[0].appendChild(script);
233 }
234 return scriptsLoaded[url];
235 };
236
475e9f44
CW
237 /**
238 * Populate a select list, overwriting the existing options except for the placeholder.
581c7be2 239 * @param select jquery selector - 1 or more select elements
475e9f44 240 * @param options array in format returned by api.getoptions
b7ceb253
CW
241 * @param placeholder string|bool - new placeholder or false (default) to keep the old one
242 * @param value string|array - will silently update the element with new value without triggering change
475e9f44 243 */
b7ceb253 244 CRM.utils.setOptions = function(select, options, placeholder, value) {
581c7be2 245 $(select).each(function() {
475e9f44
CW
246 var
247 $elect = $(this),
b7ceb253
CW
248 val = value || $elect.val() || [],
249 opts = placeholder || placeholder === '' ? '' : '[value!=""]';
23e8a31b 250 $elect.find('option' + opts).remove();
b7ceb253 251 var newOptions = CRM.utils.renderOptions(options, val);
a9442e6e
SL
252 if (options.length == 0) {
253 $elect.removeClass('required');
254 } else if ($elect.hasClass('crm-field-required') && !$elect.hasClass('required')) {
255 $elect.addClass('required');
256 }
1d07e7ab 257 if (typeof placeholder === 'string') {
581c7be2
CW
258 if ($elect.is('[multiple]')) {
259 select.attr('placeholder', placeholder);
1d07e7ab
CW
260 } else {
261 newOptions = '<option value="">' + placeholder + '</option>' + newOptions;
262 }
263 }
264 $elect.append(newOptions);
b7ceb253
CW
265 if (!value) {
266 $elect.trigger('crmOptionsUpdated', $.extend({}, options)).trigger('change');
267 }
475e9f44
CW
268 });
269 };
270
b7ceb253
CW
271 /**
272 * Render an option list
8decea37
CW
273 * @param options {array}
274 * @param val {string} default value
275 * @param escapeHtml {bool}
b7ceb253
CW
276 * @return string
277 */
8decea37
CW
278 CRM.utils.renderOptions = function(options, val, escapeHtml) {
279 var rendered = '',
280 esc = escapeHtml === false ? _.identity : _.escape;
b7ceb253
CW
281 if (!$.isArray(val)) {
282 val = [val];
283 }
284 _.each(options, function(option) {
285 if (option.children) {
8decea37 286 rendered += '<optgroup label="' + esc(option.value) + '">' +
52e6588d
CW
287 CRM.utils.renderOptions(option.children, val) +
288 '</optgroup>';
b7ceb253
CW
289 } else {
290 var selected = ($.inArray('' + option.key, val) > -1) ? 'selected="selected"' : '';
8decea37 291 rendered += '<option value="' + esc(option.key) + '"' + selected + '>' + esc(option.value) + '</option>';
b7ceb253 292 }
475e9f44 293 });
b7ceb253 294 return rendered;
475e9f44
CW
295 };
296
9b85cd33
CW
297 CRM.utils.getOptions = function(select) {
298 var options = [];
299 $('option', select).each(function() {
300 var option = {key: $(this).attr('value'), value: $(this).text()};
301 if (option.key !== '') {
302 options.push(option);
303 }
304 });
305 return options;
306 };
307
1d07e7ab
CW
308 function chainSelect() {
309 var $form = $(this).closest('form'),
310 $target = $('select[data-name="' + $(this).data('target') + '"]', $form),
311 data = $target.data(),
312 val = $(this).val();
313 $target.prop('disabled', true);
314 if ($target.is('select.crm-chain-select-control')) {
315 $('select[data-name="' + $target.data('target') + '"]', $form).prop('disabled', true).blur();
316 }
317 if (!(val && val.length)) {
318 CRM.utils.setOptions($target.blur(), [], data.emptyPrompt);
319 } else {
320 $target.addClass('loading');
321 $.getJSON(CRM.url(data.callback), {_value: val}, function(vals) {
322 $target.prop('disabled', false).removeClass('loading');
323 CRM.utils.setOptions($target, vals || [], (vals && vals.length ? data.selectPrompt : data.nonePrompt));
324 });
325 }
326 }
327
52e6588d
CW
328 /**
329 * Compare Form Input values against cached initial value.
330 *
331 * @return {Boolean} true if changes have been made.
332 */
3e201321 333 CRM.utils.initialValueChanged = function(el) {
88e9380e 334 var isDirty = false;
d4fa3633 335 $(':input:visible, .select2-container:visible+:input:hidden', el).not('[type=submit], [type=button], .crm-action-menu, :disabled').each(function () {
603f899a
CW
336 var
337 initialValue = $(this).data('crm-initial-value'),
338 currentValue = $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val();
3c0624e2 339 // skip change of value for submit buttons
603f899a 340 if (initialValue !== undefined && !_.isEqual(initialValue, currentValue)) {
88e9380e
CW
341 isDirty = true;
342 }
3e201321 343 });
344 return isDirty;
8d36b801 345 };
52e6588d 346
b1fc510d
CW
347 /**
348 * This provides defaults for ui.dialog which either need to be calculated or are different from global defaults
349 *
350 * @param settings
351 * @returns {*}
352 */
353 CRM.utils.adjustDialogDefaults = function(settings) {
354 settings = $.extend({width: '65%', height: '65%', modal: true}, settings || {});
355 // Support relative height
356 if (typeof settings.height === 'string' && settings.height.indexOf('%') > 0) {
357 settings.height = parseInt($(window).height() * (parseFloat(settings.height)/100), 10);
358 }
359 // Responsive adjustment - increase percent width on small screens
360 if (typeof settings.width === 'string' && settings.width.indexOf('%') > 0) {
361 var screenWidth = $(window).width(),
362 percentage = parseInt(settings.width.replace('%', ''), 10),
363 gap = 100-percentage;
364 if (screenWidth < 701) {
365 settings.width = '100%';
366 }
367 else if (screenWidth < 1400) {
368 settings.width = '' + parseInt(percentage+gap-((screenWidth - 700)/7*(gap)/100), 10) + '%';
369 }
370 }
441b1700
CW
371 if (settings.dialogClass && !_.includes(settings.dialogClass, 'crm-container')) {
372 settings.dialogClass += ' crm-container';
373 }
b1fc510d
CW
374 return settings;
375 };
52e6588d 376
00c2ff9e 377 function formatCrmSelect2(row) {
b733747a
CW
378 var icon = row.icon || $(row.element).data('icon'),
379 color = row.color || $(row.element).data('color'),
380 description = row.description || $(row.element).data('description'),
381 ret = '';
382 if (icon) {
ba8bb6ee 383 ret += '<i class="crm-i ' + icon + '" aria-hidden="true"></i> ';
b733747a
CW
384 }
385 if (color) {
386 ret += '<span class="crm-select-item-color" style="background-color: ' + color + '"></span> ';
387 }
388 return ret + _.escape(row.text) + (description ? '<div class="crm-select2-row-description"><p>' + _.escape(description) + '</p></div>' : '');
00c2ff9e
CW
389 }
390
bc37053c
AH
391 /**
392 * Helper to generate an icon with alt text.
393 *
394 * See also smarty `{icon}` and CRM_Core_Page::crmIcon() functions
395 *
396 * @param string icon
397 * The Font Awesome icon class to use.
398 * @param string text
399 * Alt text to display.
400 * @param mixed condition
401 * This will only display if this is truthy.
402 *
403 * @return string
404 * The formatted icon markup.
405 */
fb50811e
AH
406 CRM.utils.formatIcon = function (icon, text, condition) {
407 if (typeof condition !== 'undefined' && !condition) {
bc37053c
AH
408 return '';
409 }
410 var title = '';
411 var sr = '';
412 if (text) {
bc1a7648 413 text = _.escape(text);
bc37053c
AH
414 title = ' title="' + text + '"';
415 sr = '<span class="sr-only">' + text + '</span>';
416 }
ba8bb6ee 417 return '<i class="crm-i ' + icon + '"' + title + ' aria-hidden="true"></i>' + sr;
bc37053c
AH
418 };
419
ba4fb2b2 420 /**
353ea873 421 * Wrapper for select2 initialization function; supplies defaults
a88cf11a 422 * @param options object
ba4fb2b2 423 */
a88cf11a 424 $.fn.crmSelect2 = function(options) {
4cd02904
CW
425 if (options === 'destroy') {
426 return $(this).each(function() {
427 $(this)
428 .removeClass('crm-ajax-select')
be677a1c 429 .off('.crmSelect2')
4cd02904
CW
430 .select2('destroy');
431 });
432 }
a88cf11a
CW
433 return $(this).each(function () {
434 var
435 $el = $(this),
9597c394 436 iconClass,
00c2ff9e
CW
437 settings = {
438 allowClear: !$el.hasClass('required'),
439 formatResult: formatCrmSelect2,
440 formatSelection: formatCrmSelect2
441 };
704a585b 442
a88cf11a 443 // quickform doesn't support optgroups so here's a hack :(
8a5929be
RT
444 // Instead of using wrapAll or similar that repeatedly appends options to the group and redraw the page (=> very slow on large lists),
445 // build bulk HTML and insert in single shot
704a585b 446 var optGroups = {};
a88cf11a 447 $('option[value^=crm_optgroup]', this).each(function () {
704a585b
RT
448 var groupHtml = '';
449 $(this).nextUntil('option[value^=crm_optgroup]').each(function () {
450 groupHtml += this.outerHTML;
451 });
452 optGroups[$(this).text()] = groupHtml;
a88cf11a
CW
453 $(this).remove();
454 });
704a585b
RT
455 var replacedHtml = '';
456 for (var groupLabel in optGroups) {
457 replacedHtml += '<optgroup label="' + groupLabel + '">' + optGroups[groupLabel] + '</optgroup>';
458 }
459 if (replacedHtml) {
460 $el.html(replacedHtml);
461 }
47358d92 462
463 // quickform does not support disabled option, so yet another hack to
464 // add disabled property for option values
711da13f 465 $('option[value^=crm_disabled_opt]', this).attr('disabled', 'disabled');
27a6b676 466
8c3a97ed 467 // Placeholder icon - total hack hikacking the escapeMarkup function but select2 3.5 dosn't have any other callbacks for this :(
9597c394 468 if ($el.is('[class*=fa-]')) {
8c3a97ed
CW
469 settings.escapeMarkup = function (m) {
470 var out = _.escape(m),
471 placeholder = settings.placeholder || $el.data('placeholder') || $el.attr('placeholder') || $('option[value=""]', $el).text();
472 if (m.length && placeholder === m) {
473 iconClass = $el.attr('class').match(/(fa-\S*)/)[1];
ba8bb6ee 474 out = '<i class="crm-i ' + iconClass + '" aria-hidden="true"></i> ' + out;
8c3a97ed
CW
475 }
476 return out;
477 };
9597c394
CW
478 }
479
ca914657
CW
480 $el
481 .on('select2-loaded.crmSelect2', function() {
482 // Use description as title for each option
483 $('.crm-select2-row-description', '#select2-drop').each(function() {
484 $(this).closest('.select2-result-label').attr('title', $(this).text());
485 });
486 // Collapsible optgroups should be expanded when searching
487 if ($('#select2-drop.collapsible-optgroups-enabled .select2-search input.select2-input').val()) {
488 $('#select2-drop.collapsible-optgroups-enabled li.select2-result-with-children')
489 .addClass('optgroup-expanded');
490 }
491 })
492 // Handle collapsible optgroups
493 .on('select2-open', function(e) {
494 var isCollapsible = $(e.target).hasClass('collapsible-optgroups');
495 $('#select2-drop')
496 .off('.collapseOptionGroup')
497 .toggleClass('collapsible-optgroups-enabled', isCollapsible);
498 if (isCollapsible) {
499 $('#select2-drop')
500 .on('click.collapseOptionGroup', '.select2-result-with-children > .select2-result-label', function() {
501 $(this).parent().toggleClass('optgroup-expanded');
502 })
503 // If the first item in the list is an optgroup, expand it
504 .find('li.select2-result-with-children:first-child').addClass('optgroup-expanded');
505 }
506 })
507 .on('select2-close', function() {
508 $('#select2-drop').off('.collapseOptionGroup').removeClass('collapsible-optgroups-enabled');
be677a1c 509 });
be677a1c 510
a88cf11a
CW
511 // Defaults for single-selects
512 if ($el.is('select:not([multiple])')) {
a243158e 513 settings.minimumResultsForSearch = 10;
a88cf11a 514 if ($('option:first', this).val() === '') {
a243158e 515 settings.placeholderOption = 'first';
a88cf11a 516 }
ba4fb2b2 517 }
a243158e
CW
518 $.extend(settings, $el.data('select-params') || {}, options || {});
519 if (settings.ajax) {
520 $el.addClass('crm-ajax-select');
521 }
522 $el.select2(settings);
a88cf11a
CW
523 });
524 };
525
526 /**
353ea873 527 * @see CRM_Core_Form::addEntityRef for docs
a88cf11a
CW
528 * @param options object
529 */
530 $.fn.crmEntityRef = function(options) {
4cd02904
CW
531 if (options === 'destroy') {
532 return $(this).each(function() {
533 var entity = $(this).data('api-entity') || '';
534 $(this)
535 .off('.crmEntity')
2229cf4f 536 .removeClass('crm-form-entityref crm-' + _.kebabCase(entity) + '-ref')
4cd02904
CW
537 .crmSelect2('destroy');
538 });
539 }
a88cf11a
CW
540 options = options || {};
541 options.select = options.select || {};
542 return $(this).each(function() {
543 var
4c993609 544 $el = $(this).off('.crmEntity'),
2229cf4f 545 entity = options.entity || $el.data('api-entity') || 'Contact',
7a4cf127
CW
546 selectParams = {},
547 staticPresets = {
548 user_contact_id: {
549 id: 'user_contact_id',
550 label: ts('Select Current User'),
551 icon: 'fa-user-circle-o'
552 }
553 };
2229cf4f
CW
554 // Legacy: fix entity name if passed in as snake case
555 if (entity.charAt(0).toUpperCase() !== entity.charAt(0)) {
556 entity = _.capitalize(_.camelCase(entity));
557 }
a88cf11a
CW
558 $el.data('api-entity', entity);
559 $el.data('select-params', $.extend({}, $el.data('select-params') || {}, options.select));
0ed35ef8 560 $el.data('api-params', $.extend(true, {}, $el.data('api-params') || {}, options.api));
a4799f04 561 $el.data('create-links', options.create || $el.data('create-links'));
7a4cf127
CW
562 var staticItems = options.static || $el.data('static') || [];
563 _.each(staticItems, function(option, i) {
564 if (_.isString(option)) {
565 staticItems[i] = staticPresets[option];
566 }
567 });
568
569 function staticItemMarkup() {
570 if (!staticItems.length) {
571 return '';
572 }
573 var markup = '<div class="crm-entityref-links crm-entityref-links-static">';
574 _.each(staticItems, function(link) {
575 markup += ' <a class="crm-hover-button" href="#' + link.id + '">' +
576 '<i class="crm-i ' + link.icon + '" aria-hidden="true"></i> ' +
577 _.escape(link.label) + '</a>';
578 });
579 markup += '</div>';
580 return markup;
581 }
582
2229cf4f 583 $el.addClass('crm-form-entityref crm-' + _.kebabCase(entity) + '-ref');
ba4fb2b2 584 var settings = {
b7ceb253 585 // Use select2 ajax helper instead of CRM.api3 because it provides more value
ba4fb2b2
CW
586 ajax: {
587 url: CRM.url('civicrm/ajax/rest'),
3d06f63c 588 quietMillis: 300,
ba4fb2b2 589 data: function (input, page_num) {
b7ceb253 590 var params = getEntityRefApiParams($el);
ba4fb2b2
CW
591 params.input = input;
592 params.page_num = page_num;
593 return {
594 entity: $el.data('api-entity'),
595 action: 'getlist',
596 json: JSON.stringify(params)
597 };
598 },
599 results: function(data) {
600 return {more: data.more_results, results: data.values || []};
601 }
602 },
a88cf11a 603 minimumInputLength: 1,
3c0b6a40 604 formatResult: CRM.utils.formatSelect2Result,
4fcc9877 605 formatSelection: formatEntityRefSelection,
7a5edd0a 606 escapeMarkup: _.identity,
a88cf11a
CW
607 initSelection: function($el, callback) {
608 var
609 multiple = !!$el.data('select-params').multiple,
610 val = $el.val(),
7a4cf127 611 stored = ($el.data('entity-value') || []).concat(staticItems);
a88cf11a
CW
612 if (val === '') {
613 return;
614 }
7a4cf127
CW
615 var idsNeeded = _.difference(val.split(','), _.pluck(stored, 'id'));
616 var existing = _.remove(stored, function(item) {
617 return _.includes(val.split(','), item.id);
618 });
a88cf11a 619 // If we already have this data, just return it
7a4cf127
CW
620 if (!idsNeeded.length) {
621 callback(multiple ? existing : existing[0]);
a88cf11a 622 } else {
7a4cf127 623 var params = $.extend({}, $el.data('api-params') || {}, {id: idsNeeded.join(',')});
a88cf11a 624 CRM.api3($el.data('api-entity'), 'getlist', params).done(function(result) {
7a4cf127 625 callback(multiple ? result.values.concat(existing) : result.values[0]);
d2b4810b
CW
626 // Trigger change (store data to avoid an infinite loop of lookups)
627 $el.data('entity-value', result.values).trigger('change');
a88cf11a
CW
628 });
629 }
ba4fb2b2
CW
630 }
631 };
4c993609 632 // Create new items inline - works for tags
2229cf4f 633 if ($el.data('create-links') && entity === 'Tag') {
4c993609
CW
634 selectParams.createSearchChoice = function(term, data) {
635 if (!_.findKey(data, {label: term})) {
636 return {id: "0", term: term, label: term + ' (' + ts('new tag') + ')'};
637 }
638 };
e4f4dc22 639 selectParams.tokenSeparators = [','];
4c993609 640 selectParams.createSearchChoicePosition = 'bottom';
05b21c58 641 $el.on('select2-selecting.crmEntity', function(e) {
4c993609 642 if (e.val === "0") {
a2c28a94 643 // Create a new term
4c993609
CW
644 e.object.label = e.object.term;
645 CRM.api3(entity, 'create', $.extend({name: e.object.term}, $el.data('api-params').params || {}))
646 .done(function(created) {
647 var
4c993609
CW
648 val = $el.select2('val'),
649 data = $el.select2('data'),
650 item = {id: created.id, label: e.object.term};
651 if (val === "0") {
e4f4dc22 652 $el.select2('data', item, true);
4c993609
CW
653 }
654 else if ($.isArray(val) && $.inArray("0", val) > -1) {
655 _.remove(data, {id: "0"});
656 data.push(item);
e4f4dc22 657 $el.select2('data', data, true);
4c993609
CW
658 }
659 });
660 }
661 });
b7ceb253
CW
662 }
663 else {
a88cf11a
CW
664 selectParams.formatInputTooShort = function() {
665 var txt = $el.data('select-params').formatInputTooShort || $.fn.select2.defaults.formatInputTooShort.call(this);
7a4cf127 666 txt += entityRefFiltersMarkup($el) + staticItemMarkup() + renderEntityRefCreateLinks($el);
ba4fb2b2
CW
667 return txt;
668 };
a88cf11a 669 selectParams.formatNoMatches = function() {
ba4fb2b2 670 var txt = $el.data('select-params').formatNoMatches || $.fn.select2.defaults.formatNoMatches;
fd7c068f 671 txt += entityRefFiltersMarkup($el) + renderEntityRefCreateLinks($el);
b7ceb253 672 return txt;
ba4fb2b2 673 };
4c993609 674 $el.on('select2-open.crmEntity', function() {
ba4fb2b2 675 var $el = $(this);
b7ceb253
CW
676 $('#select2-drop')
677 .off('.crmEntity')
678 .on('click.crmEntity', 'a.crm-add-entity', function(e) {
882a1be2 679 var extra = $el.data('api-params').extra,
2b63c577 680 formUrl = $(this).attr('href') + '&returnExtra=display_name,sort_name' + (extra ? (',' + extra) : '');
b7ceb253 681 $el.select2('close');
882a1be2 682 CRM.loadForm(formUrl, {
8dbd6052 683 dialog: {width: '50%', height: 220}
b7ceb253
CW
684 }).on('crmFormSuccess', function(e, data) {
685 if (data.status === 'success' && data.id) {
8dbd6052
CW
686 if (!data.crmMessages) {
687 CRM.status(ts('%1 Created', {1: data.label || data.extra.display_name}));
688 }
689 data.label = data.label || data.extra.sort_name;
b7ceb253
CW
690 if ($el.select2('container').hasClass('select2-container-multi')) {
691 var selection = $el.select2('data');
692 selection.push(data);
693 $el.select2('data', selection, true);
694 } else {
695 $el.select2('data', data, true);
696 }
c92f6436 697 }
b7ceb253
CW
698 });
699 return false;
700 })
7a4cf127
CW
701 .on('click.crmEntity', '.crm-entityref-links-static a', function(e) {
702 var id = $(this).attr('href').substr(1),
703 item = _.findWhere(staticItems, {id: id});
704 $el.select2('close');
705 if ($el.select2('container').hasClass('select2-container-multi')) {
706 var selection = $el.select2('data');
707 if (!_.findWhere(selection, {id: id})) {
708 selection.push(item);
709 $el.select2('data', selection, true);
710 }
711 } else {
712 $el.select2('data', item, true);
713 }
714 return false;
715 })
fd7c068f 716 .on('change.crmEntity', '.crm-entityref-filter-value', function() {
b7ceb253
CW
717 var filter = $el.data('user-filter') || {};
718 filter.value = $(this).val();
719 $(this).toggleClass('active', !!filter.value);
720 $el.data('user-filter', filter);
0943a2d8 721 if (filter.value && $(this).is('select')) {
b7ceb253
CW
722 // Once a filter has been chosen, rerender create links and refocus the search box
723 $el.select2('close');
724 $el.select2('open');
38558375 725 } else {
7a4cf127 726 $('.crm-entityref-links-create', '#select2-drop').replaceWith(renderEntityRefCreateLinks($el));
ba4fb2b2 727 }
b7ceb253
CW
728 })
729 .on('change.crmEntity', 'select.crm-entityref-filter-key', function() {
38558375 730 var filter = {key: $(this).val()};
b7ceb253
CW
731 $(this).toggleClass('active', !!filter.key);
732 $el.data('user-filter', filter);
fd7c068f
CW
733 renderEntityRefFilterValue($el);
734 $('.crm-entityref-filter-key', '#select2-drop').focus();
ba4fb2b2 735 });
ba4fb2b2
CW
736 });
737 }
05b21c58 738 $el.crmSelect2($.extend(settings, $el.data('select-params'), selectParams));
a88cf11a 739 });
ba4fb2b2
CW
740 };
741
b7ceb253
CW
742 /**
743 * Combine api-params with user-filter
744 * @param $el
745 * @returns {*}
746 */
747 function getEntityRefApiParams($el) {
748 var
749 params = $.extend({params: {}}, $el.data('api-params') || {}),
750 // Prevent original data from being modified - $.extend and _.clone don't cut it, they pass nested objects by reference!
751 combined = _.cloneDeep(params),
752 filter = $.extend({}, $el.data('user-filter') || {});
753 if (filter.key && filter.value) {
06606cd1
CW
754 // Fieldname may be prefixed with joins
755 var fieldName = _.last(filter.key.split('.'));
b7ceb253 756 // Special case for contact type/sub-type combo
06606cd1
CW
757 if (fieldName === 'contact_type' && (filter.value.indexOf('__') > 0)) {
758 combined.params[filter.key] = filter.value.split('__')[0];
759 combined.params[filter.key.replace('contact_type', 'contact_sub_type')] = filter.value.split('__')[1];
b7ceb253 760 } else {
cdc8d05f
CW
761 // Allow json-encoded api filters e.g. {"BETWEEN":[123,456]}
762 combined.params[filter.key] = filter.value.charAt(0) === '{' ? $.parseJSON(filter.value) : filter.value;
b7ceb253
CW
763 }
764 }
765 return combined;
766 }
767
403c52ac 768 CRM.utils.copyAttributes = function ($source, $target, attributes) {
9bce560a 769 _.each(attributes, function(name) {
a2973721 770 if ($source.attr(name) !== undefined) {
9bce560a
CW
771 $target.attr(name, $source.attr(name));
772 }
773 });
9bce560a
CW
774 };
775
3c0b6a40 776 CRM.utils.formatSelect2Result = function (row) {
88881f79 777 var markup = '<div class="crm-select2-row">';
ff88d165 778 if (row.image !== undefined) {
88881f79 779 markup += '<div class="crm-select2-image"><img src="' + row.image + '"/></div>';
ff88d165 780 }
54bee7df 781 else if (row.icon_class) {
88881f79 782 markup += '<div class="crm-select2-icon"><div class="crm-icon ' + row.icon_class + '-icon"></div></div>';
54bee7df 783 }
8a938c69 784 markup += '<div><div class="crm-select2-row-label '+(row.label_class || '')+'">' +
4fcc9877 785 (row.color ? '<span class="crm-select-item-color" style="background-color: ' + row.color + '"></span> ' : '') +
70218987 786 (row.icon ? '<i class="crm-i ' + row.icon + '" aria-hidden="true"></i> ' : '') +
7a5edd0a 787 _.escape((row.prefix !== undefined ? row.prefix + ' ' : '') + row.label + (row.suffix !== undefined ? ' ' + row.suffix : '')) +
8a938c69
CW
788 '</div>' +
789 '<div class="crm-select2-row-description">';
88881f79 790 $.each(row.description || [], function(k, text) {
3eb539fd 791 markup += '<p>' + _.escape(text) + '</p> ';
88881f79
CW
792 });
793 markup += '</div></div></div>';
ff88d165 794 return markup;
3c0b6a40 795 };
a4799f04 796
4fcc9877
CW
797 function formatEntityRefSelection(row) {
798 return (row.color ? '<span class="crm-select-item-color" style="background-color: ' + row.color + '"></span> ' : '') +
799 _.escape((row.prefix !== undefined ? row.prefix + ' ' : '') + row.label + (row.suffix !== undefined ? ' ' + row.suffix : ''));
800 }
801
b7ceb253 802 function renderEntityRefCreateLinks($el) {
a4799f04
CW
803 var
804 createLinks = $el.data('create-links'),
b7ceb253 805 params = getEntityRefApiParams($el).params,
2229cf4f 806 entity = $el.data('api-entity'),
7a4cf127 807 markup = '<div class="crm-entityref-links crm-entityref-links-create">';
e695ee7c 808 if (!createLinks || (createLinks === true && !CRM.config.entityRef.links[entity])) {
b7ceb253
CW
809 return '';
810 }
a4799f04 811 if (createLinks === true) {
e695ee7c 812 createLinks = params.contact_type ? _.where(CRM.config.entityRef.links[entity], {type: params.contact_type}) : CRM.config.entityRef.links[entity];
a4799f04 813 }
a4799f04 814 _.each(createLinks, function(link) {
8dbd6052 815 markup += ' <a class="crm-add-entity crm-hover-button" href="' + link.url + '">' +
ba8bb6ee 816 '<i class="crm-i ' + (link.icon || 'fa-plus-circle') + '" aria-hidden="true"></i> ' +
8dbd6052 817 _.escape(link.label) + '</a>';
79ae07d9 818 });
b7ceb253
CW
819 markup += '</div>';
820 return markup;
821 }
822
823 function getEntityRefFilters($el) {
824 var
2229cf4f 825 entity = $el.data('api-entity'),
11f907d4 826 filters = CRM.config.entityRef.filters[entity] || [],
b7ceb253
CW
827 params = $.extend({params: {}}, $el.data('api-params') || {}).params,
828 result = [];
74e8320f
CW
829 _.each(filters, function(filter) {
830 _.defaults(filter, {type: 'select', 'attributes': {}, entity: entity});
11f907d4 831 if (!params[filter.key]) {
7fd621e8
CW
832 // Filter out options if params don't match its condition
833 if (filter.condition && !_.isMatch(params, _.pick(filter.condition, _.keys(params)))) {
834 return;
835 }
fd7c068f 836 result.push(filter);
b7ceb253 837 }
fd7c068f 838 else if (filter.key == 'contact_type' && typeof params.contact_sub_type === 'undefined') {
fd7c068f 839 result.push(filter);
b7ceb253
CW
840 }
841 });
842 return result;
843 }
844
fd7c068f
CW
845 /**
846 * Provide markup for entity ref filters
847 */
848 function entityRefFiltersMarkup($el) {
b7ceb253
CW
849 var
850 filters = getEntityRefFilters($el),
851 filter = $el.data('user-filter') || {},
852 filterSpec = filter.key ? _.find(filters, {key: filter.key}) : null;
853 if (!filters.length) {
854 return '';
855 }
856 var markup = '<div class="crm-entityref-filters">' +
857 '<select class="crm-entityref-filter-key' + (filter.key ? ' active' : '') + '">' +
7a5edd0a 858 '<option value="">' + _.escape(ts('Refine search...')) + '</option>' +
52e6588d 859 CRM.utils.renderOptions(filters, filter.key) +
0943a2d8 860 '</select>' + entityRefFilterValueMarkup($el, filter, filterSpec) + '</div>';
fd7c068f
CW
861 return markup;
862 }
863
864 /**
865 * Provide markup for entity ref filter value field
866 */
0943a2d8 867 function entityRefFilterValueMarkup($el, filter, filterSpec) {
fd7c068f
CW
868 var markup = '';
869 if (filterSpec) {
870 var attrs = '',
871 attributes = _.cloneDeep(filterSpec.attributes);
872 if (filterSpec.type !== 'select') {
873 attributes.type = filterSpec.type;
874 attributes.value = typeof filter.value !== 'undefined' ? filter.value : '';
875 }
876 attributes.class = 'crm-entityref-filter-value' + (filter.value ? ' active' : '');
877 $.each(attributes, function (attr, val) {
878 attrs += ' ' + attr + '="' + val + '"';
879 });
880 if (filterSpec.type === 'select') {
0943a2d8
CW
881 var fieldName = _.last(filter.key.split('.')),
882 options = [{key: '', value: ts('- select -')}];
883 if (filterSpec.options) {
884 options = options.concat(getEntityRefFilterOptions(fieldName, $el, filterSpec));
885 }
886 markup = '<select' + attrs + '>' + CRM.utils.renderOptions(options, filter.value) + '</select>';
fd7c068f
CW
887 } else {
888 markup = '<input' + attrs + '/>';
889 }
b7ceb253 890 }
79ae07d9 891 return markup;
a4799f04 892 }
ff88d165 893
b7ceb253 894 /**
fd7c068f 895 * Render the entity ref filter value field
b7ceb253 896 */
fd7c068f 897 function renderEntityRefFilterValue($el) {
b7ceb253 898 var
b7ceb253 899 filter = $el.data('user-filter') || {},
fd7c068f
CW
900 filterSpec = filter.key ? _.find(getEntityRefFilters($el), {key: filter.key}) : null,
901 $keyField = $('.crm-entityref-filter-key', '#select2-drop'),
902 $valField = null;
b7ceb253 903 if (filterSpec) {
fd7c068f 904 $('.crm-entityref-filter-value', '#select2-drop').remove();
0943a2d8 905 $valField = $(entityRefFilterValueMarkup($el, filter, filterSpec));
fd7c068f 906 $keyField.after($valField);
11f907d4 907 if (filterSpec.type === 'select') {
fd7c068f 908 loadEntityRefFilterOptions(filter, filterSpec, $valField, $el);
b7ceb253
CW
909 }
910 } else {
fd7c068f 911 $('.crm-entityref-filter-value', '#select2-drop').hide().val('').change();
b7ceb253
CW
912 }
913 }
914
fd7c068f 915 /**
11f907d4 916 * Fetch options for a filter from cache or ajax api
fd7c068f
CW
917 */
918 function loadEntityRefFilterOptions(filter, filterSpec, $valField, $el) {
fd7c068f 919 // Fieldname may be prefixed with joins - strip those out
0943a2d8 920 var fieldName = _.last(filter.key.split('.'));
11f907d4 921 if (filterSpec.options) {
0943a2d8 922 CRM.utils.setOptions($valField, getEntityRefFilterOptions(fieldName, $el, filterSpec), false, filter.value);
11f907d4
CW
923 return;
924 }
925 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', true);
fd7c068f
CW
926 CRM.api3(filterSpec.entity, 'getoptions', {field: fieldName, context: 'search', sequential: 1})
927 .done(function(result) {
11f907d4 928 var entity = $el.data('api-entity').toLowerCase();
fd7c068f 929 // Store options globally so we don't have to look them up again
11f907d4
CW
930 filterSpec.options = result.values;
931 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', false);
0943a2d8 932 CRM.utils.setOptions($valField, getEntityRefFilterOptions(fieldName, $el, filterSpec), false, filter.value);
fd7c068f
CW
933 });
934 }
935
0943a2d8
CW
936 function getEntityRefFilterOptions(fieldName, $el, filterSpec) {
937 var values = _.cloneDeep(filterSpec.options),
938 params = $.extend({params: {}}, $el.data('api-params') || {}).params;
11f907d4
CW
939 if (fieldName === 'contact_type' && params.contact_type) {
940 values = _.remove(values, function(option) {
941 return option.key.indexOf(params.contact_type + '__') === 0;
942 });
943 }
0943a2d8 944 return values;
11f907d4
CW
945 }
946
1136a401 947 //CRM-15598 - Override url validator method to allow relative url's (e.g. /index.htm)
948 $.validator.addMethod("url", function(value, element) {
949 if (/^\//.test(value)) {
950 // Relative url: prepend dummy path for validation.
951 value = 'http://domain.tld' + value;
952 }
953 // From jQuery Validation Plugin v1.12.0
954 return this.optional(element) || /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value);
955 });
956
3d527838
CW
957 /**
958 * Wrapper for jQuery validate initialization function; supplies defaults
3d527838
CW
959 */
960 $.fn.crmValidate = function(params) {
961 return $(this).each(function () {
012db0fa
MW
962 var validator = $(this).validate();
963 var that = this;
964 validator.settings = $.extend({}, validator.settings, CRM.validate._defaults, CRM.validate.params);
e3b2c4da 965 // Call our custom validation handler.
dd283518 966 $(validator.currentForm).on("invalid-form.validate", validator.settings.invalidHandler);
3d527838
CW
967 // Call any post-initialization callbacks
968 if (CRM.validate.functions && CRM.validate.functions.length) {
969 $.each(CRM.validate.functions, function(i, func) {
970 func.call(that);
971 });
972 }
973 });
b7ceb253 974 };
3d527838 975
423616fa
CW
976 // Submit-once
977 var submitted = [],
978 submitButton;
979 function submitOnceForm(e) {
980 if (e.isDefaultPrevented()) {
981 return;
982 }
983 if (_.contains(submitted, e.target)) {
984 return false;
985 }
986 submitted.push(e.target);
987 // Spin submit button icon
988 if (submitButton && $(submitButton, e.target).length) {
989 // Dialog button
990 if ($(e.target).closest('.ui-dialog .crm-ajax-container')) {
991 var identifier = $(submitButton).attr('name') || $(submitButton).attr('href');
992 if (identifier) {
993 submitButton = $(e.target).closest('.ui-dialog').find('button[data-identifier="' + identifier + '"]')[0] || submitButton;
994 }
995 }
c53716ff
CW
996 var $icon = $(submitButton).siblings('.crm-i').add('.crm-i, .ui-icon', submitButton);
997 $icon.data('origClass', $icon.attr('class')).removeClass().addClass('crm-i crm-submit-icon fa-spinner fa-pulse');
423616fa
CW
998 }
999 }
1000
c53716ff
CW
1001 // If form fails validation, restore button icon and reset the submitted array
1002 function submitFormInvalid(form) {
1003 submitted = [];
1004 $('.crm-i.crm-submit-icon').each(function() {
1005 if ($(this).data('origClass')) {
1006 $(this).removeClass().addClass($(this).data('origClass'));
1007 }
1008 });
1009 }
1010
f7b92fcd 1011 // Initialize widgets
eb90857a
CW
1012 $(document)
1013 .on('crmLoad', function(e) {
1014 $('table.row-highlight', e.target)
1015 .off('.rowHighlight')
7e13d44e
CW
1016 .on('change.rowHighlight', 'input.select-row, input.select-rows', function (e, data) {
1017 var filter, $table = $(this).closest('table');
eb90857a 1018 if ($(this).hasClass('select-rows')) {
7e13d44e
CW
1019 filter = $(this).prop('checked') ? ':not(:checked)' : ':checked';
1020 $('input.select-row' + filter, $table).prop('checked', $(this).prop('checked')).trigger('change', 'master-selected');
eb90857a
CW
1021 }
1022 else {
7e13d44e
CW
1023 $(this).closest('tr').toggleClass('crm-row-selected', $(this).prop('checked'));
1024 if (data !== 'master-selected') {
1025 $('input.select-rows', $table).prop('checked', $(".select-row:not(':checked')", $table).length < 1);
1026 }
eb90857a 1027 }
eb90857a
CW
1028 })
1029 .find('input.select-row:checked').parents('tr').addClass('crm-row-selected');
4e086328 1030 $('.crm-sortable-list', e.target).sortable();
7d12de7f 1031 $('table.crm-sortable', e.target).DataTable();
16f4d319
CW
1032 $('table.crm-ajax-table', e.target).each(function() {
1033 var
1034 $table = $(this),
1c952f13 1035 script = CRM.config.resourceBase + 'js/jquery/jquery.crmAjaxTable.js',
16f4d319
CW
1036 $accordion = $table.closest('.crm-accordion-wrapper.collapsed, .crm-collapsible.collapsed');
1037 // For tables hidden by collapsed accordions, wait.
1038 if ($accordion.length) {
1039 $accordion.one('crmAccordion:open', function() {
1c952f13
CW
1040 CRM.loadScript(script).done(function() {
1041 $table.crmAjaxTable();
1042 });
16f4d319
CW
1043 });
1044 } else {
1c952f13
CW
1045 CRM.loadScript(script).done(function() {
1046 $table.crmAjaxTable();
1047 });
16f4d319
CW
1048 }
1049 });
76859ea7 1050 if ($("input:radio[name=radio_ts]").length == 1) {
82661158
JP
1051 $("input:radio[name=radio_ts]").prop("checked", true);
1052 }
5f34e50b
CW
1053 $('.crm-select2:not(.select2-offscreen, .select2-container)', e.target).crmSelect2();
1054 $('.crm-form-entityref:not(.select2-offscreen, .select2-container)', e.target).crmEntityRef();
1d07e7ab 1055 $('select.crm-chain-select-control', e.target).off('.chainSelect').on('change.chainSelect', chainSelect);
238fee7f
CW
1056 $('.crm-form-text[data-crm-datepicker]', e.target).each(function() {
1057 $(this).crmDatepicker($(this).data('crmDatepicker'));
1058 });
30b7642c
CW
1059 $('.crm-editable', e.target).not('thead *').each(function() {
1060 var $el = $(this);
1061 CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmEditable.js').done(function() {
1062 $el.crmEditable();
1063 });
1064 });
3e201321 1065 // Cache Form Input initial values
88e9380e 1066 $('form[data-warn-changes] :input', e.target).each(function() {
603f899a 1067 $(this).data('crm-initial-value', $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val());
3e201321 1068 });
9db15279
CW
1069 $('textarea.crm-form-wysiwyg', e.target).each(function() {
1070 if ($(this).hasClass("collapsed")) {
1071 CRM.wysiwyg.createCollapsed(this);
1072 } else {
1073 CRM.wysiwyg.create(this);
1074 }
1075 });
c53716ff
CW
1076 // Submit once handlers
1077 $('form[data-submit-once]', e.target)
1078 .submit(submitOnceForm)
1079 .on('invalid-form', submitFormInvalid);
15ef6755 1080 $('form[data-submit-once] button[type=submit]', e.target).click(function(e) {
423616fa
CW
1081 submitButton = e.target;
1082 });
eb90857a 1083 })
eb90857a 1084 .on('dialogopen', function(e) {
f292709b 1085 var $el = $(e.target);
89696a2f 1086 $('body').addClass('ui-dialog-open');
f292709b
CW
1087 // Modal dialogs should disable scrollbars
1088 if ($el.dialog('option', 'modal')) {
1089 $el.addClass('modal-dialog');
eb90857a
CW
1090 $('body').css({overflow: 'hidden'});
1091 }
3479049c 1092 $el.parent().find('.ui-dialog-titlebar .ui-icon-closethick').removeClass('ui-icon-closethick').addClass('fa-times');
f292709b 1093 // Add resize button
a243158e 1094 if ($el.parent().hasClass('crm-container') && $el.dialog('option', 'resizable')) {
7a5edd0a 1095 $el.parent().find('.ui-dialog-titlebar').append($('<button class="crm-dialog-titlebar-resize ui-dialog-titlebar-close" title="'+ _.escape(ts('Toggle fullscreen'))+'" style="right:2em;"/>').button({icons: {primary: 'fa-expand'}, text: false}));
f292709b
CW
1096 $('.crm-dialog-titlebar-resize', $el.parent()).click(function(e) {
1097 if ($el.data('origSize')) {
1098 $el.dialog('option', $el.data('origSize'));
1099 $el.data('origSize', null);
e915be1d 1100 $(this).button('option', 'icons', {primary: 'fa-expand'});
f292709b 1101 } else {
28d510ab 1102 var menuHeight = $('#civicrm-menu').outerHeight();
59007035
CW
1103 if ($('body').hasClass('crm-menubar-below-cms-menu')) {
1104 menuHeight += $('#civicrm-menu').offset().top;
1105 }
f292709b 1106 $el.data('origSize', {
28d510ab 1107 position: {my: 'center', at: 'center center+' + (menuHeight / 2), of: window},
f292709b
CW
1108 width: $el.dialog('option', 'width'),
1109 height: $el.dialog('option', 'height')
1110 });
f2f191fe 1111 $el.dialog('option', {width: '100%', height: ($(window).height() - menuHeight), position: {my: "top", at: "top+"+menuHeight, of: window}});
e915be1d 1112 $(this).button('option', 'icons', {primary: 'fa-compress'});
f292709b 1113 }
02cd9764 1114 $el.trigger('dialogresize');
f292709b
CW
1115 e.preventDefault();
1116 });
1117 }
eb90857a
CW
1118 })
1119 .on('dialogclose', function(e) {
f292709b 1120 // Restore scrollbars when closing modal
5a6148a0 1121 if ($('.ui-dialog .modal-dialog:visible').not(e.target).length < 1) {
eb90857a
CW
1122 $('body').css({overflow: ''});
1123 }
89696a2f
CW
1124 if ($('.ui-dialog-content:visible').not(e.target).length < 1) {
1125 $('body').removeClass('ui-dialog-open');
1126 }
afc021d8 1127 })
1128 .on('submit', function(e) {
f582fc8f 1129 // CRM-14353 - disable changes warn when submitting a form
abb6e044 1130 $('[data-warn-changes]').attr('data-warn-changes', 'false');
52e6588d 1131 });
f582fc8f
CW
1132
1133 // CRM-14353 - Warn of unsaved changes for forms which have opted in
1134 window.onbeforeunload = function() {
18469bf2 1135 if (CRM.utils.initialValueChanged($('form[data-warn-changes=true]:visible'))) {
f582fc8f 1136 return ts('You have unsaved changes.');
0f5816a6 1137 }
f582fc8f 1138 };
6a488035 1139
0f5816a6 1140 $.fn.crmtooltip = function () {
2e23a078
RO
1141 var TOOLTIP_HIDE_DELAY = 300;
1142
2c29c2ac
RN
1143 $(document)
1144 .on('mouseover', 'a.crm-summary-link:not(.crm-processed)', function (e) {
73ed0c3f 1145 $(this).addClass('crm-processed crm-tooltip-active');
e24b17b9 1146 var topDistance = e.pageY - $(window).scrollTop();
2b3ddf6e 1147 if (topDistance < 300 || topDistance < $(this).children('.crm-tooltip-wrapper').height()) {
e24b17b9
CW
1148 $(this).addClass('crm-tooltip-down');
1149 }
1150 if (!$(this).children('.crm-tooltip-wrapper').length) {
6a488035
TO
1151 $(this).append('<div class="crm-tooltip-wrapper"><div class="crm-tooltip"></div></div>');
1152 $(this).children().children('.crm-tooltip')
1153 .html('<div class="crm-loading-element"></div>')
1154 .load(this.href);
1155 }
1156 })
2e23a078
RO
1157 .on('mouseleave', 'a.crm-summary-link', function () {
1158 var tooltipLink = $(this);
1159 setTimeout(function () {
1160 if (tooltipLink.filter(':hover').length === 0) {
1161 tooltipLink.removeClass('crm-processed crm-tooltip-active crm-tooltip-down');
1162 }
1163 }, TOOLTIP_HIDE_DELAY);
e24b17b9 1164 })
2c29c2ac 1165 .on('click', 'a.crm-summary-link', false);
6a488035
TO
1166 };
1167
b0ca6188 1168 var helpDisplay, helpPrevious;
2a243675
CW
1169 // Non-ajax example:
1170 // CRM.help('Example title', 'Here is some text to describe this example');
1171 // Ajax example (will load help id "foo" from templates/CRM/bar.tpl):
1172 // CRM.help('Example title', {id: 'foo', file: 'CRM/bar'});
8e3272a1 1173 CRM.help = function (title, params, url) {
2a243675 1174 var ajax = typeof params !== 'string';
55a93b02 1175 if (helpDisplay && helpDisplay.close) {
2a243675
CW
1176 // If the same link is clicked twice, just close the display
1177 if (helpDisplay.isOpen && _.isEqual(helpPrevious, params)) {
55a93b02 1178 helpDisplay.close();
b0ca6188
CW
1179 return;
1180 }
55a93b02 1181 helpDisplay.close();
b0ca6188 1182 }
2a243675
CW
1183 helpPrevious = _.cloneDeep(params);
1184 helpDisplay = CRM.alert(ajax ? '...' : params, title, 'crm-help ' + (ajax ? 'crm-msg-loading' : 'info'), {expires: 0});
1185 if (ajax) {
1186 if (!url) {
1187 url = CRM.url('civicrm/ajax/inline');
1188 params.class_name = 'CRM_Core_Page_Inline_Help';
1189 params.type = 'page';
1190 }
1191 $.ajax(url, {
6a488035
TO
1192 data: params,
1193 dataType: 'html',
e24b17b9 1194 success: function (data) {
6a488035
TO
1195 $('#crm-notification-container .crm-help .notify-content:last').html(data);
1196 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('info');
1197 },
e24b17b9 1198 error: function () {
6a488035
TO
1199 $('#crm-notification-container .crm-help .notify-content:last').html('Unable to load help file.');
1200 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('error');
1201 }
2a243675
CW
1202 });
1203 }
6a488035 1204 };
8960d9b9 1205 /**
033af979 1206 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
8960d9b9 1207 */
1b2475e1 1208 CRM.status = function(options, deferred) {
9a7ef94f 1209 // For simple usage without async operations you can pass in a string. 2nd param is optional string 'error' if this is not a success msg.
1b2475e1
CW
1210 if (typeof options === 'string') {
1211 return CRM.status({start: options, success: options, error: options})[deferred === 'error' ? 'reject' : 'resolve']();
8960d9b9 1212 }
1b2475e1
CW
1213 var opts = $.extend({
1214 start: ts('Saving...'),
9a7ef94f 1215 success: ts('Saved'),
47737104
CW
1216 error: function(data) {
1217 var msg = $.isPlainObject(data) && data.error_message;
1218 CRM.alert(msg || ts('Sorry an error occurred and your information was not saved'), ts('Error'), 'error');
1b2475e1
CW
1219 }
1220 }, options || {});
7a5edd0a 1221 var $msg = $('<div class="crm-status-box-outer status-start"><div class="crm-status-box-inner"><div class="crm-status-box-msg">' + _.escape(opts.start) + '</div></div></div>')
1b2475e1
CW
1222 .appendTo('body');
1223 $msg.css('min-width', $msg.width());
1224 function handle(status, data) {
1225 var endMsg = typeof(opts[status]) === 'function' ? opts[status](data) : opts[status];
1226 if (endMsg) {
7a5edd0a 1227 $msg.removeClass('status-start').addClass('status-' + status).find('.crm-status-box-msg').text(endMsg);
1b2475e1 1228 window.setTimeout(function() {
f54254d8
TO
1229 $msg.fadeOut('slow', function() {
1230 $msg.remove();
1231 });
4bad157e
CW
1232 }, 2000);
1233 } else {
1b2475e1 1234 $msg.remove();
4bad157e 1235 }
1b2475e1
CW
1236 }
1237 return (deferred || new $.Deferred())
1238 .done(function(data) {
1239 // If the server returns an error msg call the error handler
1240 var status = $.isPlainObject(data) && (data.is_error || data.status === 'error') ? 'error' : 'success';
1241 handle(status, data);
1242 })
1243 .fail(function(data) {
1244 handle('error', data);
1245 });
8960d9b9 1246 };
6a488035 1247
beab9d1b
TO
1248 // Convert an Angular promise to a jQuery promise
1249 CRM.toJqPromise = function(aPromise) {
1250 var jqDeferred = $.Deferred();
1251 aPromise.then(
1252 function(data) { jqDeferred.resolve(data); },
1253 function(data) { jqDeferred.reject(data); }
1254 // should we also handle progress events?
1255 );
1256 return jqDeferred.promise();
1257 };
1258
705c61e9
TO
1259 CRM.toAPromise = function($q, jqPromise) {
1260 var aDeferred = $q.defer();
1261 jqPromise.then(
1262 function(data) { aDeferred.resolve(data); },
1263 function(data) { aDeferred.reject(data); }
1264 // should we also handle progress events?
1265 );
1266 return aDeferred.promise;
1267 };
1268
6a488035 1269 /**
033af979 1270 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
6a488035 1271 */
0f5816a6 1272 CRM.alert = function (text, title, type, options) {
6a488035
TO
1273 type = type || 'alert';
1274 title = title || '';
1275 options = options || {};
1276 if ($('#crm-notification-container').length) {
1277 var params = {
1278 text: text,
1279 title: title,
1280 type: type
1281 };
1282 // By default, don't expire errors and messages containing links
1283 var extra = {
1284 expires: (type == 'error' || text.indexOf('<a ') > -1) ? 0 : (text ? 10000 : 5000),
1285 unique: true
1286 };
1287 options = $.extend(extra, options);
d19abfd1 1288 options.expires = (options.expires === false || !CRM.config.allowAlertAutodismissal) ? 0 : parseInt(options.expires, 10);
6a488035 1289 if (options.unique && options.unique !== '0') {
0f5816a6 1290 $('#crm-notification-container .ui-notify-message').each(function () {
6a488035
TO
1291 if (title === $('h1', this).html() && text === $('.notify-content', this).html()) {
1292 $('.icon.ui-notify-close', this).click();
1293 }
1294 });
1295 }
1296 return $('#crm-notification-container').notify('create', params, options);
1297 }
1298 else {
1299 if (title.length) {
1300 text = title + "\n" + text;
1301 }
a0a0f60d
N
1302 // strip html tags as they are not parsed in standard alerts
1303 alert($("<div/>").html(text).text());
6a488035
TO
1304 return null;
1305 }
e24b17b9 1306 };
6a488035
TO
1307
1308 /**
1309 * Close whichever alert contains the given node
1310 *
1311 * @param node
1312 */
0f5816a6 1313 CRM.closeAlertByChild = function (node) {
6a488035 1314 $(node).closest('.ui-notify-message').find('.icon.ui-notify-close').click();
e24b17b9 1315 };
6a488035
TO
1316
1317 /**
033af979 1318 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
6a488035 1319 */
5fb83680 1320 CRM.confirm = function (options) {
27f190b4 1321 var dialog, url, msg, buttons = [], settings = {
a65e5f52 1322 title: ts('Confirm'),
7553cf23 1323 message: ts('Are you sure you want to continue?'),
3f4328da 1324 url: null,
0d5f99d4 1325 width: 'auto',
a8a8ddac 1326 height: 'auto',
a243158e 1327 resizable: false,
5fb83680 1328 dialogClass: 'crm-container crm-confirm',
0f5816a6 1329 close: function () {
5fb83680 1330 $(this).dialog('destroy').remove();
0f5816a6 1331 },
5fb83680
CW
1332 options: {
1333 no: ts('Cancel'),
1334 yes: ts('Continue')
1335 }
0f5816a6 1336 };
a8a8ddac
CW
1337 if (options && options.url) {
1338 settings.resizable = true;
1339 settings.height = '50%';
1340 }
5fb83680 1341 $.extend(settings, ($.isFunction(options) ? arguments[1] : options) || {});
a8a8ddac 1342 settings = CRM.utils.adjustDialogDefaults(settings);
5fb83680 1343 if (!settings.buttons && $.isPlainObject(settings.options)) {
27f190b4
CW
1344 $.each(settings.options, function(op, label) {
1345 buttons.push({
5fb83680 1346 text: label,
27f190b4 1347 'data-op': op,
972bd897 1348 icons: {primary: op === 'no' ? 'fa-times' : 'fa-check'},
5fb83680 1349 click: function() {
27f190b4 1350 var event = $.Event('crmConfirm:' + op);
5fb83680
CW
1351 $(this).trigger(event);
1352 if (!event.isDefaultPrevented()) {
1353 dialog.dialog('close');
1354 }
1355 }
1356 });
1357 });
27f190b4
CW
1358 // Order buttons so that "no" goes on the right-hand side
1359 settings.buttons = _.sortBy(buttons, 'data-op').reverse();
2a06342c 1360 }
3f4328da 1361 url = settings.url;
c0b7c815 1362 msg = url ? '' : settings.message;
5fb83680
CW
1363 delete settings.options;
1364 delete settings.message;
3f4328da 1365 delete settings.url;
c0b7c815 1366 dialog = $('<div class="crm-confirm-dialog"></div>').html(msg || '').dialog(settings);
5fb83680
CW
1367 if ($.isFunction(options)) {
1368 dialog.on('crmConfirm:yes', options);
7553cf23 1369 }
3f4328da
CW
1370 if (url) {
1371 CRM.loadPage(url, {target: dialog});
1372 }
c0b7c815
CW
1373 else {
1374 dialog.trigger('crmLoad');
3f4328da
CW
1375 }
1376 return dialog;
e24b17b9 1377 };
6a488035 1378
ed7225e6
CW
1379 /** provides a local copy of ts for a domain */
1380 CRM.ts = function(domain) {
1381 return function(message, options) {
1382 if (domain) {
1383 options = $.extend(options || {}, {domain: domain});
1384 }
f97524d9
TO
1385 return ts(message, options);
1386 };
f97524d9
TO
1387 };
1388
e3d90d6c
TO
1389 CRM.addStrings = function(domain, strings) {
1390 var bucket = (domain == 'civicrm' ? 'strings' : 'strings::' + domain);
1391 CRM[bucket] = CRM[bucket] || {};
1392 _.extend(CRM[bucket], strings);
1393 };
1394
6a488035 1395 /**
033af979 1396 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
6a488035 1397 */
0f5816a6 1398 $.fn.crmError = function (text, title, options) {
6a488035
TO
1399 title = title || '';
1400 text = text || '';
1401 options = options || {};
1402
1403 var extra = {
1404 expires: 0
2badf248 1405 }, label;
6a488035 1406 if ($(this).length) {
0d75c29c 1407 if (title === '') {
2badf248 1408 label = $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
6a488035
TO
1409 if (label.length) {
1410 label.addClass('crm-error');
1411 var $label = label.clone();
0d75c29c 1412 if (text === '' && $('.crm-marker', $label).length > 0) {
6a488035
TO
1413 text = $('.crm-marker', $label).attr('title');
1414 }
1415 $('.crm-marker', $label).remove();
1416 title = $label.text();
1417 }
1418 }
47737104 1419 $(this).addClass('crm-error');
6a488035
TO
1420 }
1421 var msg = CRM.alert(text, title, 'error', $.extend(extra, options));
1422 if ($(this).length) {
1423 var ele = $(this);
0f5816a6
KJ
1424 setTimeout(function () {
1425 ele.one('change', function () {
f54254d8 1426 if (msg && msg.close) msg.close();
eafad194 1427 ele.removeClass('crm-error');
2badf248
CW
1428 if (label) {
1429 label.removeClass('crm-error');
1430 }
0f5816a6
KJ
1431 });
1432 }, 1000);
6a488035
TO
1433 }
1434 return msg;
e24b17b9 1435 };
6a488035
TO
1436
1437 // Display system alerts through js notifications
1438 function messagesFromMarkup() {
0f5816a6 1439 $('div.messages:visible', this).not('.help').not('.no-popup').each(function () {
e24b17b9 1440 var text, title = '';
6a488035
TO
1441 $(this).removeClass('status messages');
1442 var type = $(this).attr('class').split(' ')[0] || 'alert';
1443 type = type.replace('crm-', '');
1444 $('.icon', this).remove();
6a488035 1445 if ($('.msg-text', this).length > 0) {
e24b17b9 1446 text = $('.msg-text', this).html();
6a488035
TO
1447 title = $('.msg-title', this).html();
1448 }
1449 else {
e24b17b9 1450 text = $(this).html();
6a488035
TO
1451 }
1452 var options = $(this).data('options') || {};
1453 $(this).remove();
1454 // Duplicates were already removed server-side
1455 options.unique = false;
1456 CRM.alert(text, title, type, options);
1457 });
1458 // Handle qf form errors
1459 $('form :input.error', this).one('blur', function() {
1460 $('.ui-notify-message.error a.ui-notify-close').click();
1461 $(this).removeClass('error');
1462 $(this).next('span.crm-error').remove();
1463 $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]')
1464 .removeClass('crm-error')
1465 .find('.crm-error').removeClass('crm-error');
1466 });
1467 }
1468
e4762285
CW
1469 /**
1470 * Improve blockUI when used with jQuery dialog
1471 */
1adbbe2d
CW
1472 var originalBlock = $.fn.block,
1473 originalUnblock = $.fn.unblock;
1474
1475 $.fn.block = function(opts) {
1476 if ($(this).is('.ui-dialog-content')) {
1477 originalBlock.call($(this).parents('.ui-dialog'), opts);
1478 return $(this);
1479 }
1480 return originalBlock.call(this, opts);
e4762285 1481 };
1adbbe2d
CW
1482 $.fn.unblock = function(opts) {
1483 if ($(this).is('.ui-dialog-content')) {
1484 originalUnblock.call($(this).parents('.ui-dialog'), opts);
1485 return $(this);
1486 }
1487 return originalUnblock.call(this, opts);
e4762285 1488 };
1adbbe2d 1489
e4762285 1490 // Preprocess all CRM ajax calls to display messages
03a7ec8f
CW
1491 $(document).ajaxSuccess(function(event, xhr, settings) {
1492 try {
1493 if ((!settings.dataType || settings.dataType == 'json') && xhr.responseText) {
1494 var response = $.parseJSON(xhr.responseText);
1495 if (typeof(response.crmMessages) == 'object') {
1496 $.each(response.crmMessages, function(n, msg) {
1497 CRM.alert(msg.text, msg.title, msg.type, msg.options);
f54254d8 1498 });
03a7ec8f 1499 }
bba9b4f0
CW
1500 if (response.backtrace) {
1501 CRM.console('log', response.backtrace);
1502 }
82983331
CW
1503 if (typeof response.deprecated === 'string') {
1504 CRM.console('warn', response.deprecated);
1505 }
03a7ec8f
CW
1506 }
1507 }
82983331 1508 // Ignore errors thrown by parseJSON
03a7ec8f
CW
1509 catch (e) {}
1510 });
1511
0f5816a6 1512 $(function () {
fdeb4de2 1513 $.blockUI.defaults.message = null;
1adbbe2d 1514 $.blockUI.defaults.ignoreIfBlocked = true;
fdeb4de2 1515
65b86482
CW
1516 if ($('#crm-container').hasClass('crm-public')) {
1517 $.fn.select2.defaults.dropdownCssClass = $.ui.dialog.prototype.options.dialogClass = 'crm-container crm-public';
1518 }
1519
205bb8ae 1520 // Trigger crmLoad on initial content for consistency. It will also be triggered for ajax-loaded content.
8547369d 1521 $('.crm-container').trigger('crmLoad');
205bb8ae 1522
ef3309b6 1523 if ($('#crm-notification-container').length) {
6a488035
TO
1524 // Initialize notifications
1525 $('#crm-notification-container').notify();
1526 messagesFromMarkup.call($('#crm-container'));
6a488035 1527 }
ebb9197b 1528
475e9f44 1529 $('body')
5fb83680
CW
1530 // bind the event for image popup
1531 .on('click', 'a.crm-image-popup', function(e) {
1532 CRM.confirm({
1533 title: ts('Preview'),
a243158e 1534 resizable: true,
135880c6
CW
1535 // Prevent overlap with the menubar
1536 maxHeight: $(window).height() - 30,
1537 position: {my: 'center', at: 'center center+15', of: window},
e4762285 1538 message: '<div class="crm-custom-image-popup"><img style="max-width: 100%" src="' + $(this).attr('href') + '"></div>',
5fb83680
CW
1539 options: null
1540 });
1541 e.preventDefault();
475e9f44 1542 })
ebb9197b 1543
475e9f44
CW
1544 .on('click', function (event) {
1545 $('.btn-slide-active').removeClass('btn-slide-active').find('.panel').hide();
1546 if ($(event.target).is('.btn-slide')) {
1547 $(event.target).addClass('btn-slide-active').find('.panel').show();
1548 }
1549 })
d664f648 1550
4a143c04
CW
1551 // Handle clear button for form elements
1552 .on('click', 'a.crm-clear-link', function() {
9bce560a
CW
1553 $(this).css({visibility: 'hidden'}).siblings('.crm-form-radio:checked').prop('checked', false).trigger('change', ['crmClear']);
1554 $(this).siblings('input:text').val('').trigger('change', ['crmClear']);
4a143c04
CW
1555 return false;
1556 })
754affae 1557 .on('change keyup', 'input.crm-form-radio:checked, input[allowclear=1]', function(e, context) {
0c6fe5b5
CW
1558 if (context !== 'crmClear' && ($(this).is(':checked') || ($(this).is('[allowclear=1]') && $(this).val()))) {
1559 $(this).siblings('.crm-clear-link').css({visibility: ''});
1560 }
754affae
CW
1561 if (context !== 'crmClear' && $(this).is('[allowclear=1]') && $(this).val() === '') {
1562 $(this).siblings('.crm-clear-link').css({visibility: 'hidden'});
1563 }
843bfb07 1564 })
6a488035 1565
843bfb07 1566 // Allow normal clicking of links within accordions
25ce04ec 1567 .on('click.crmAccordions', 'div.crm-accordion-header a, .collapsible-title a', function (e) {
843bfb07 1568 e.stopPropagation();
cf021bc5 1569 })
843bfb07
CW
1570 // Handle accordions
1571 .on('click.crmAccordions', '.crm-accordion-header, .crm-collapsible .collapsible-title', function (e) {
16f4d319 1572 var action = 'open';
6a488035 1573 if ($(this).parent().hasClass('collapsed')) {
843bfb07 1574 $(this).next().css('display', 'none').slideDown(200);
6a488035
TO
1575 }
1576 else {
843bfb07 1577 $(this).next().css('display', 'block').slideUp(200);
16f4d319 1578 action = 'close';
6a488035 1579 }
16f4d319 1580 $(this).parent().toggleClass('collapsed').trigger('crmAccordion:' + action);
843bfb07 1581 e.preventDefault();
6a488035 1582 });
843bfb07
CW
1583
1584 $().crmtooltip();
1585 });
d5768733 1586
843bfb07
CW
1587 /**
1588 * Collapse or expand an accordion
1589 * @param speed
1590 */
0f5816a6
KJ
1591 $.fn.crmAccordionToggle = function (speed) {
1592 $(this).each(function () {
16f4d319 1593 var action = 'open';
6a488035
TO
1594 if ($(this).hasClass('collapsed')) {
1595 $('.crm-accordion-body', this).first().css('display', 'none').slideDown(speed);
1596 }
1597 else {
1598 $('.crm-accordion-body', this).first().css('display', 'block').slideUp(speed);
16f4d319 1599 action = 'close';
6a488035 1600 }
16f4d319 1601 $(this).toggleClass('collapsed').trigger('crmAccordion:' + action);
6a488035
TO
1602 });
1603 };
5ec182d9
CW
1604
1605 /**
1606 * Clientside currency formatting
e4762285 1607 * @param number value
e4f46be0 1608 * @param [optional] boolean onlyNumber - if true, we return formatted amount without currency sign
e4762285 1609 * @param [optional] string format - currency representation of the number 1234.56
5ec182d9
CW
1610 * @return string
1611 */
1612 var currencyTemplate;
7f92cfa9 1613 CRM.formatMoney = function(value, onlyNumber, format) {
db9e0947 1614 var precision, decimal, separator, sign, i, j, result;
5ec182d9
CW
1615 if (value === 'init' && format) {
1616 currencyTemplate = format;
1617 return;
1618 }
1619 format = format || currencyTemplate;
db9e0947
SS
1620 if ((result = /1(.?)234(.?)56/.exec(format)) !== null) { // If value is formatted to 2 decimals
1621 precision = 2;
1622 }
1623 else if ((result = /1(.?)234(.?)6/.exec(format)) !== null) { // If value is formatted to 1 decimal
1624 precision = 1;
1625 }
1626 else if ((result = /1(.?)235/.exec(format)) !== null) { // If value is formatted to zero decimals
1627 precision = false;
1628 }
1629 else {
5ec182d9
CW
1630 return 'Invalid format passed to CRM.formatMoney';
1631 }
1632 separator = result[1];
db9e0947 1633 decimal = precision ? result[2] : false;
5ec182d9
CW
1634 sign = (value < 0) ? '-' : '';
1635 //extracting the absolute value of the integer part of the number and converting to string
1636 i = parseInt(value = Math.abs(value).toFixed(2)) + '';
5ec182d9 1637 j = ((j = i.length) > 3) ? j % 3 : 0;
db9e0947
SS
1638 result = sign + (j ? i.substr(0, j) + separator : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + separator) + (precision ? decimal + Math.abs(value - i).toFixed(precision).slice(2) : '');
1639 if (onlyNumber) {
567e9eea 1640 return result;
1641 }
db9e0947
SS
1642 switch (precision) {
1643 case 2:
1644 return format.replace(/1.*234.*56/, result);
1645 case 1:
1646 return format.replace(/1.*234.*6/, result);
1647 case false:
1648 return format.replace(/1.*235/, result);
1649 }
5ec182d9 1650 };
bba9b4f0 1651
5438399c
TO
1652 CRM.angRequires = function(name) {
1653 return CRM.angular.requires[name] || [];
1654 };
1655
bba9b4f0
CW
1656 CRM.console = function(method, title, msg) {
1657 if (window.console) {
1658 method = $.isFunction(console[method]) ? method : 'log';
1659 if (msg === undefined) {
1660 return console[method](title);
1661 } else {
1662 return console[method](title, msg);
1663 }
1664 }
e4762285 1665 };
90efc417 1666
6cfd220b
CW
1667 // Sugar methods for window.localStorage, with a fallback for older browsers
1668 var cacheItems = {};
1669 CRM.cache = {
1670 get: function (name, defaultValue) {
1671 try {
1672 if (localStorage.getItem('CRM' + name) !== null) {
1673 return JSON.parse(localStorage.getItem('CRM' + name));
1674 }
1675 } catch(e) {}
1676 return cacheItems[name] === undefined ? defaultValue : cacheItems[name];
1677 },
1678 set: function (name, value) {
1679 try {
1680 localStorage.setItem('CRM' + name, JSON.stringify(value));
1681 } catch(e) {}
1682 cacheItems[name] = value;
1683 },
1684 clear: function(name) {
1685 try {
1686 localStorage.removeItem('CRM' + name);
1687 } catch(e) {}
1688 delete cacheItems[name];
1689 }
1690 };
1691
1692
1693
90efc417
TO
1694 // Determine if a user has a given permission.
1695 // @see CRM_Core_Resources::addPermissions
1696 CRM.checkPerm = function(perm) {
3d6e14f0 1697 return CRM.permissions && CRM.permissions[perm];
90efc417 1698 };
2cfa1092
TO
1699
1700 // Round while preserving sigfigs
b24d5a1e 1701 CRM.utils.sigfig = function(n, digits) {
2cfa1092
TO
1702 var len = ("" + n).length;
1703 var scale = Math.pow(10.0, len-digits);
1704 return Math.round(n / scale) * scale;
1705 };
4cf9188e 1706
b7a4b8d2
CW
1707 /**
1708 * Create a js Date object from a unix timestamp or a yyyy-mm-dd string
1709 * @param input
1710 * @returns {Date}
1711 */
3c68de9d 1712 CRM.utils.makeDate = function(input) {
4cf9188e
CW
1713 switch (typeof input) {
1714 case 'object':
1715 // already a date object
3c68de9d 1716 return input;
4cf9188e
CW
1717
1718 case 'string':
d3cbd0a5 1719 // convert iso format with or without dashes
b7a4b8d2
CW
1720 input = input.replace(/[- :]/g, '');
1721 var output = $.datepicker.parseDate('yymmdd', input.substr(0, 8));
1722 if (input.length === 14) {
1723 output.setHours(
1724 parseInt(input.substr(8, 2), 10),
1725 parseInt(input.substr(10, 2), 10),
1726 parseInt(input.substr(12, 2), 10)
1727 );
d3cbd0a5 1728 }
b7a4b8d2 1729 return output;
4cf9188e
CW
1730
1731 case 'number':
1732 // convert unix timestamp
3c68de9d 1733 return new Date(input * 1000);
4cf9188e 1734 }
3c68de9d
CW
1735 throw 'Invalid input passed to CRM.utils.makeDate';
1736 };
1737
b7a4b8d2
CW
1738 /**
1739 * Format a date (and optionally time) for output to the user
1740 *
1741 * @param {string|int|Date} input
1742 * Input may be a js Date object, a unix timestamp or a 'yyyy-mm-dd' string
1743 * @param {string|null} dateFormat
1744 * A string like 'yy-mm-dd' or null to use the system default
1745 * @param {int|bool} timeFormat
1746 * Leave empty to omit time from the output (default)
1747 * Or pass 12, 24, or true to use the system default for 12/24hr format
1748 * @returns {string}
1749 */
1750 CRM.utils.formatDate = function(input, dateFormat, timeFormat) {
1751 if (!input) {
1752 return '';
1753 }
1754 var date = CRM.utils.makeDate(input),
1755 output = $.datepicker.formatDate(dateFormat || CRM.config.dateInputFormat, date);
1756 if (timeFormat) {
1757 var hour = date.getHours(),
1758 min = date.getMinutes(),
1759 suf = '';
1760 if (timeFormat === 12 || (timeFormat === true && !CRM.config.timeIs24Hr)) {
1761 suf = ' ' + (hour < 12 ? ts('AM') : ts('PM'));
1762 if (hour === 0 || hour > 12) {
1763 hour = Math.abs(hour - 12);
1764 }
1765 } else if (hour < 10) {
1766 hour = '0' + hour;
1767 }
1768 output += ' ' + hour + ':' + (min < 10 ? '0' : '') + min + suf;
1769 }
1770 return output;
232785ea 1771 };
8cc43365
CW
1772
1773 // Used to set appropriate text color for a given background
1774 CRM.utils.colorContrast = function (hexcolor) {
1775 hexcolor = hexcolor.replace(/[ #]/g, '');
1776 var r = parseInt(hexcolor.substr(0, 2), 16),
1777 g = parseInt(hexcolor.substr(2, 2), 16),
1778 b = parseInt(hexcolor.substr(4, 2), 16),
1779 yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
1780 return (yiq >= 128) ? 'black' : 'white';
1781 };
1782
279877e8
CW
1783 // CVE-2015-9251 - Prevent auto-execution of scripts when no explicit dataType was provided
1784 $.ajaxPrefilter(function(s) {
1785 if (s.crossDomain) {
1786 s.contents.script = false;
1787 }
1788 });
1789
0c29b4b5
SL
1790 // CVE-2020-11022 and CVE-2020-11023 Passing HTML from untrusted sources - even after sanitizing it - to one of jQuery's DOM manipulation methods (i.e. .html(), .append(), and others) may execute untrusted code.
1791 $.htmlPrefilter = function(html) {
972838ed
SL
1792 // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
1793 // their XML equivalent: e.g., "<div />" to "<div></div>". This is
1794 // problematic for several reasons, including that it's vulnerable to XSS
1795 // attacks. However, since this was jQuery's behavior for many years, many
1796 // Drupal modules and jQuery plugins may be relying on it. Therefore, we
1797 // preserve that behavior, but for a limited set of tags only, that we believe
1798 // to not be vulnerable. This is the set of HTML tags that satisfy all of the
1799 // following conditions:
1800 // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
1801 // appear in that list, then we don't want to mess with it here either.
1802 // @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
1803 // - A normal element (not a void, template, text, or foreign element).
1804 // @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
1805 // - An element that is still defined by the current HTML specification
1806 // (not a deprecated element), because we do not want to rely on how
1807 // browsers parse deprecated elements.
1808 // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
1809 // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
1810 // designed for fragments, not entire documents.
1811 // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
1812 // regular expression, it didn't match on colgroup, and we don't want to
1813 // introduce a behavior change for that.
1814 var selfClosingTagsToReplace = [
1815 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
1816 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
1817 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
1818 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
1819 'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
1820 'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
1821 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
1822 'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
1823 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
1824 'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
1825 ];
1826
1827 // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
1828 // two expressions makes it easier to target <a/> without also targeting
1829 // every tag that starts with "a".
1830 var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')';
1831 var whitespace = '[\\x20\\t\\r\\n\\f]';
1832 var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi');
1833 var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi');
1834
1835 // jQuery 3.5 also fixed a vulnerability for when </select> appears within
1836 // an <option> or <optgroup>, but it did that in local code that we can't
1837 // backport directly. Instead, we filter such cases out. To do so, we need to
1838 // determine when jQuery would otherwise invoke the vulnerable code, which it
1839 // uses this regular expression to determine. The regular expression changed
1840 // for version 3.0.0 and changed again for 3.4.0.
1841 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
1842 // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
1843 // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
1844 var rtagName = /<([\w:]+)/;
1845
1846 // The regular expression that jQuery uses to determine which self-closing
1847 // tags to expand to open and close tags. This is vulnerable, because it
1848 // matches all tag names except the few excluded ones. We only use this
1849 // expression for determining vulnerability. The expression changed for
1850 // version 3, but we only need to check for vulnerability in versions 1 and 2,
1851 // so we use the expression from those versions.
1852 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
1853 var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
1854
0ffae32f
SL
1855 // This is how jQuery determines the first tag in the HTML.
1856 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
1857 var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase();
1858
1859 // It is not valid HTML for <option> or <optgroup> to have <select> as
1860 // either a descendant or sibling, and attempts to inject one can cause
1861 // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
1862 // possible XSS attack, reject the entire string.
1863 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
1864 if ((tag === 'option' || tag === 'optgroup') && html.match(/<\/?select/i)) {
1865 html = '';
1866 }
1867
1868 // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
1869 // the tags in the `selfClosingTagsToReplace` list defined above.
1870 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
1871 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
1872 html = html.replace(rxhtmlTagWithoutSpaceOrAttributes, "<$1></$1>");
1873 html = html.replace(rxhtmlTagWithSpaceAndMaybeAttributes, "<$1$2></$1>");
1874
0c29b4b5
SL
1875 return html;
1876 };
1877
4b513f23 1878})(jQuery, _);