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