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