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