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