PageSourceSearch

https://cristianismeijusticia.net/libraries/civicrm/core/js/Common.js?r=NYF5Aca_ES

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