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