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