PageSourceSearch

https://theartssociety.org/sites/all/modules/civicrm/js/Common.js?tlx4vg

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