PageSourceSearch

https://speleo.se/wp-content/plugins/civicrm/civicrm/js/Common.js?r=9YnAxsv_SE

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