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