Merge pull request #23798 from seamuslee001/fix_auto_renew_membership
[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 });
390bfe80
CW
486 // Collapsible optgroups should be expanded when searching (searching happens within select2-drop for single selects, but within the element for multiselects; this handles both)
487 if ($('#select2-drop.collapsible-optgroups-enabled .select2-search input.select2-input, .select2-dropdown-open.collapsible-optgroups .select2-search-field input.select2-input').val()) {
ca914657
CW
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) {
8e767b90
KJ
812 if (!params.contact_type) {
813 createLinks = CRM.config.entityRef.links[entity];
814 }
815 else if (typeof params.contact_type === 'string') {
816 createLinks = _.where(CRM.config.entityRef.links[entity], {type: params.contact_type});
817 } else {
818 // lets assume it's an array with filters such as IN etc
819 createLinks = [];
820 _.each(params.contact_type, function(types) {
821 _.each(types, function(type) {
822 createLinks.push(_.findWhere(CRM.config.entityRef.links[entity], {type: type}));
823 });
824 });
825 }
a4799f04 826 }
a4799f04 827 _.each(createLinks, function(link) {
8dbd6052 828 markup += ' <a class="crm-add-entity crm-hover-button" href="' + link.url + '">' +
ba8bb6ee 829 '<i class="crm-i ' + (link.icon || 'fa-plus-circle') + '" aria-hidden="true"></i> ' +
8dbd6052 830 _.escape(link.label) + '</a>';
79ae07d9 831 });
b7ceb253
CW
832 markup += '</div>';
833 return markup;
834 }
835
836 function getEntityRefFilters($el) {
837 var
2229cf4f 838 entity = $el.data('api-entity'),
11f907d4 839 filters = CRM.config.entityRef.filters[entity] || [],
b7ceb253
CW
840 params = $.extend({params: {}}, $el.data('api-params') || {}).params,
841 result = [];
74e8320f
CW
842 _.each(filters, function(filter) {
843 _.defaults(filter, {type: 'select', 'attributes': {}, entity: entity});
11f907d4 844 if (!params[filter.key]) {
7fd621e8
CW
845 // Filter out options if params don't match its condition
846 if (filter.condition && !_.isMatch(params, _.pick(filter.condition, _.keys(params)))) {
847 return;
848 }
fd7c068f 849 result.push(filter);
b7ceb253 850 }
fd7c068f 851 else if (filter.key == 'contact_type' && typeof params.contact_sub_type === 'undefined') {
fd7c068f 852 result.push(filter);
b7ceb253
CW
853 }
854 });
855 return result;
856 }
857
fd7c068f
CW
858 /**
859 * Provide markup for entity ref filters
860 */
861 function entityRefFiltersMarkup($el) {
b7ceb253
CW
862 var
863 filters = getEntityRefFilters($el),
864 filter = $el.data('user-filter') || {},
865 filterSpec = filter.key ? _.find(filters, {key: filter.key}) : null;
866 if (!filters.length) {
867 return '';
868 }
869 var markup = '<div class="crm-entityref-filters">' +
870 '<select class="crm-entityref-filter-key' + (filter.key ? ' active' : '') + '">' +
7a5edd0a 871 '<option value="">' + _.escape(ts('Refine search...')) + '</option>' +
52e6588d 872 CRM.utils.renderOptions(filters, filter.key) +
0943a2d8 873 '</select>' + entityRefFilterValueMarkup($el, filter, filterSpec) + '</div>';
fd7c068f
CW
874 return markup;
875 }
876
877 /**
878 * Provide markup for entity ref filter value field
879 */
0943a2d8 880 function entityRefFilterValueMarkup($el, filter, filterSpec) {
fd7c068f
CW
881 var markup = '';
882 if (filterSpec) {
883 var attrs = '',
884 attributes = _.cloneDeep(filterSpec.attributes);
885 if (filterSpec.type !== 'select') {
886 attributes.type = filterSpec.type;
887 attributes.value = typeof filter.value !== 'undefined' ? filter.value : '';
888 }
889 attributes.class = 'crm-entityref-filter-value' + (filter.value ? ' active' : '');
890 $.each(attributes, function (attr, val) {
891 attrs += ' ' + attr + '="' + val + '"';
892 });
893 if (filterSpec.type === 'select') {
0943a2d8
CW
894 var fieldName = _.last(filter.key.split('.')),
895 options = [{key: '', value: ts('- select -')}];
896 if (filterSpec.options) {
897 options = options.concat(getEntityRefFilterOptions(fieldName, $el, filterSpec));
898 }
899 markup = '<select' + attrs + '>' + CRM.utils.renderOptions(options, filter.value) + '</select>';
fd7c068f
CW
900 } else {
901 markup = '<input' + attrs + '/>';
902 }
b7ceb253 903 }
79ae07d9 904 return markup;
a4799f04 905 }
ff88d165 906
b7ceb253 907 /**
fd7c068f 908 * Render the entity ref filter value field
b7ceb253 909 */
fd7c068f 910 function renderEntityRefFilterValue($el) {
b7ceb253 911 var
b7ceb253 912 filter = $el.data('user-filter') || {},
fd7c068f
CW
913 filterSpec = filter.key ? _.find(getEntityRefFilters($el), {key: filter.key}) : null,
914 $keyField = $('.crm-entityref-filter-key', '#select2-drop'),
915 $valField = null;
b7ceb253 916 if (filterSpec) {
fd7c068f 917 $('.crm-entityref-filter-value', '#select2-drop').remove();
0943a2d8 918 $valField = $(entityRefFilterValueMarkup($el, filter, filterSpec));
fd7c068f 919 $keyField.after($valField);
11f907d4 920 if (filterSpec.type === 'select') {
fd7c068f 921 loadEntityRefFilterOptions(filter, filterSpec, $valField, $el);
b7ceb253
CW
922 }
923 } else {
fd7c068f 924 $('.crm-entityref-filter-value', '#select2-drop').hide().val('').change();
b7ceb253
CW
925 }
926 }
927
fd7c068f 928 /**
11f907d4 929 * Fetch options for a filter from cache or ajax api
fd7c068f
CW
930 */
931 function loadEntityRefFilterOptions(filter, filterSpec, $valField, $el) {
fd7c068f 932 // Fieldname may be prefixed with joins - strip those out
0943a2d8 933 var fieldName = _.last(filter.key.split('.'));
11f907d4 934 if (filterSpec.options) {
0943a2d8 935 CRM.utils.setOptions($valField, getEntityRefFilterOptions(fieldName, $el, filterSpec), false, filter.value);
11f907d4
CW
936 return;
937 }
938 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', true);
fd7c068f
CW
939 CRM.api3(filterSpec.entity, 'getoptions', {field: fieldName, context: 'search', sequential: 1})
940 .done(function(result) {
11f907d4 941 var entity = $el.data('api-entity').toLowerCase();
fd7c068f 942 // Store options globally so we don't have to look them up again
11f907d4
CW
943 filterSpec.options = result.values;
944 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', false);
0943a2d8 945 CRM.utils.setOptions($valField, getEntityRefFilterOptions(fieldName, $el, filterSpec), false, filter.value);
fd7c068f
CW
946 });
947 }
948
0943a2d8
CW
949 function getEntityRefFilterOptions(fieldName, $el, filterSpec) {
950 var values = _.cloneDeep(filterSpec.options),
951 params = $.extend({params: {}}, $el.data('api-params') || {}).params;
11f907d4
CW
952 if (fieldName === 'contact_type' && params.contact_type) {
953 values = _.remove(values, function(option) {
954 return option.key.indexOf(params.contact_type + '__') === 0;
955 });
956 }
0943a2d8 957 return values;
11f907d4
CW
958 }
959
1136a401 960 //CRM-15598 - Override url validator method to allow relative url's (e.g. /index.htm)
961 $.validator.addMethod("url", function(value, element) {
962 if (/^\//.test(value)) {
963 // Relative url: prepend dummy path for validation.
964 value = 'http://domain.tld' + value;
965 }
966 // From jQuery Validation Plugin v1.12.0
967 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);
968 });
969
3d527838
CW
970 /**
971 * Wrapper for jQuery validate initialization function; supplies defaults
3d527838
CW
972 */
973 $.fn.crmValidate = function(params) {
974 return $(this).each(function () {
012db0fa
MW
975 var validator = $(this).validate();
976 var that = this;
977 validator.settings = $.extend({}, validator.settings, CRM.validate._defaults, CRM.validate.params);
e3b2c4da 978 // Call our custom validation handler.
dd283518 979 $(validator.currentForm).on("invalid-form.validate", validator.settings.invalidHandler);
3d527838
CW
980 // Call any post-initialization callbacks
981 if (CRM.validate.functions && CRM.validate.functions.length) {
982 $.each(CRM.validate.functions, function(i, func) {
983 func.call(that);
984 });
985 }
986 });
b7ceb253 987 };
3d527838 988
423616fa
CW
989 // Submit-once
990 var submitted = [],
991 submitButton;
992 function submitOnceForm(e) {
993 if (e.isDefaultPrevented()) {
994 return;
995 }
996 if (_.contains(submitted, e.target)) {
997 return false;
998 }
999 submitted.push(e.target);
1000 // Spin submit button icon
1001 if (submitButton && $(submitButton, e.target).length) {
1002 // Dialog button
1003 if ($(e.target).closest('.ui-dialog .crm-ajax-container')) {
1004 var identifier = $(submitButton).attr('name') || $(submitButton).attr('href');
1005 if (identifier) {
1006 submitButton = $(e.target).closest('.ui-dialog').find('button[data-identifier="' + identifier + '"]')[0] || submitButton;
1007 }
1008 }
c53716ff
CW
1009 var $icon = $(submitButton).siblings('.crm-i').add('.crm-i, .ui-icon', submitButton);
1010 $icon.data('origClass', $icon.attr('class')).removeClass().addClass('crm-i crm-submit-icon fa-spinner fa-pulse');
423616fa
CW
1011 }
1012 }
1013
c53716ff
CW
1014 // If form fails validation, restore button icon and reset the submitted array
1015 function submitFormInvalid(form) {
1016 submitted = [];
1017 $('.crm-i.crm-submit-icon').each(function() {
1018 if ($(this).data('origClass')) {
1019 $(this).removeClass().addClass($(this).data('origClass'));
1020 }
1021 });
1022 }
1023
f7b92fcd 1024 // Initialize widgets
eb90857a
CW
1025 $(document)
1026 .on('crmLoad', function(e) {
1027 $('table.row-highlight', e.target)
1028 .off('.rowHighlight')
7e13d44e
CW
1029 .on('change.rowHighlight', 'input.select-row, input.select-rows', function (e, data) {
1030 var filter, $table = $(this).closest('table');
eb90857a 1031 if ($(this).hasClass('select-rows')) {
7e13d44e
CW
1032 filter = $(this).prop('checked') ? ':not(:checked)' : ':checked';
1033 $('input.select-row' + filter, $table).prop('checked', $(this).prop('checked')).trigger('change', 'master-selected');
eb90857a
CW
1034 }
1035 else {
7e13d44e
CW
1036 $(this).closest('tr').toggleClass('crm-row-selected', $(this).prop('checked'));
1037 if (data !== 'master-selected') {
1038 $('input.select-rows', $table).prop('checked', $(".select-row:not(':checked')", $table).length < 1);
1039 }
eb90857a 1040 }
eb90857a
CW
1041 })
1042 .find('input.select-row:checked').parents('tr').addClass('crm-row-selected');
4e086328 1043 $('.crm-sortable-list', e.target).sortable();
7d12de7f 1044 $('table.crm-sortable', e.target).DataTable();
16f4d319
CW
1045 $('table.crm-ajax-table', e.target).each(function() {
1046 var
1047 $table = $(this),
1c952f13 1048 script = CRM.config.resourceBase + 'js/jquery/jquery.crmAjaxTable.js',
16f4d319
CW
1049 $accordion = $table.closest('.crm-accordion-wrapper.collapsed, .crm-collapsible.collapsed');
1050 // For tables hidden by collapsed accordions, wait.
1051 if ($accordion.length) {
1052 $accordion.one('crmAccordion:open', function() {
1c952f13
CW
1053 CRM.loadScript(script).done(function() {
1054 $table.crmAjaxTable();
1055 });
16f4d319
CW
1056 });
1057 } else {
1c952f13
CW
1058 CRM.loadScript(script).done(function() {
1059 $table.crmAjaxTable();
1060 });
16f4d319
CW
1061 }
1062 });
76859ea7 1063 if ($("input:radio[name=radio_ts]").length == 1) {
82661158
JP
1064 $("input:radio[name=radio_ts]").prop("checked", true);
1065 }
5f34e50b
CW
1066 $('.crm-select2:not(.select2-offscreen, .select2-container)', e.target).crmSelect2();
1067 $('.crm-form-entityref:not(.select2-offscreen, .select2-container)', e.target).crmEntityRef();
1d07e7ab 1068 $('select.crm-chain-select-control', e.target).off('.chainSelect').on('change.chainSelect', chainSelect);
238fee7f
CW
1069 $('.crm-form-text[data-crm-datepicker]', e.target).each(function() {
1070 $(this).crmDatepicker($(this).data('crmDatepicker'));
1071 });
30b7642c
CW
1072 $('.crm-editable', e.target).not('thead *').each(function() {
1073 var $el = $(this);
1074 CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmEditable.js').done(function() {
1075 $el.crmEditable();
1076 });
1077 });
3e201321 1078 // Cache Form Input initial values
88e9380e 1079 $('form[data-warn-changes] :input', e.target).each(function() {
603f899a 1080 $(this).data('crm-initial-value', $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val());
3e201321 1081 });
9db15279
CW
1082 $('textarea.crm-form-wysiwyg', e.target).each(function() {
1083 if ($(this).hasClass("collapsed")) {
1084 CRM.wysiwyg.createCollapsed(this);
1085 } else {
1086 CRM.wysiwyg.create(this);
1087 }
1088 });
c53716ff
CW
1089 // Submit once handlers
1090 $('form[data-submit-once]', e.target)
1091 .submit(submitOnceForm)
1092 .on('invalid-form', submitFormInvalid);
15ef6755 1093 $('form[data-submit-once] button[type=submit]', e.target).click(function(e) {
423616fa
CW
1094 submitButton = e.target;
1095 });
eb90857a 1096 })
eb90857a 1097 .on('dialogopen', function(e) {
f292709b 1098 var $el = $(e.target);
89696a2f 1099 $('body').addClass('ui-dialog-open');
f292709b
CW
1100 // Modal dialogs should disable scrollbars
1101 if ($el.dialog('option', 'modal')) {
1102 $el.addClass('modal-dialog');
eb90857a
CW
1103 $('body').css({overflow: 'hidden'});
1104 }
3479049c 1105 $el.parent().find('.ui-dialog-titlebar .ui-icon-closethick').removeClass('ui-icon-closethick').addClass('fa-times');
f292709b 1106 // Add resize button
a243158e 1107 if ($el.parent().hasClass('crm-container') && $el.dialog('option', 'resizable')) {
7a5edd0a 1108 $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
1109 $('.crm-dialog-titlebar-resize', $el.parent()).click(function(e) {
1110 if ($el.data('origSize')) {
1111 $el.dialog('option', $el.data('origSize'));
1112 $el.data('origSize', null);
e915be1d 1113 $(this).button('option', 'icons', {primary: 'fa-expand'});
f292709b 1114 } else {
28d510ab 1115 var menuHeight = $('#civicrm-menu').outerHeight();
59007035
CW
1116 if ($('body').hasClass('crm-menubar-below-cms-menu')) {
1117 menuHeight += $('#civicrm-menu').offset().top;
1118 }
f292709b 1119 $el.data('origSize', {
28d510ab 1120 position: {my: 'center', at: 'center center+' + (menuHeight / 2), of: window},
f292709b
CW
1121 width: $el.dialog('option', 'width'),
1122 height: $el.dialog('option', 'height')
1123 });
f2f191fe 1124 $el.dialog('option', {width: '100%', height: ($(window).height() - menuHeight), position: {my: "top", at: "top+"+menuHeight, of: window}});
e915be1d 1125 $(this).button('option', 'icons', {primary: 'fa-compress'});
f292709b 1126 }
02cd9764 1127 $el.trigger('dialogresize');
f292709b
CW
1128 e.preventDefault();
1129 });
1130 }
eb90857a
CW
1131 })
1132 .on('dialogclose', function(e) {
f292709b 1133 // Restore scrollbars when closing modal
5a6148a0 1134 if ($('.ui-dialog .modal-dialog:visible').not(e.target).length < 1) {
eb90857a
CW
1135 $('body').css({overflow: ''});
1136 }
89696a2f
CW
1137 if ($('.ui-dialog-content:visible').not(e.target).length < 1) {
1138 $('body').removeClass('ui-dialog-open');
1139 }
afc021d8 1140 })
1141 .on('submit', function(e) {
f582fc8f 1142 // CRM-14353 - disable changes warn when submitting a form
abb6e044 1143 $('[data-warn-changes]').attr('data-warn-changes', 'false');
52e6588d 1144 });
f582fc8f
CW
1145
1146 // CRM-14353 - Warn of unsaved changes for forms which have opted in
1147 window.onbeforeunload = function() {
18469bf2 1148 if (CRM.utils.initialValueChanged($('form[data-warn-changes=true]:visible'))) {
f582fc8f 1149 return ts('You have unsaved changes.');
0f5816a6 1150 }
f582fc8f 1151 };
6a488035 1152
0f5816a6 1153 $.fn.crmtooltip = function () {
2e23a078
RO
1154 var TOOLTIP_HIDE_DELAY = 300;
1155
2c29c2ac
RN
1156 $(document)
1157 .on('mouseover', 'a.crm-summary-link:not(.crm-processed)', function (e) {
73ed0c3f 1158 $(this).addClass('crm-processed crm-tooltip-active');
e24b17b9 1159 var topDistance = e.pageY - $(window).scrollTop();
2b3ddf6e 1160 if (topDistance < 300 || topDistance < $(this).children('.crm-tooltip-wrapper').height()) {
e24b17b9
CW
1161 $(this).addClass('crm-tooltip-down');
1162 }
1163 if (!$(this).children('.crm-tooltip-wrapper').length) {
2ba5b7f7 1164 var tooltipContents = $(this)[0].hasAttribute('data-tooltip-url') ? $(this).attr('data-tooltip-url') : this.href;
6a488035
TO
1165 $(this).append('<div class="crm-tooltip-wrapper"><div class="crm-tooltip"></div></div>');
1166 $(this).children().children('.crm-tooltip')
1167 .html('<div class="crm-loading-element"></div>')
2ba5b7f7 1168 .load(tooltipContents);
6a488035
TO
1169 }
1170 })
2e23a078
RO
1171 .on('mouseleave', 'a.crm-summary-link', function () {
1172 var tooltipLink = $(this);
1173 setTimeout(function () {
1174 if (tooltipLink.filter(':hover').length === 0) {
1175 tooltipLink.removeClass('crm-processed crm-tooltip-active crm-tooltip-down');
1176 }
1177 }, TOOLTIP_HIDE_DELAY);
e24b17b9 1178 })
2c29c2ac 1179 .on('click', 'a.crm-summary-link', false);
6a488035
TO
1180 };
1181
b0ca6188 1182 var helpDisplay, helpPrevious;
2a243675
CW
1183 // Non-ajax example:
1184 // CRM.help('Example title', 'Here is some text to describe this example');
1185 // Ajax example (will load help id "foo" from templates/CRM/bar.tpl):
1186 // CRM.help('Example title', {id: 'foo', file: 'CRM/bar'});
8e3272a1 1187 CRM.help = function (title, params, url) {
2a243675 1188 var ajax = typeof params !== 'string';
55a93b02 1189 if (helpDisplay && helpDisplay.close) {
2a243675
CW
1190 // If the same link is clicked twice, just close the display
1191 if (helpDisplay.isOpen && _.isEqual(helpPrevious, params)) {
55a93b02 1192 helpDisplay.close();
b0ca6188
CW
1193 return;
1194 }
55a93b02 1195 helpDisplay.close();
b0ca6188 1196 }
2a243675
CW
1197 helpPrevious = _.cloneDeep(params);
1198 helpDisplay = CRM.alert(ajax ? '...' : params, title, 'crm-help ' + (ajax ? 'crm-msg-loading' : 'info'), {expires: 0});
1199 if (ajax) {
1200 if (!url) {
1201 url = CRM.url('civicrm/ajax/inline');
1202 params.class_name = 'CRM_Core_Page_Inline_Help';
1203 params.type = 'page';
1204 }
1205 $.ajax(url, {
6a488035
TO
1206 data: params,
1207 dataType: 'html',
e24b17b9 1208 success: function (data) {
6a488035
TO
1209 $('#crm-notification-container .crm-help .notify-content:last').html(data);
1210 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('info');
1211 },
e24b17b9 1212 error: function () {
6a488035
TO
1213 $('#crm-notification-container .crm-help .notify-content:last').html('Unable to load help file.');
1214 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('error');
1215 }
2a243675
CW
1216 });
1217 }
6a488035 1218 };
8960d9b9 1219 /**
033af979 1220 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
8960d9b9 1221 */
1b2475e1 1222 CRM.status = function(options, deferred) {
9a7ef94f 1223 // 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
1224 if (typeof options === 'string') {
1225 return CRM.status({start: options, success: options, error: options})[deferred === 'error' ? 'reject' : 'resolve']();
8960d9b9 1226 }
1b2475e1
CW
1227 var opts = $.extend({
1228 start: ts('Saving...'),
9a7ef94f 1229 success: ts('Saved'),
47737104
CW
1230 error: function(data) {
1231 var msg = $.isPlainObject(data) && data.error_message;
1232 CRM.alert(msg || ts('Sorry an error occurred and your information was not saved'), ts('Error'), 'error');
1b2475e1
CW
1233 }
1234 }, options || {});
7a5edd0a 1235 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
1236 .appendTo('body');
1237 $msg.css('min-width', $msg.width());
1238 function handle(status, data) {
1239 var endMsg = typeof(opts[status]) === 'function' ? opts[status](data) : opts[status];
1240 if (endMsg) {
7a5edd0a 1241 $msg.removeClass('status-start').addClass('status-' + status).find('.crm-status-box-msg').text(endMsg);
1b2475e1 1242 window.setTimeout(function() {
f54254d8
TO
1243 $msg.fadeOut('slow', function() {
1244 $msg.remove();
1245 });
4bad157e
CW
1246 }, 2000);
1247 } else {
1b2475e1 1248 $msg.remove();
4bad157e 1249 }
1b2475e1
CW
1250 }
1251 return (deferred || new $.Deferred())
1252 .done(function(data) {
1253 // If the server returns an error msg call the error handler
1254 var status = $.isPlainObject(data) && (data.is_error || data.status === 'error') ? 'error' : 'success';
1255 handle(status, data);
1256 })
1257 .fail(function(data) {
1258 handle('error', data);
1259 });
8960d9b9 1260 };
6a488035 1261
beab9d1b
TO
1262 // Convert an Angular promise to a jQuery promise
1263 CRM.toJqPromise = function(aPromise) {
1264 var jqDeferred = $.Deferred();
1265 aPromise.then(
1266 function(data) { jqDeferred.resolve(data); },
1267 function(data) { jqDeferred.reject(data); }
1268 // should we also handle progress events?
1269 );
1270 return jqDeferred.promise();
1271 };
1272
705c61e9
TO
1273 CRM.toAPromise = function($q, jqPromise) {
1274 var aDeferred = $q.defer();
1275 jqPromise.then(
1276 function(data) { aDeferred.resolve(data); },
1277 function(data) { aDeferred.reject(data); }
1278 // should we also handle progress events?
1279 );
1280 return aDeferred.promise;
1281 };
1282
6a488035 1283 /**
033af979 1284 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
6a488035 1285 */
0f5816a6 1286 CRM.alert = function (text, title, type, options) {
6a488035
TO
1287 type = type || 'alert';
1288 title = title || '';
1289 options = options || {};
1290 if ($('#crm-notification-container').length) {
1291 var params = {
1292 text: text,
1293 title: title,
1294 type: type
1295 };
1296 // By default, don't expire errors and messages containing links
1297 var extra = {
1298 expires: (type == 'error' || text.indexOf('<a ') > -1) ? 0 : (text ? 10000 : 5000),
1299 unique: true
1300 };
1301 options = $.extend(extra, options);
d19abfd1 1302 options.expires = (options.expires === false || !CRM.config.allowAlertAutodismissal) ? 0 : parseInt(options.expires, 10);
6a488035 1303 if (options.unique && options.unique !== '0') {
0f5816a6 1304 $('#crm-notification-container .ui-notify-message').each(function () {
6a488035
TO
1305 if (title === $('h1', this).html() && text === $('.notify-content', this).html()) {
1306 $('.icon.ui-notify-close', this).click();
1307 }
1308 });
1309 }
1310 return $('#crm-notification-container').notify('create', params, options);
1311 }
1312 else {
1313 if (title.length) {
1314 text = title + "\n" + text;
1315 }
a0a0f60d
N
1316 // strip html tags as they are not parsed in standard alerts
1317 alert($("<div/>").html(text).text());
6a488035
TO
1318 return null;
1319 }
e24b17b9 1320 };
6a488035
TO
1321
1322 /**
1323 * Close whichever alert contains the given node
1324 *
1325 * @param node
1326 */
0f5816a6 1327 CRM.closeAlertByChild = function (node) {
6a488035 1328 $(node).closest('.ui-notify-message').find('.icon.ui-notify-close').click();
e24b17b9 1329 };
6a488035
TO
1330
1331 /**
033af979 1332 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
6a488035 1333 */
5fb83680 1334 CRM.confirm = function (options) {
27f190b4 1335 var dialog, url, msg, buttons = [], settings = {
a65e5f52 1336 title: ts('Confirm'),
7553cf23 1337 message: ts('Are you sure you want to continue?'),
3f4328da 1338 url: null,
0d5f99d4 1339 width: 'auto',
a8a8ddac 1340 height: 'auto',
a243158e 1341 resizable: false,
5fb83680 1342 dialogClass: 'crm-container crm-confirm',
0f5816a6 1343 close: function () {
5fb83680 1344 $(this).dialog('destroy').remove();
0f5816a6 1345 },
5fb83680
CW
1346 options: {
1347 no: ts('Cancel'),
1348 yes: ts('Continue')
1349 }
0f5816a6 1350 };
a8a8ddac
CW
1351 if (options && options.url) {
1352 settings.resizable = true;
1353 settings.height = '50%';
1354 }
5fb83680 1355 $.extend(settings, ($.isFunction(options) ? arguments[1] : options) || {});
a8a8ddac 1356 settings = CRM.utils.adjustDialogDefaults(settings);
5fb83680 1357 if (!settings.buttons && $.isPlainObject(settings.options)) {
27f190b4
CW
1358 $.each(settings.options, function(op, label) {
1359 buttons.push({
5fb83680 1360 text: label,
27f190b4 1361 'data-op': op,
972bd897 1362 icons: {primary: op === 'no' ? 'fa-times' : 'fa-check'},
5fb83680 1363 click: function() {
27f190b4 1364 var event = $.Event('crmConfirm:' + op);
5fb83680
CW
1365 $(this).trigger(event);
1366 if (!event.isDefaultPrevented()) {
1367 dialog.dialog('close');
1368 }
1369 }
1370 });
1371 });
27f190b4
CW
1372 // Order buttons so that "no" goes on the right-hand side
1373 settings.buttons = _.sortBy(buttons, 'data-op').reverse();
2a06342c 1374 }
3f4328da 1375 url = settings.url;
c0b7c815 1376 msg = url ? '' : settings.message;
5fb83680
CW
1377 delete settings.options;
1378 delete settings.message;
3f4328da 1379 delete settings.url;
c0b7c815 1380 dialog = $('<div class="crm-confirm-dialog"></div>').html(msg || '').dialog(settings);
5fb83680
CW
1381 if ($.isFunction(options)) {
1382 dialog.on('crmConfirm:yes', options);
7553cf23 1383 }
3f4328da
CW
1384 if (url) {
1385 CRM.loadPage(url, {target: dialog});
1386 }
c0b7c815
CW
1387 else {
1388 dialog.trigger('crmLoad');
3f4328da
CW
1389 }
1390 return dialog;
e24b17b9 1391 };
6a488035 1392
ed7225e6
CW
1393 /** provides a local copy of ts for a domain */
1394 CRM.ts = function(domain) {
1395 return function(message, options) {
1396 if (domain) {
1397 options = $.extend(options || {}, {domain: domain});
1398 }
f97524d9
TO
1399 return ts(message, options);
1400 };
f97524d9
TO
1401 };
1402
e3d90d6c
TO
1403 CRM.addStrings = function(domain, strings) {
1404 var bucket = (domain == 'civicrm' ? 'strings' : 'strings::' + domain);
1405 CRM[bucket] = CRM[bucket] || {};
1406 _.extend(CRM[bucket], strings);
1407 };
1408
6a488035 1409 /**
033af979 1410 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
6a488035 1411 */
0f5816a6 1412 $.fn.crmError = function (text, title, options) {
6a488035
TO
1413 title = title || '';
1414 text = text || '';
1415 options = options || {};
1416
1417 var extra = {
1418 expires: 0
2badf248 1419 }, label;
6a488035 1420 if ($(this).length) {
0d75c29c 1421 if (title === '') {
2badf248 1422 label = $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
6a488035
TO
1423 if (label.length) {
1424 label.addClass('crm-error');
1425 var $label = label.clone();
0d75c29c 1426 if (text === '' && $('.crm-marker', $label).length > 0) {
6a488035
TO
1427 text = $('.crm-marker', $label).attr('title');
1428 }
1429 $('.crm-marker', $label).remove();
1430 title = $label.text();
1431 }
1432 }
47737104 1433 $(this).addClass('crm-error');
6a488035
TO
1434 }
1435 var msg = CRM.alert(text, title, 'error', $.extend(extra, options));
1436 if ($(this).length) {
1437 var ele = $(this);
0f5816a6
KJ
1438 setTimeout(function () {
1439 ele.one('change', function () {
f54254d8 1440 if (msg && msg.close) msg.close();
eafad194 1441 ele.removeClass('crm-error');
2badf248
CW
1442 if (label) {
1443 label.removeClass('crm-error');
1444 }
0f5816a6
KJ
1445 });
1446 }, 1000);
6a488035
TO
1447 }
1448 return msg;
e24b17b9 1449 };
6a488035
TO
1450
1451 // Display system alerts through js notifications
1452 function messagesFromMarkup() {
0f5816a6 1453 $('div.messages:visible', this).not('.help').not('.no-popup').each(function () {
e24b17b9 1454 var text, title = '';
6a488035
TO
1455 $(this).removeClass('status messages');
1456 var type = $(this).attr('class').split(' ')[0] || 'alert';
1457 type = type.replace('crm-', '');
1458 $('.icon', this).remove();
6a488035 1459 if ($('.msg-text', this).length > 0) {
e24b17b9 1460 text = $('.msg-text', this).html();
6a488035
TO
1461 title = $('.msg-title', this).html();
1462 }
1463 else {
e24b17b9 1464 text = $(this).html();
6a488035
TO
1465 }
1466 var options = $(this).data('options') || {};
1467 $(this).remove();
1468 // Duplicates were already removed server-side
1469 options.unique = false;
1470 CRM.alert(text, title, type, options);
1471 });
1472 // Handle qf form errors
1473 $('form :input.error', this).one('blur', function() {
1474 $('.ui-notify-message.error a.ui-notify-close').click();
1475 $(this).removeClass('error');
1476 $(this).next('span.crm-error').remove();
1477 $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]')
1478 .removeClass('crm-error')
1479 .find('.crm-error').removeClass('crm-error');
1480 });
1481 }
1482
e4762285
CW
1483 /**
1484 * Improve blockUI when used with jQuery dialog
1485 */
1adbbe2d
CW
1486 var originalBlock = $.fn.block,
1487 originalUnblock = $.fn.unblock;
1488
1489 $.fn.block = function(opts) {
1490 if ($(this).is('.ui-dialog-content')) {
1491 originalBlock.call($(this).parents('.ui-dialog'), opts);
1492 return $(this);
1493 }
1494 return originalBlock.call(this, opts);
e4762285 1495 };
1adbbe2d
CW
1496 $.fn.unblock = function(opts) {
1497 if ($(this).is('.ui-dialog-content')) {
1498 originalUnblock.call($(this).parents('.ui-dialog'), opts);
1499 return $(this);
1500 }
1501 return originalUnblock.call(this, opts);
e4762285 1502 };
1adbbe2d 1503
e4762285 1504 // Preprocess all CRM ajax calls to display messages
03a7ec8f
CW
1505 $(document).ajaxSuccess(function(event, xhr, settings) {
1506 try {
1507 if ((!settings.dataType || settings.dataType == 'json') && xhr.responseText) {
1508 var response = $.parseJSON(xhr.responseText);
1509 if (typeof(response.crmMessages) == 'object') {
1510 $.each(response.crmMessages, function(n, msg) {
1511 CRM.alert(msg.text, msg.title, msg.type, msg.options);
f54254d8 1512 });
03a7ec8f 1513 }
bba9b4f0
CW
1514 if (response.backtrace) {
1515 CRM.console('log', response.backtrace);
1516 }
82983331
CW
1517 if (typeof response.deprecated === 'string') {
1518 CRM.console('warn', response.deprecated);
1519 }
03a7ec8f
CW
1520 }
1521 }
82983331 1522 // Ignore errors thrown by parseJSON
03a7ec8f
CW
1523 catch (e) {}
1524 });
1525
0f5816a6 1526 $(function () {
fdeb4de2 1527 $.blockUI.defaults.message = null;
1adbbe2d 1528 $.blockUI.defaults.ignoreIfBlocked = true;
fdeb4de2 1529
65b86482
CW
1530 if ($('#crm-container').hasClass('crm-public')) {
1531 $.fn.select2.defaults.dropdownCssClass = $.ui.dialog.prototype.options.dialogClass = 'crm-container crm-public';
1532 }
1533
205bb8ae 1534 // Trigger crmLoad on initial content for consistency. It will also be triggered for ajax-loaded content.
8547369d 1535 $('.crm-container').trigger('crmLoad');
205bb8ae 1536
ef3309b6 1537 if ($('#crm-notification-container').length) {
6a488035
TO
1538 // Initialize notifications
1539 $('#crm-notification-container').notify();
1540 messagesFromMarkup.call($('#crm-container'));
6a488035 1541 }
ebb9197b 1542
475e9f44 1543 $('body')
5fb83680
CW
1544 // bind the event for image popup
1545 .on('click', 'a.crm-image-popup', function(e) {
1546 CRM.confirm({
1547 title: ts('Preview'),
a243158e 1548 resizable: true,
135880c6
CW
1549 // Prevent overlap with the menubar
1550 maxHeight: $(window).height() - 30,
1551 position: {my: 'center', at: 'center center+15', of: window},
e4762285 1552 message: '<div class="crm-custom-image-popup"><img style="max-width: 100%" src="' + $(this).attr('href') + '"></div>',
5fb83680
CW
1553 options: null
1554 });
1555 e.preventDefault();
475e9f44 1556 })
ebb9197b 1557
475e9f44
CW
1558 .on('click', function (event) {
1559 $('.btn-slide-active').removeClass('btn-slide-active').find('.panel').hide();
1560 if ($(event.target).is('.btn-slide')) {
1561 $(event.target).addClass('btn-slide-active').find('.panel').show();
1562 }
1563 })
d664f648 1564
4a143c04
CW
1565 // Handle clear button for form elements
1566 .on('click', 'a.crm-clear-link', function() {
9bce560a
CW
1567 $(this).css({visibility: 'hidden'}).siblings('.crm-form-radio:checked').prop('checked', false).trigger('change', ['crmClear']);
1568 $(this).siblings('input:text').val('').trigger('change', ['crmClear']);
4a143c04
CW
1569 return false;
1570 })
754affae 1571 .on('change keyup', 'input.crm-form-radio:checked, input[allowclear=1]', function(e, context) {
0c6fe5b5
CW
1572 if (context !== 'crmClear' && ($(this).is(':checked') || ($(this).is('[allowclear=1]') && $(this).val()))) {
1573 $(this).siblings('.crm-clear-link').css({visibility: ''});
1574 }
754affae
CW
1575 if (context !== 'crmClear' && $(this).is('[allowclear=1]') && $(this).val() === '') {
1576 $(this).siblings('.crm-clear-link').css({visibility: 'hidden'});
1577 }
843bfb07 1578 })
6a488035 1579
843bfb07 1580 // Allow normal clicking of links within accordions
25ce04ec 1581 .on('click.crmAccordions', 'div.crm-accordion-header a, .collapsible-title a', function (e) {
843bfb07 1582 e.stopPropagation();
cf021bc5 1583 })
843bfb07
CW
1584 // Handle accordions
1585 .on('click.crmAccordions', '.crm-accordion-header, .crm-collapsible .collapsible-title', function (e) {
16f4d319 1586 var action = 'open';
6a488035 1587 if ($(this).parent().hasClass('collapsed')) {
843bfb07 1588 $(this).next().css('display', 'none').slideDown(200);
6a488035
TO
1589 }
1590 else {
843bfb07 1591 $(this).next().css('display', 'block').slideUp(200);
16f4d319 1592 action = 'close';
6a488035 1593 }
16f4d319 1594 $(this).parent().toggleClass('collapsed').trigger('crmAccordion:' + action);
843bfb07 1595 e.preventDefault();
6a488035 1596 });
843bfb07
CW
1597
1598 $().crmtooltip();
1599 });
d5768733 1600
843bfb07
CW
1601 /**
1602 * Collapse or expand an accordion
1603 * @param speed
1604 */
0f5816a6
KJ
1605 $.fn.crmAccordionToggle = function (speed) {
1606 $(this).each(function () {
16f4d319 1607 var action = 'open';
6a488035
TO
1608 if ($(this).hasClass('collapsed')) {
1609 $('.crm-accordion-body', this).first().css('display', 'none').slideDown(speed);
1610 }
1611 else {
1612 $('.crm-accordion-body', this).first().css('display', 'block').slideUp(speed);
16f4d319 1613 action = 'close';
6a488035 1614 }
16f4d319 1615 $(this).toggleClass('collapsed').trigger('crmAccordion:' + action);
6a488035
TO
1616 });
1617 };
5ec182d9
CW
1618
1619 /**
1620 * Clientside currency formatting
e4762285 1621 * @param number value
e4f46be0 1622 * @param [optional] boolean onlyNumber - if true, we return formatted amount without currency sign
e4762285 1623 * @param [optional] string format - currency representation of the number 1234.56
5ec182d9
CW
1624 * @return string
1625 */
1626 var currencyTemplate;
7f92cfa9 1627 CRM.formatMoney = function(value, onlyNumber, format) {
db9e0947 1628 var precision, decimal, separator, sign, i, j, result;
5ec182d9
CW
1629 if (value === 'init' && format) {
1630 currencyTemplate = format;
1631 return;
1632 }
1633 format = format || currencyTemplate;
db9e0947
SS
1634 if ((result = /1(.?)234(.?)56/.exec(format)) !== null) { // If value is formatted to 2 decimals
1635 precision = 2;
1636 }
1637 else if ((result = /1(.?)234(.?)6/.exec(format)) !== null) { // If value is formatted to 1 decimal
1638 precision = 1;
1639 }
1640 else if ((result = /1(.?)235/.exec(format)) !== null) { // If value is formatted to zero decimals
1641 precision = false;
1642 }
1643 else {
5ec182d9
CW
1644 return 'Invalid format passed to CRM.formatMoney';
1645 }
1646 separator = result[1];
db9e0947 1647 decimal = precision ? result[2] : false;
5ec182d9
CW
1648 sign = (value < 0) ? '-' : '';
1649 //extracting the absolute value of the integer part of the number and converting to string
1650 i = parseInt(value = Math.abs(value).toFixed(2)) + '';
5ec182d9 1651 j = ((j = i.length) > 3) ? j % 3 : 0;
db9e0947
SS
1652 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) : '');
1653 if (onlyNumber) {
567e9eea 1654 return result;
1655 }
db9e0947
SS
1656 switch (precision) {
1657 case 2:
1658 return format.replace(/1.*234.*56/, result);
1659 case 1:
1660 return format.replace(/1.*234.*6/, result);
1661 case false:
1662 return format.replace(/1.*235/, result);
1663 }
5ec182d9 1664 };
bba9b4f0 1665
5438399c
TO
1666 CRM.angRequires = function(name) {
1667 return CRM.angular.requires[name] || [];
1668 };
1669
bba9b4f0
CW
1670 CRM.console = function(method, title, msg) {
1671 if (window.console) {
1672 method = $.isFunction(console[method]) ? method : 'log';
1673 if (msg === undefined) {
1674 return console[method](title);
1675 } else {
1676 return console[method](title, msg);
1677 }
1678 }
e4762285 1679 };
90efc417 1680
6cfd220b
CW
1681 // Sugar methods for window.localStorage, with a fallback for older browsers
1682 var cacheItems = {};
1683 CRM.cache = {
1684 get: function (name, defaultValue) {
1685 try {
1686 if (localStorage.getItem('CRM' + name) !== null) {
1687 return JSON.parse(localStorage.getItem('CRM' + name));
1688 }
1689 } catch(e) {}
1690 return cacheItems[name] === undefined ? defaultValue : cacheItems[name];
1691 },
1692 set: function (name, value) {
1693 try {
1694 localStorage.setItem('CRM' + name, JSON.stringify(value));
1695 } catch(e) {}
1696 cacheItems[name] = value;
1697 },
1698 clear: function(name) {
1699 try {
1700 localStorage.removeItem('CRM' + name);
1701 } catch(e) {}
1702 delete cacheItems[name];
1703 }
1704 };
1705
1706
1707
90efc417
TO
1708 // Determine if a user has a given permission.
1709 // @see CRM_Core_Resources::addPermissions
1710 CRM.checkPerm = function(perm) {
3d6e14f0 1711 return CRM.permissions && CRM.permissions[perm];
90efc417 1712 };
2cfa1092
TO
1713
1714 // Round while preserving sigfigs
b24d5a1e 1715 CRM.utils.sigfig = function(n, digits) {
2cfa1092
TO
1716 var len = ("" + n).length;
1717 var scale = Math.pow(10.0, len-digits);
1718 return Math.round(n / scale) * scale;
1719 };
4cf9188e 1720
b7a4b8d2
CW
1721 /**
1722 * Create a js Date object from a unix timestamp or a yyyy-mm-dd string
1723 * @param input
1724 * @returns {Date}
1725 */
3c68de9d 1726 CRM.utils.makeDate = function(input) {
4cf9188e
CW
1727 switch (typeof input) {
1728 case 'object':
1729 // already a date object
3c68de9d 1730 return input;
4cf9188e
CW
1731
1732 case 'string':
d3cbd0a5 1733 // convert iso format with or without dashes
b7a4b8d2
CW
1734 input = input.replace(/[- :]/g, '');
1735 var output = $.datepicker.parseDate('yymmdd', input.substr(0, 8));
1736 if (input.length === 14) {
1737 output.setHours(
1738 parseInt(input.substr(8, 2), 10),
1739 parseInt(input.substr(10, 2), 10),
1740 parseInt(input.substr(12, 2), 10)
1741 );
d3cbd0a5 1742 }
b7a4b8d2 1743 return output;
4cf9188e
CW
1744
1745 case 'number':
1746 // convert unix timestamp
3c68de9d 1747 return new Date(input * 1000);
4cf9188e 1748 }
3c68de9d
CW
1749 throw 'Invalid input passed to CRM.utils.makeDate';
1750 };
1751
b7a4b8d2
CW
1752 /**
1753 * Format a date (and optionally time) for output to the user
1754 *
1755 * @param {string|int|Date} input
1756 * Input may be a js Date object, a unix timestamp or a 'yyyy-mm-dd' string
1757 * @param {string|null} dateFormat
1758 * A string like 'yy-mm-dd' or null to use the system default
1759 * @param {int|bool} timeFormat
1760 * Leave empty to omit time from the output (default)
1761 * Or pass 12, 24, or true to use the system default for 12/24hr format
1762 * @returns {string}
1763 */
1764 CRM.utils.formatDate = function(input, dateFormat, timeFormat) {
1765 if (!input) {
1766 return '';
1767 }
1768 var date = CRM.utils.makeDate(input),
1769 output = $.datepicker.formatDate(dateFormat || CRM.config.dateInputFormat, date);
1770 if (timeFormat) {
1771 var hour = date.getHours(),
1772 min = date.getMinutes(),
1773 suf = '';
1774 if (timeFormat === 12 || (timeFormat === true && !CRM.config.timeIs24Hr)) {
1775 suf = ' ' + (hour < 12 ? ts('AM') : ts('PM'));
1776 if (hour === 0 || hour > 12) {
1777 hour = Math.abs(hour - 12);
1778 }
1779 } else if (hour < 10) {
1780 hour = '0' + hour;
1781 }
1782 output += ' ' + hour + ':' + (min < 10 ? '0' : '') + min + suf;
1783 }
1784 return output;
232785ea 1785 };
8cc43365
CW
1786
1787 // Used to set appropriate text color for a given background
1788 CRM.utils.colorContrast = function (hexcolor) {
1789 hexcolor = hexcolor.replace(/[ #]/g, '');
1790 var r = parseInt(hexcolor.substr(0, 2), 16),
1791 g = parseInt(hexcolor.substr(2, 2), 16),
1792 b = parseInt(hexcolor.substr(4, 2), 16),
1793 yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
1794 return (yiq >= 128) ? 'black' : 'white';
1795 };
1796
279877e8
CW
1797 // CVE-2015-9251 - Prevent auto-execution of scripts when no explicit dataType was provided
1798 $.ajaxPrefilter(function(s) {
1799 if (s.crossDomain) {
1800 s.contents.script = false;
1801 }
1802 });
1803
0c29b4b5
SL
1804 // 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.
1805 $.htmlPrefilter = function(html) {
972838ed
SL
1806 // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
1807 // their XML equivalent: e.g., "<div />" to "<div></div>". This is
1808 // problematic for several reasons, including that it's vulnerable to XSS
1809 // attacks. However, since this was jQuery's behavior for many years, many
1810 // Drupal modules and jQuery plugins may be relying on it. Therefore, we
1811 // preserve that behavior, but for a limited set of tags only, that we believe
1812 // to not be vulnerable. This is the set of HTML tags that satisfy all of the
1813 // following conditions:
1814 // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
1815 // appear in that list, then we don't want to mess with it here either.
1816 // @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
1817 // - A normal element (not a void, template, text, or foreign element).
1818 // @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
1819 // - An element that is still defined by the current HTML specification
1820 // (not a deprecated element), because we do not want to rely on how
1821 // browsers parse deprecated elements.
1822 // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
1823 // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
1824 // designed for fragments, not entire documents.
1825 // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
1826 // regular expression, it didn't match on colgroup, and we don't want to
1827 // introduce a behavior change for that.
1828 var selfClosingTagsToReplace = [
1829 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
1830 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
1831 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
1832 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
1833 'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
1834 'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
1835 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
1836 'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
1837 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
1838 'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
1839 ];
1840
1841 // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
1842 // two expressions makes it easier to target <a/> without also targeting
1843 // every tag that starts with "a".
1844 var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')';
1845 var whitespace = '[\\x20\\t\\r\\n\\f]';
1846 var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi');
1847 var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi');
1848
1849 // jQuery 3.5 also fixed a vulnerability for when </select> appears within
1850 // an <option> or <optgroup>, but it did that in local code that we can't
1851 // backport directly. Instead, we filter such cases out. To do so, we need to
1852 // determine when jQuery would otherwise invoke the vulnerable code, which it
1853 // uses this regular expression to determine. The regular expression changed
1854 // for version 3.0.0 and changed again for 3.4.0.
1855 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
1856 // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
1857 // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
1858 var rtagName = /<([\w:]+)/;
1859
1860 // The regular expression that jQuery uses to determine which self-closing
1861 // tags to expand to open and close tags. This is vulnerable, because it
1862 // matches all tag names except the few excluded ones. We only use this
1863 // expression for determining vulnerability. The expression changed for
1864 // version 3, but we only need to check for vulnerability in versions 1 and 2,
1865 // so we use the expression from those versions.
1866 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
1867 var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
1868
0ffae32f
SL
1869 // This is how jQuery determines the first tag in the HTML.
1870 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
1871 var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase();
1872
1873 // It is not valid HTML for <option> or <optgroup> to have <select> as
1874 // either a descendant or sibling, and attempts to inject one can cause
1875 // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
1876 // possible XSS attack, reject the entire string.
1877 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
1878 if ((tag === 'option' || tag === 'optgroup') && html.match(/<\/?select/i)) {
1879 html = '';
1880 }
1881
1882 // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
1883 // the tags in the `selfClosingTagsToReplace` list defined above.
1884 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
1885 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
1886 html = html.replace(rxhtmlTagWithoutSpaceOrAttributes, "<$1></$1>");
1887 html = html.replace(rxhtmlTagWithSpaceAndMaybeAttributes, "<$1$2></$1>");
1888
0c29b4b5
SL
1889 return html;
1890 };
1891
4b513f23 1892})(jQuery, _);