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