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