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