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