PageSourceSearch

https://www.bookweb.org/libraries/civicrm/core/js/Common.js?r=LaAEsen_US

js bookweb.org collected 2026-09-24 08:32:52 UTC 87,449 bytes, 2,237 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.