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