1 // https://civicrm.org/licensing
4 var cj
= CRM
.$ = jQuery
;
8 * Short-named function for string translation, defined in global scope so it's available everywhere.
10 * @param text string for translating
11 * @param params object key:value of additional parameters
15 function ts(text
, params
) {
17 var d
= (params
&& params
.domain
) ? ('strings::' + params
.domain
) : null;
18 if (d
&& CRM
[d
] && CRM
[d
][text
]) {
21 else if (CRM
.strings
[text
]) {
22 text
= CRM
.strings
[text
];
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-'));
31 return text
.replace(/%-crmescaped-/g, '%');
36 // Legacy code - ignore warnings
37 /* jshint ignore:start */
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
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 ...)
50 function on_load_init_blocks(showBlocks
, hideBlocks
, elementType
) {
51 if (elementType
== null) {
52 elementType
= 'block';
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
;
65 alert('showBlocks array item not in .tpl = ' + showBlocks
[i
]);
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';
77 alert('showBlocks array item not in .tpl = ' + hideBlocks
[i
]);
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).
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
94 function showHideByValue(trigger_field_id
, trigger_value
, target_element_id
, target_element_type
, field_type
, invert
) {
97 if (field_type
== 'select') {
98 var trigger
= trigger_value
.split("|");
99 var selectedOptionValue
= cj('#' + trigger_field_id
).val();
101 target
= target_element_id
.split("|");
102 for (j
= 0; j
< target
.length
; j
++) {
104 cj('#' + target
[j
]).show();
107 cj('#' + target
[j
]).hide();
109 for (var i
= 0; i
< trigger
.length
; i
++) {
110 if (selectedOptionValue
== trigger
[i
]) {
112 cj('#' + target
[j
]).hide();
115 cj('#' + target
[j
]).show();
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')) {
128 cj('#' + target
[j
]).hide();
131 cj('#' + target
[j
]).show();
136 cj('#' + target
[j
]).show();
139 cj('#' + target
[j
]).hide();
150 * Function to show / hide the row in optionFields
152 * @param index string, element whose innerHTML is to hide else will show the hidden row.
154 function showHideRow(index
) {
156 cj('tr#optionField_' + index
).hide();
157 if (cj('table#optionField tr:hidden:first').length
) {
158 cj('div#optionFieldLink').show();
162 cj('table#optionField tr:hidden:first').show();
163 if (!cj('table#optionField tr:hidden:last').length
) {
164 cj('div#optionFieldLink').hide();
170 /* jshint ignore:end */
172 if (!CRM
.utils
) CRM
.utils
= {};
173 if (!CRM
.strings
) CRM
.strings
= {};
174 if (!CRM
.vars
) CRM
.vars
= {};
176 (function ($, _
, undefined) {
178 /* jshint validthis: true */
180 // Theme classes for unattached elements
181 $.fn
.select2
.defaults
.dropdownCssClass
= $.ui
.dialog
.prototype.options
.dialogClass
= 'crm-container';
183 // https://github.com/ivaynberg/select2/pull/2090
184 $.fn
.select2
.defaults
.width
= 'resolve';
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, #civicrm-menu').length
;
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
);
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
);
202 if ($(el
).is('.crm-form-date-wrapper .crm-hidden-date')) {
203 $(el
).siblings().prop('disabled', value
);
208 var scriptsLoaded
= {};
209 CRM
.loadScript = function(url
, appendCacheCode
) {
210 if (!scriptsLoaded
[url
]) {
211 var script
= document
.createElement('script'),
213 if (appendCacheCode
!== false) {
214 src
+= (_
.includes(url
, '?') ? '&r=' : '?r=') + CRM
.config
.resourceCacheCode
;
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
;
223 scriptsLoaded
[url
].resolve();
226 // Make jQuery global available while script is loading
227 if (window
.jQuery
!== CRM
.$) {
228 CRM
.CMSjQuery
= window
.jQuery
;
229 window
.jQuery
= CRM
.$;
232 document
.getElementsByTagName("head")[0].appendChild(script
);
234 return scriptsLoaded
[url
];
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
244 CRM
.utils
.setOptions = function(select
, options
, placeholder
, value
) {
245 $(select
).each(function() {
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');
257 if (typeof placeholder
=== 'string') {
258 if ($elect
.is('[multiple]')) {
259 select
.attr('placeholder', placeholder
);
261 newOptions
= '<option value="">' + placeholder
+ '</option>' + newOptions
;
264 $elect
.append(newOptions
);
266 $elect
.trigger('crmOptionsUpdated', $.extend({}, options
)).trigger('change');
272 * Render an option list
273 * @param options {array}
274 * @param val {string} default value
275 * @param escapeHtml {bool}
278 CRM
.utils
.renderOptions = function(options
, val
, escapeHtml
) {
280 esc
= escapeHtml
=== false ? _
.identity
: _
.escape
;
281 if (!$.isArray(val
)) {
284 _
.each(options
, function(option
) {
285 if (option
.children
) {
286 rendered
+= '<optgroup label="' + esc(option
.value
) + '">' +
287 CRM
.utils
.renderOptions(option
.children
, val
) +
290 var selected
= ($.inArray('' + option
.key
, val
) > -1) ? 'selected="selected"' : '';
291 rendered
+= '<option value="' + esc(option
.key
) + '"' + selected
+ '>' + esc(option
.value
) + '</option>';
297 CRM
.utils
.getOptions = function(select
) {
299 $('option', select
).each(function() {
300 var option
= {key
: $(this).attr('value'), value
: $(this).text()};
301 if (option
.key
!== '') {
302 options
.push(option
);
308 function chainSelect() {
309 var $form
= $(this).closest('form'),
310 $target
= $('select[data-name="' + $(this).data('target') + '"]', $form
),
311 data
= $target
.data(),
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();
317 if (!(val
&& val
.length
)) {
318 CRM
.utils
.setOptions($target
.blur(), [], data
.emptyPrompt
);
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
));
329 * Compare Form Input values against cached initial value.
331 * @return {Boolean} true if changes have been made.
333 CRM
.utils
.initialValueChanged = function(el
) {
335 $(':input:visible, .select2-container:visible+:input:hidden', el
).not('[type=submit], [type=button], .crm-action-menu, :disabled').each(function () {
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
)) {
348 * This provides defaults for ui.dialog which either need to be calculated or are different from global defaults
353 CRM
.utils
.adjustDialogDefaults = function(settings
) {
354 settings
= $.extend({width
: '65%', height
: '65%', 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);
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%';
367 else if (screenWidth
< 1400) {
368 settings
.width
= '' + parseInt(percentage
+gap
-((screenWidth
- 700)/7*(gap)/100), 10) + '%';
374 function formatCrmSelect2(row
) {
375 var icon
= row
.icon
|| $(row
.element
).data('icon'),
376 color
= row
.color
|| $(row
.element
).data('color'),
377 description
= row
.description
|| $(row
.element
).data('description'),
380 ret
+= '<i class="crm-i ' + icon
+ '" aria-hidden="true"></i> ';
383 ret
+= '<span class="crm-select-item-color" style="background-color: ' + color
+ '"></span> ';
385 return ret
+ _
.escape(row
.text
) + (description
? '<div class="crm-select2-row-description"><p>' + _
.escape(description
) + '</p></div>' : '');
389 * Helper to generate an icon with alt text.
391 * See also smarty `{icon}` and CRM_Core_Page::crmIcon() functions
394 * The Font Awesome icon class to use.
396 * Alt text to display.
397 * @param mixed condition
398 * This will only display if this is truthy.
401 * The formatted icon markup.
403 CRM
.utils
.formatIcon = function (icon
, text
, condition
) {
404 if (typeof condition
!== 'undefined' && !condition
) {
410 text
= _
.escape(text
);
411 title
= ' title="' + text
+ '"';
412 sr
= '<span class="sr-only">' + text
+ '</span>';
414 return '<i class="crm-i ' + icon
+ '"' + title
+ ' aria-hidden="true"></i>' + sr
;
418 * Wrapper for select2 initialization function; supplies defaults
419 * @param options object
421 $.fn
.crmSelect2 = function(options
) {
422 if (options
=== 'destroy') {
423 return $(this).each(function() {
425 .removeClass('crm-ajax-select')
430 return $(this).each(function () {
435 allowClear
: !$el
.hasClass('required'),
436 formatResult
: formatCrmSelect2
,
437 formatSelection
: formatCrmSelect2
439 // quickform doesn't support optgroups so here's a hack :(
440 $('option[value^=crm_optgroup]', this).each(function () {
441 $(this).nextUntil('option[value^=crm_optgroup]').wrapAll('<optgroup label="' + $(this).text() + '" />');
445 // quickform does not support disabled option, so yet another hack to
446 // add disabled property for option values
447 $('option[value^=crm_disabled_opt]', this).attr('disabled', 'disabled');
449 // Placeholder icon - total hack hikacking the escapeMarkup function but select2 3.5 dosn't have any other callbacks for this :(
450 if ($el
.is('[class*=fa-]')) {
451 settings
.escapeMarkup = function (m
) {
452 var out
= _
.escape(m
),
453 placeholder
= settings
.placeholder
|| $el
.data('placeholder') || $el
.attr('placeholder') || $('option[value=""]', $el
).text();
454 if (m
.length
&& placeholder
=== m
) {
455 iconClass
= $el
.attr('class').match(/(fa-\S*)/)[1];
456 out
= '<i class="crm-i ' + iconClass
+ '" aria-hidden="true"></i> ' + out
;
462 // Use description as title for each option
463 $el
.on('select2-loaded.crmSelect2', function() {
464 $('.crm-select2-row-description', '#select2-drop').each(function() {
465 $(this).closest('.select2-result-label').attr('title', $(this).text());
469 // Defaults for single-selects
470 if ($el
.is('select:not([multiple])')) {
471 settings
.minimumResultsForSearch
= 10;
472 if ($('option:first', this).val() === '') {
473 settings
.placeholderOption
= 'first';
476 $.extend(settings
, $el
.data('select-params') || {}, options
|| {});
478 $el
.addClass('crm-ajax-select');
480 $el
.select2(settings
);
485 * @see CRM_Core_Form::addEntityRef for docs
486 * @param options object
488 $.fn
.crmEntityRef = function(options
) {
489 if (options
=== 'destroy') {
490 return $(this).each(function() {
491 var entity
= $(this).data('api-entity') || '';
494 .removeClass('crm-form-entityref crm-' + _
.kebabCase(entity
) + '-ref')
495 .crmSelect2('destroy');
498 options
= options
|| {};
499 options
.select
= options
.select
|| {};
500 return $(this).each(function() {
502 $el
= $(this).off('.crmEntity'),
503 entity
= options
.entity
|| $el
.data('api-entity') || 'Contact',
505 // Legacy: fix entity name if passed in as snake case
506 if (entity
.charAt(0).toUpperCase() !== entity
.charAt(0)) {
507 entity
= _
.capitalize(_
.camelCase(entity
));
509 $el
.data('api-entity', entity
);
510 $el
.data('select-params', $.extend({}, $el
.data('select-params') || {}, options
.select
));
511 $el
.data('api-params', $.extend(true, {}, $el
.data('api-params') || {}, options
.api
));
512 $el
.data('create-links', options
.create
|| $el
.data('create-links'));
513 $el
.addClass('crm-form-entityref crm-' + _
.kebabCase(entity
) + '-ref');
515 // Use select2 ajax helper instead of CRM.api3 because it provides more value
517 url
: CRM
.url('civicrm/ajax/rest'),
519 data: function (input
, page_num
) {
520 var params
= getEntityRefApiParams($el
);
521 params
.input
= input
;
522 params
.page_num
= page_num
;
524 entity
: $el
.data('api-entity'),
526 json
: JSON
.stringify(params
)
529 results: function(data
) {
530 return {more
: data
.more_results
, results
: data
.values
|| []};
533 minimumInputLength
: 1,
534 formatResult
: CRM
.utils
.formatSelect2Result
,
535 formatSelection
: formatEntityRefSelection
,
536 escapeMarkup
: _
.identity
,
537 initSelection: function($el
, callback
) {
539 multiple
= !!$el
.data('select-params').multiple
,
541 stored
= $el
.data('entity-value') || [];
545 // If we already have this data, just return it
546 if (!_
.xor(val
.split(','), _
.pluck(stored
, 'id')).length
) {
547 callback(multiple
? stored
: stored
[0]);
549 var params
= $.extend({}, $el
.data('api-params') || {}, {id
: val
});
550 CRM
.api3($el
.data('api-entity'), 'getlist', params
).done(function(result
) {
551 callback(multiple
? result
.values
: result
.values
[0]);
552 // Trigger change (store data to avoid an infinite loop of lookups)
553 $el
.data('entity-value', result
.values
).trigger('change');
558 // Create new items inline - works for tags
559 if ($el
.data('create-links') && entity
=== 'Tag') {
560 selectParams
.createSearchChoice = function(term
, data
) {
561 if (!_
.findKey(data
, {label
: term
})) {
562 return {id
: "0", term
: term
, label
: term
+ ' (' + ts('new tag') + ')'};
565 selectParams
.tokenSeparators
= [','];
566 selectParams
.createSearchChoicePosition
= 'bottom';
567 $el
.on('select2-selecting.crmEntity', function(e
) {
570 e
.object
.label
= e
.object
.term
;
571 CRM
.api3(entity
, 'create', $.extend({name
: e
.object
.term
}, $el
.data('api-params').params
|| {}))
572 .done(function(created
) {
574 val
= $el
.select2('val'),
575 data
= $el
.select2('data'),
576 item
= {id
: created
.id
, label
: e
.object
.term
};
578 $el
.select2('data', item
, true);
580 else if ($.isArray(val
) && $.inArray("0", val
) > -1) {
581 _
.remove(data
, {id
: "0"});
583 $el
.select2('data', data
, true);
590 selectParams
.formatInputTooShort = function() {
591 var txt
= $el
.data('select-params').formatInputTooShort
|| $.fn
.select2
.defaults
.formatInputTooShort
.call(this);
592 txt
+= entityRefFiltersMarkup($el
) + renderEntityRefCreateLinks($el
);
595 selectParams
.formatNoMatches = function() {
596 var txt
= $el
.data('select-params').formatNoMatches
|| $.fn
.select2
.defaults
.formatNoMatches
;
597 txt
+= entityRefFiltersMarkup($el
) + renderEntityRefCreateLinks($el
);
600 $el
.on('select2-open.crmEntity', function() {
604 .on('click.crmEntity', 'a.crm-add-entity', function(e
) {
605 var extra
= $el
.data('api-params').extra
,
606 formUrl
= $(this).attr('href') + '&returnExtra=display_name,sort_name' + (extra
? (',' + extra
) : '');
607 $el
.select2('close');
608 CRM
.loadForm(formUrl
, {
609 dialog
: {width
: '50%', height
: 220}
610 }).on('crmFormSuccess', function(e
, data
) {
611 if (data
.status
=== 'success' && data
.id
) {
612 if (!data
.crmMessages
) {
613 CRM
.status(ts('%1 Created', {1: data
.label
|| data
.extra
.display_name
}));
615 data
.label
= data
.label
|| data
.extra
.sort_name
;
616 if ($el
.select2('container').hasClass('select2-container-multi')) {
617 var selection
= $el
.select2('data');
618 selection
.push(data
);
619 $el
.select2('data', selection
, true);
621 $el
.select2('data', data
, true);
627 .on('change.crmEntity', '.crm-entityref-filter-value', function() {
628 var filter
= $el
.data('user-filter') || {};
629 filter
.value
= $(this).val();
630 $(this).toggleClass('active', !!filter
.value
);
631 $el
.data('user-filter', filter
);
632 if (filter
.value
&& $(this).is('select')) {
633 // Once a filter has been chosen, rerender create links and refocus the search box
634 $el
.select2('close');
637 $('.crm-entityref-links', '#select2-drop').replaceWith(renderEntityRefCreateLinks($el
));
640 .on('change.crmEntity', 'select.crm-entityref-filter-key', function() {
641 var filter
= {key
: $(this).val()};
642 $(this).toggleClass('active', !!filter
.key
);
643 $el
.data('user-filter', filter
);
644 renderEntityRefFilterValue($el
);
645 $('.crm-entityref-filter-key', '#select2-drop').focus();
649 $el
.crmSelect2($.extend(settings
, $el
.data('select-params'), selectParams
));
654 * Combine api-params with user-filter
658 function getEntityRefApiParams($el
) {
660 params
= $.extend({params
: {}}, $el
.data('api-params') || {}),
661 // Prevent original data from being modified - $.extend and _.clone don't cut it, they pass nested objects by reference!
662 combined
= _
.cloneDeep(params
),
663 filter
= $.extend({}, $el
.data('user-filter') || {});
664 if (filter
.key
&& filter
.value
) {
665 // Fieldname may be prefixed with joins
666 var fieldName
= _
.last(filter
.key
.split('.'));
667 // Special case for contact type/sub-type combo
668 if (fieldName
=== 'contact_type' && (filter
.value
.indexOf('__') > 0)) {
669 combined
.params
[filter
.key
] = filter
.value
.split('__')[0];
670 combined
.params
[filter
.key
.replace('contact_type', 'contact_sub_type')] = filter
.value
.split('__')[1];
672 // Allow json-encoded api filters e.g. {"BETWEEN":[123,456]}
673 combined
.params
[filter
.key
] = filter
.value
.charAt(0) === '{' ? $.parseJSON(filter
.value
) : filter
.value
;
679 CRM
.utils
.copyAttributes = function ($source
, $target
, attributes
) {
680 _
.each(attributes
, function(name
) {
681 if ($source
.attr(name
) !== undefined) {
682 $target
.attr(name
, $source
.attr(name
));
687 CRM
.utils
.formatSelect2Result = function (row
) {
688 var markup
= '<div class="crm-select2-row">';
689 if (row
.image
!== undefined) {
690 markup
+= '<div class="crm-select2-image"><img src="' + row
.image
+ '"/></div>';
692 else if (row
.icon_class
) {
693 markup
+= '<div class="crm-select2-icon"><div class="crm-icon ' + row
.icon_class
+ '-icon"></div></div>';
695 markup
+= '<div><div class="crm-select2-row-label '+(row
.label_class
|| '')+'">' +
696 (row
.color
? '<span class="crm-select-item-color" style="background-color: ' + row
.color
+ '"></span> ' : '') +
697 _
.escape((row
.prefix
!== undefined ? row
.prefix
+ ' ' : '') + row
.label
+ (row
.suffix
!== undefined ? ' ' + row
.suffix
: '')) +
699 '<div class="crm-select2-row-description">';
700 $.each(row
.description
|| [], function(k
, text
) {
701 markup
+= '<p>' + _
.escape(text
) + '</p> ';
703 markup
+= '</div></div></div>';
707 function formatEntityRefSelection(row
) {
708 return (row
.color
? '<span class="crm-select-item-color" style="background-color: ' + row
.color
+ '"></span> ' : '') +
709 _
.escape((row
.prefix
!== undefined ? row
.prefix
+ ' ' : '') + row
.label
+ (row
.suffix
!== undefined ? ' ' + row
.suffix
: ''));
712 function renderEntityRefCreateLinks($el
) {
714 createLinks
= $el
.data('create-links'),
715 params
= getEntityRefApiParams($el
).params
,
716 entity
= $el
.data('api-entity'),
717 markup
= '<div class="crm-entityref-links">';
718 if (!createLinks
|| (createLinks
=== true && !CRM
.config
.entityRef
.links
[entity
])) {
721 if (createLinks
=== true) {
722 createLinks
= params
.contact_type
? _
.where(CRM
.config
.entityRef
.links
[entity
], {type
: params
.contact_type
}) : CRM
.config
.entityRef
.links
[entity
];
724 _
.each(createLinks
, function(link
) {
725 markup
+= ' <a class="crm-add-entity crm-hover-button" href="' + link
.url
+ '">' +
726 '<i class="crm-i ' + (link
.icon
|| 'fa-plus-circle') + '" aria-hidden="true"></i> ' +
727 _
.escape(link
.label
) + '</a>';
733 function getEntityRefFilters($el
) {
735 entity
= $el
.data('api-entity'),
736 filters
= CRM
.config
.entityRef
.filters
[entity
] || [],
737 params
= $.extend({params
: {}}, $el
.data('api-params') || {}).params
,
739 _
.each(filters
, function(filter
) {
740 _
.defaults(filter
, {type
: 'select', 'attributes': {}, entity
: entity
});
741 if (!params
[filter
.key
]) {
742 // Filter out options if params don't match its condition
743 if (filter
.condition
&& !_
.isMatch(params
, _
.pick(filter
.condition
, _
.keys(params
)))) {
748 else if (filter
.key
== 'contact_type' && typeof params
.contact_sub_type
=== 'undefined') {
756 * Provide markup for entity ref filters
758 function entityRefFiltersMarkup($el
) {
760 filters
= getEntityRefFilters($el
),
761 filter
= $el
.data('user-filter') || {},
762 filterSpec
= filter
.key
? _
.find(filters
, {key
: filter
.key
}) : null;
763 if (!filters
.length
) {
766 var markup
= '<div class="crm-entityref-filters">' +
767 '<select class="crm-entityref-filter-key' + (filter
.key
? ' active' : '') + '">' +
768 '<option value="">' + _
.escape(ts('Refine search...')) + '</option>' +
769 CRM
.utils
.renderOptions(filters
, filter
.key
) +
770 '</select>' + entityRefFilterValueMarkup($el
, filter
, filterSpec
) + '</div>';
775 * Provide markup for entity ref filter value field
777 function entityRefFilterValueMarkup($el
, filter
, filterSpec
) {
781 attributes
= _
.cloneDeep(filterSpec
.attributes
);
782 if (filterSpec
.type
!== 'select') {
783 attributes
.type
= filterSpec
.type
;
784 attributes
.value
= typeof filter
.value
!== 'undefined' ? filter
.value
: '';
786 attributes
.class = 'crm-entityref-filter-value' + (filter
.value
? ' active' : '');
787 $.each(attributes
, function (attr
, val
) {
788 attrs
+= ' ' + attr
+ '="' + val
+ '"';
790 if (filterSpec
.type
=== 'select') {
791 var fieldName
= _
.last(filter
.key
.split('.')),
792 options
= [{key
: '', value
: ts('- select -')}];
793 if (filterSpec
.options
) {
794 options
= options
.concat(getEntityRefFilterOptions(fieldName
, $el
, filterSpec
));
796 markup
= '<select' + attrs
+ '>' + CRM
.utils
.renderOptions(options
, filter
.value
) + '</select>';
798 markup
= '<input' + attrs
+ '/>';
805 * Render the entity ref filter value field
807 function renderEntityRefFilterValue($el
) {
809 filter
= $el
.data('user-filter') || {},
810 filterSpec
= filter
.key
? _
.find(getEntityRefFilters($el
), {key
: filter
.key
}) : null,
811 $keyField
= $('.crm-entityref-filter-key', '#select2-drop'),
814 $('.crm-entityref-filter-value', '#select2-drop').remove();
815 $valField
= $(entityRefFilterValueMarkup($el
, filter
, filterSpec
));
816 $keyField
.after($valField
);
817 if (filterSpec
.type
=== 'select') {
818 loadEntityRefFilterOptions(filter
, filterSpec
, $valField
, $el
);
821 $('.crm-entityref-filter-value', '#select2-drop').hide().val('').change();
826 * Fetch options for a filter from cache or ajax api
828 function loadEntityRefFilterOptions(filter
, filterSpec
, $valField
, $el
) {
829 // Fieldname may be prefixed with joins - strip those out
830 var fieldName
= _
.last(filter
.key
.split('.'));
831 if (filterSpec
.options
) {
832 CRM
.utils
.setOptions($valField
, getEntityRefFilterOptions(fieldName
, $el
, filterSpec
), false, filter
.value
);
835 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', true);
836 CRM
.api3(filterSpec
.entity
, 'getoptions', {field
: fieldName
, context
: 'search', sequential
: 1})
837 .done(function(result
) {
838 var entity
= $el
.data('api-entity').toLowerCase();
839 // Store options globally so we don't have to look them up again
840 filterSpec
.options
= result
.values
;
841 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', false);
842 CRM
.utils
.setOptions($valField
, getEntityRefFilterOptions(fieldName
, $el
, filterSpec
), false, filter
.value
);
846 function getEntityRefFilterOptions(fieldName
, $el
, filterSpec
) {
847 var values
= _
.cloneDeep(filterSpec
.options
),
848 params
= $.extend({params
: {}}, $el
.data('api-params') || {}).params
;
849 if (fieldName
=== 'contact_type' && params
.contact_type
) {
850 values
= _
.remove(values
, function(option
) {
851 return option
.key
.indexOf(params
.contact_type
+ '__') === 0;
857 //CRM-15598 - Override url validator method to allow relative url's (e.g. /index.htm)
858 $.validator
.addMethod("url", function(value
, element
) {
859 if (/^\//.test(value
)) {
860 // Relative url: prepend dummy path for validation.
861 value
= 'http://domain.tld' + value
;
863 // From jQuery Validation Plugin v1.12.0
864 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
);
868 * Wrapper for jQuery validate initialization function; supplies defaults
870 $.fn
.crmValidate = function(params
) {
871 return $(this).each(function () {
872 var validator
= $(this).validate();
874 validator
.settings
= $.extend({}, validator
.settings
, CRM
.validate
._defaults
, CRM
.validate
.params
);
875 // Call our custom validation handler.
876 $(validator
.currentForm
).on("invalid-form.validate", validator
.settings
.invalidHandler
);
877 // Call any post-initialization callbacks
878 if (CRM
.validate
.functions
&& CRM
.validate
.functions
.length
) {
879 $.each(CRM
.validate
.functions
, function(i
, func
) {
889 function submitOnceForm(e
) {
890 if (e
.isDefaultPrevented()) {
893 if (_
.contains(submitted
, e
.target
)) {
896 submitted
.push(e
.target
);
897 // Spin submit button icon
898 if (submitButton
&& $(submitButton
, e
.target
).length
) {
900 if ($(e
.target
).closest('.ui-dialog .crm-ajax-container')) {
901 var identifier
= $(submitButton
).attr('name') || $(submitButton
).attr('href');
903 submitButton
= $(e
.target
).closest('.ui-dialog').find('button[data-identifier="' + identifier
+ '"]')[0] || submitButton
;
906 var $icon
= $(submitButton
).siblings('.crm-i').add('.crm-i, .ui-icon', submitButton
);
907 $icon
.data('origClass', $icon
.attr('class')).removeClass().addClass('crm-i crm-submit-icon fa-spinner fa-pulse');
911 // If form fails validation, restore button icon and reset the submitted array
912 function submitFormInvalid(form
) {
914 $('.crm-i.crm-submit-icon').each(function() {
915 if ($(this).data('origClass')) {
916 $(this).removeClass().addClass($(this).data('origClass'));
921 // Initialize widgets
923 .on('crmLoad', function(e
) {
924 $('table.row-highlight', e
.target
)
925 .off('.rowHighlight')
926 .on('change.rowHighlight', 'input.select-row, input.select-rows', function (e
, data
) {
927 var filter
, $table
= $(this).closest('table');
928 if ($(this).hasClass('select-rows')) {
929 filter
= $(this).prop('checked') ? ':not(:checked)' : ':checked';
930 $('input.select-row' + filter
, $table
).prop('checked', $(this).prop('checked')).trigger('change', 'master-selected');
933 $(this).closest('tr').toggleClass('crm-row-selected', $(this).prop('checked'));
934 if (data
!== 'master-selected') {
935 $('input.select-rows', $table
).prop('checked', $(".select-row:not(':checked')", $table
).length
< 1);
939 .find('input.select-row:checked').parents('tr').addClass('crm-row-selected');
940 $('.crm-sortable-list', e
.target
).sortable();
941 $('table.crm-sortable', e
.target
).DataTable();
942 $('table.crm-ajax-table', e
.target
).each(function() {
945 script
= CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmAjaxTable.js',
946 $accordion
= $table
.closest('.crm-accordion-wrapper.collapsed, .crm-collapsible.collapsed');
947 // For tables hidden by collapsed accordions, wait.
948 if ($accordion
.length
) {
949 $accordion
.one('crmAccordion:open', function() {
950 CRM
.loadScript(script
).done(function() {
951 $table
.crmAjaxTable();
955 CRM
.loadScript(script
).done(function() {
956 $table
.crmAjaxTable();
960 if ($("input:radio[name=radio_ts]").length
== 1) {
961 $("input:radio[name=radio_ts]").prop("checked", true);
963 $('.crm-select2:not(.select2-offscreen, .select2-container)', e
.target
).crmSelect2();
964 $('.crm-form-entityref:not(.select2-offscreen, .select2-container)', e
.target
).crmEntityRef();
965 $('select.crm-chain-select-control', e
.target
).off('.chainSelect').on('change.chainSelect', chainSelect
);
966 $('.crm-form-text[data-crm-datepicker]', e
.target
).each(function() {
967 $(this).crmDatepicker($(this).data('crmDatepicker'));
969 $('.crm-editable', e
.target
).not('thead *').each(function() {
971 CRM
.loadScript(CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmEditable.js').done(function() {
975 // Cache Form Input initial values
976 $('form[data-warn-changes] :input', e
.target
).each(function() {
977 $(this).data('crm-initial-value', $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val());
979 $('textarea.crm-form-wysiwyg', e
.target
).each(function() {
980 if ($(this).hasClass("collapsed")) {
981 CRM
.wysiwyg
.createCollapsed(this);
983 CRM
.wysiwyg
.create(this);
986 // Submit once handlers
987 $('form[data-submit-once]', e
.target
)
988 .submit(submitOnceForm
)
989 .on('invalid-form', submitFormInvalid
);
990 $('form[data-submit-once] button[type=submit]', e
.target
).click(function(e
) {
991 submitButton
= e
.target
;
994 .on('dialogopen', function(e
) {
995 var $el
= $(e
.target
);
996 $('body').addClass('ui-dialog-open');
997 // Modal dialogs should disable scrollbars
998 if ($el
.dialog('option', 'modal')) {
999 $el
.addClass('modal-dialog');
1000 $('body').css({overflow
: 'hidden'});
1002 $el
.parent().find('.ui-dialog-titlebar .ui-icon-closethick').removeClass('ui-icon-closethick').addClass('fa-times');
1003 // Add resize button
1004 if ($el
.parent().hasClass('crm-container') && $el
.dialog('option', 'resizable')) {
1005 $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}));
1006 $('.crm-dialog-titlebar-resize', $el
.parent()).click(function(e
) {
1007 if ($el
.data('origSize')) {
1008 $el
.dialog('option', $el
.data('origSize'));
1009 $el
.data('origSize', null);
1010 $(this).button('option', 'icons', {primary
: 'fa-expand'});
1012 var menuHeight
= $('#civicrm-menu').outerHeight();
1013 if ($('body').hasClass('crm-menubar-below-cms-menu')) {
1014 menuHeight
+= $('#civicrm-menu').offset().top
;
1016 $el
.data('origSize', {
1017 position
: {my
: 'center', at
: 'center center+' + (menuHeight
/ 2), of: window
},
1018 width
: $el
.dialog('option', 'width'),
1019 height
: $el
.dialog('option', 'height')
1021 $el
.dialog('option', {width
: '100%', height
: ($(window
).height() - menuHeight
), position
: {my
: "top", at
: "top+"+menuHeight
, of: window
}});
1022 $(this).button('option', 'icons', {primary
: 'fa-compress'});
1024 $el
.trigger('dialogresize');
1029 .on('dialogclose', function(e
) {
1030 // Restore scrollbars when closing modal
1031 if ($('.ui-dialog .modal-dialog:visible').not(e
.target
).length
< 1) {
1032 $('body').css({overflow
: ''});
1034 if ($('.ui-dialog-content:visible').not(e
.target
).length
< 1) {
1035 $('body').removeClass('ui-dialog-open');
1038 .on('submit', function(e
) {
1039 // CRM-14353 - disable changes warn when submitting a form
1040 $('[data-warn-changes]').attr('data-warn-changes', 'false');
1043 // CRM-14353 - Warn of unsaved changes for forms which have opted in
1044 window
.onbeforeunload = function() {
1045 if (CRM
.utils
.initialValueChanged($('form[data-warn-changes=true]:visible'))) {
1046 return ts('You have unsaved changes.');
1050 $.fn
.crmtooltip = function () {
1052 .on('mouseover', 'a.crm-summary-link:not(.crm-processed)', function (e
) {
1053 $(this).addClass('crm-processed crm-tooltip-active');
1054 var topDistance
= e
.pageY
- $(window
).scrollTop();
1055 if (topDistance
< 300 || topDistance
< $(this).children('.crm-tooltip-wrapper').height()) {
1056 $(this).addClass('crm-tooltip-down');
1058 if (!$(this).children('.crm-tooltip-wrapper').length
) {
1059 $(this).append('<div class="crm-tooltip-wrapper"><div class="crm-tooltip"></div></div>');
1060 $(this).children().children('.crm-tooltip')
1061 .html('<div class="crm-loading-element"></div>')
1065 .on('mouseout', 'a.crm-summary-link', function () {
1066 $(this).removeClass('crm-processed crm-tooltip-active crm-tooltip-down');
1068 .on('click', 'a.crm-summary-link', false);
1071 var helpDisplay
, helpPrevious
;
1072 // Non-ajax example:
1073 // CRM.help('Example title', 'Here is some text to describe this example');
1074 // Ajax example (will load help id "foo" from templates/CRM/bar.tpl):
1075 // CRM.help('Example title', {id: 'foo', file: 'CRM/bar'});
1076 CRM
.help = function (title
, params
, url
) {
1077 var ajax
= typeof params
!== 'string';
1078 if (helpDisplay
&& helpDisplay
.close
) {
1079 // If the same link is clicked twice, just close the display
1080 if (helpDisplay
.isOpen
&& _
.isEqual(helpPrevious
, params
)) {
1081 helpDisplay
.close();
1084 helpDisplay
.close();
1086 helpPrevious
= _
.cloneDeep(params
);
1087 helpDisplay
= CRM
.alert(ajax
? '...' : params
, title
, 'crm-help ' + (ajax
? 'crm-msg-loading' : 'info'), {expires
: 0});
1090 url
= CRM
.url('civicrm/ajax/inline');
1091 params
.class_name
= 'CRM_Core_Page_Inline_Help';
1092 params
.type
= 'page';
1097 success: function (data
) {
1098 $('#crm-notification-container .crm-help .notify-content:last').html(data
);
1099 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('info');
1101 error: function () {
1102 $('#crm-notification-container .crm-help .notify-content:last').html('Unable to load help file.');
1103 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('error');
1109 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1111 CRM
.status = function(options
, deferred
) {
1112 // 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.
1113 if (typeof options
=== 'string') {
1114 return CRM
.status({start
: options
, success
: options
, error
: options
})[deferred
=== 'error' ? 'reject' : 'resolve']();
1116 var opts
= $.extend({
1117 start
: ts('Saving...'),
1118 success
: ts('Saved'),
1119 error: function(data
) {
1120 var msg
= $.isPlainObject(data
) && data
.error_message
;
1121 CRM
.alert(msg
|| ts('Sorry an error occurred and your information was not saved'), ts('Error'), 'error');
1124 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>')
1126 $msg
.css('min-width', $msg
.width());
1127 function handle(status
, data
) {
1128 var endMsg
= typeof(opts
[status
]) === 'function' ? opts
[status
](data
) : opts
[status
];
1130 $msg
.removeClass('status-start').addClass('status-' + status
).find('.crm-status-box-msg').text(endMsg
);
1131 window
.setTimeout(function() {
1132 $msg
.fadeOut('slow', function() {
1140 return (deferred
|| new $.Deferred())
1141 .done(function(data
) {
1142 // If the server returns an error msg call the error handler
1143 var status
= $.isPlainObject(data
) && (data
.is_error
|| data
.status
=== 'error') ? 'error' : 'success';
1144 handle(status
, data
);
1146 .fail(function(data
) {
1147 handle('error', data
);
1151 // Convert an Angular promise to a jQuery promise
1152 CRM
.toJqPromise = function(aPromise
) {
1153 var jqDeferred
= $.Deferred();
1155 function(data
) { jqDeferred
.resolve(data
); },
1156 function(data
) { jqDeferred
.reject(data
); }
1157 // should we also handle progress events?
1159 return jqDeferred
.promise();
1162 CRM
.toAPromise = function($q
, jqPromise
) {
1163 var aDeferred
= $q
.defer();
1165 function(data
) { aDeferred
.resolve(data
); },
1166 function(data
) { aDeferred
.reject(data
); }
1167 // should we also handle progress events?
1169 return aDeferred
.promise
;
1173 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1175 CRM
.alert = function (text
, title
, type
, options
) {
1176 type
= type
|| 'alert';
1177 title
= title
|| '';
1178 options
= options
|| {};
1179 if ($('#crm-notification-container').length
) {
1185 // By default, don't expire errors and messages containing links
1187 expires
: (type
== 'error' || text
.indexOf('<a ') > -1) ? 0 : (text
? 10000 : 5000),
1190 options
= $.extend(extra
, options
);
1191 options
.expires
= (options
.expires
=== false || !CRM
.config
.allowAlertAutodismissal
) ? 0 : parseInt(options
.expires
, 10);
1192 if (options
.unique
&& options
.unique
!== '0') {
1193 $('#crm-notification-container .ui-notify-message').each(function () {
1194 if (title
=== $('h1', this).html() && text
=== $('.notify-content', this).html()) {
1195 $('.icon.ui-notify-close', this).click();
1199 return $('#crm-notification-container').notify('create', params
, options
);
1203 text
= title
+ "\n" + text
;
1205 // strip html tags as they are not parsed in standard alerts
1206 alert($("<div/>").html(text
).text());
1212 * Close whichever alert contains the given node
1216 CRM
.closeAlertByChild = function (node
) {
1217 $(node
).closest('.ui-notify-message').find('.icon.ui-notify-close').click();
1221 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1223 CRM
.confirm = function (options
) {
1224 var dialog
, url
, msg
, buttons
= [], settings
= {
1225 title
: ts('Confirm'),
1226 message
: ts('Are you sure you want to continue?'),
1231 dialogClass
: 'crm-container crm-confirm',
1232 close: function () {
1233 $(this).dialog('destroy').remove();
1240 if (options
&& options
.url
) {
1241 settings
.resizable
= true;
1242 settings
.height
= '50%';
1244 $.extend(settings
, ($.isFunction(options
) ? arguments
[1] : options
) || {});
1245 settings
= CRM
.utils
.adjustDialogDefaults(settings
);
1246 if (!settings
.buttons
&& $.isPlainObject(settings
.options
)) {
1247 $.each(settings
.options
, function(op
, label
) {
1251 icons
: {primary
: op
=== 'no' ? 'fa-times' : 'fa-check'},
1253 var event
= $.Event('crmConfirm:' + op
);
1254 $(this).trigger(event
);
1255 if (!event
.isDefaultPrevented()) {
1256 dialog
.dialog('close');
1261 // Order buttons so that "no" goes on the right-hand side
1262 settings
.buttons
= _
.sortBy(buttons
, 'data-op').reverse();
1265 msg
= url
? '' : settings
.message
;
1266 delete settings
.options
;
1267 delete settings
.message
;
1268 delete settings
.url
;
1269 dialog
= $('<div class="crm-confirm-dialog"></div>').html(msg
|| '').dialog(settings
);
1270 if ($.isFunction(options
)) {
1271 dialog
.on('crmConfirm:yes', options
);
1274 CRM
.loadPage(url
, {target
: dialog
});
1277 dialog
.trigger('crmLoad');
1282 /** provides a local copy of ts for a domain */
1283 CRM
.ts = function(domain
) {
1284 return function(message
, options
) {
1286 options
= $.extend(options
|| {}, {domain
: domain
});
1288 return ts(message
, options
);
1292 CRM
.addStrings = function(domain
, strings
) {
1293 var bucket
= (domain
== 'civicrm' ? 'strings' : 'strings::' + domain
);
1294 CRM
[bucket
] = CRM
[bucket
] || {};
1295 _
.extend(CRM
[bucket
], strings
);
1299 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1301 $.fn
.crmError = function (text
, title
, options
) {
1302 title
= title
|| '';
1304 options
= options
|| {};
1309 if ($(this).length
) {
1311 label
= $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
1313 label
.addClass('crm-error');
1314 var $label
= label
.clone();
1315 if (text
=== '' && $('.crm-marker', $label
).length
> 0) {
1316 text
= $('.crm-marker', $label
).attr('title');
1318 $('.crm-marker', $label
).remove();
1319 title
= $label
.text();
1322 $(this).addClass('crm-error');
1324 var msg
= CRM
.alert(text
, title
, 'error', $.extend(extra
, options
));
1325 if ($(this).length
) {
1327 setTimeout(function () {
1328 ele
.one('change', function () {
1329 if (msg
&& msg
.close
) msg
.close();
1330 ele
.removeClass('crm-error');
1332 label
.removeClass('crm-error');
1340 // Display system alerts through js notifications
1341 function messagesFromMarkup() {
1342 $('div.messages:visible', this).not('.help').not('.no-popup').each(function () {
1343 var text
, title
= '';
1344 $(this).removeClass('status messages');
1345 var type
= $(this).attr('class').split(' ')[0] || 'alert';
1346 type
= type
.replace('crm-', '');
1347 $('.icon', this).remove();
1348 if ($('.msg-text', this).length
> 0) {
1349 text
= $('.msg-text', this).html();
1350 title
= $('.msg-title', this).html();
1353 text
= $(this).html();
1355 var options
= $(this).data('options') || {};
1357 // Duplicates were already removed server-side
1358 options
.unique
= false;
1359 CRM
.alert(text
, title
, type
, options
);
1361 // Handle qf form errors
1362 $('form :input.error', this).one('blur', function() {
1363 $('.ui-notify-message.error a.ui-notify-close').click();
1364 $(this).removeClass('error');
1365 $(this).next('span.crm-error').remove();
1366 $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]')
1367 .removeClass('crm-error')
1368 .find('.crm-error').removeClass('crm-error');
1373 * Improve blockUI when used with jQuery dialog
1375 var originalBlock
= $.fn
.block
,
1376 originalUnblock
= $.fn
.unblock
;
1378 $.fn
.block = function(opts
) {
1379 if ($(this).is('.ui-dialog-content')) {
1380 originalBlock
.call($(this).parents('.ui-dialog'), opts
);
1383 return originalBlock
.call(this, opts
);
1385 $.fn
.unblock = function(opts
) {
1386 if ($(this).is('.ui-dialog-content')) {
1387 originalUnblock
.call($(this).parents('.ui-dialog'), opts
);
1390 return originalUnblock
.call(this, opts
);
1393 // Preprocess all CRM ajax calls to display messages
1394 $(document
).ajaxSuccess(function(event
, xhr
, settings
) {
1396 if ((!settings
.dataType
|| settings
.dataType
== 'json') && xhr
.responseText
) {
1397 var response
= $.parseJSON(xhr
.responseText
);
1398 if (typeof(response
.crmMessages
) == 'object') {
1399 $.each(response
.crmMessages
, function(n
, msg
) {
1400 CRM
.alert(msg
.text
, msg
.title
, msg
.type
, msg
.options
);
1403 if (response
.backtrace
) {
1404 CRM
.console('log', response
.backtrace
);
1406 if (typeof response
.deprecated
=== 'string') {
1407 CRM
.console('warn', response
.deprecated
);
1411 // Ignore errors thrown by parseJSON
1416 $.blockUI
.defaults
.message
= null;
1417 $.blockUI
.defaults
.ignoreIfBlocked
= true;
1419 if ($('#crm-container').hasClass('crm-public')) {
1420 $.fn
.select2
.defaults
.dropdownCssClass
= $.ui
.dialog
.prototype.options
.dialogClass
= 'crm-container crm-public';
1423 // Trigger crmLoad on initial content for consistency. It will also be triggered for ajax-loaded content.
1424 $('.crm-container').trigger('crmLoad');
1426 if ($('#crm-notification-container').length
) {
1427 // Initialize notifications
1428 $('#crm-notification-container').notify();
1429 messagesFromMarkup
.call($('#crm-container'));
1433 // bind the event for image popup
1434 .on('click', 'a.crm-image-popup', function(e
) {
1436 title
: ts('Preview'),
1438 // Prevent overlap with the menubar
1439 maxHeight
: $(window
).height() - 30,
1440 position
: {my
: 'center', at
: 'center center+15', of: window
},
1441 message
: '<div class="crm-custom-image-popup"><img style="max-width: 100%" src="' + $(this).attr('href') + '"></div>',
1447 .on('click', function (event
) {
1448 $('.btn-slide-active').removeClass('btn-slide-active').find('.panel').hide();
1449 if ($(event
.target
).is('.btn-slide')) {
1450 $(event
.target
).addClass('btn-slide-active').find('.panel').show();
1454 // Handle clear button for form elements
1455 .on('click', 'a.crm-clear-link', function() {
1456 $(this).css({visibility
: 'hidden'}).siblings('.crm-form-radio:checked').prop('checked', false).trigger('change', ['crmClear']);
1457 $(this).siblings('input:text').val('').trigger('change', ['crmClear']);
1460 .on('change keyup', 'input.crm-form-radio:checked, input[allowclear=1]', function(e
, context
) {
1461 if (context
!== 'crmClear' && ($(this).is(':checked') || ($(this).is('[allowclear=1]') && $(this).val()))) {
1462 $(this).siblings('.crm-clear-link').css({visibility
: ''});
1464 if (context
!== 'crmClear' && $(this).is('[allowclear=1]') && $(this).val() === '') {
1465 $(this).siblings('.crm-clear-link').css({visibility
: 'hidden'});
1469 // Allow normal clicking of links within accordions
1470 .on('click.crmAccordions', 'div.crm-accordion-header a, .collapsible-title a', function (e
) {
1471 e
.stopPropagation();
1473 // Handle accordions
1474 .on('click.crmAccordions', '.crm-accordion-header, .crm-collapsible .collapsible-title', function (e
) {
1475 var action
= 'open';
1476 if ($(this).parent().hasClass('collapsed')) {
1477 $(this).next().css('display', 'none').slideDown(200);
1480 $(this).next().css('display', 'block').slideUp(200);
1483 $(this).parent().toggleClass('collapsed').trigger('crmAccordion:' + action
);
1491 * Collapse or expand an accordion
1494 $.fn
.crmAccordionToggle = function (speed
) {
1495 $(this).each(function () {
1496 var action
= 'open';
1497 if ($(this).hasClass('collapsed')) {
1498 $('.crm-accordion-body', this).first().css('display', 'none').slideDown(speed
);
1501 $('.crm-accordion-body', this).first().css('display', 'block').slideUp(speed
);
1504 $(this).toggleClass('collapsed').trigger('crmAccordion:' + action
);
1509 * Clientside currency formatting
1510 * @param number value
1511 * @param [optional] boolean onlyNumber - if true, we return formatted amount without currency sign
1512 * @param [optional] string format - currency representation of the number 1234.56
1515 var currencyTemplate
;
1516 CRM
.formatMoney = function(value
, onlyNumber
, format
) {
1517 var precision
, decimal, separator
, sign
, i
, j
, result
;
1518 if (value
=== 'init' && format
) {
1519 currencyTemplate
= format
;
1522 format
= format
|| currencyTemplate
;
1523 if ((result
= /1(.?)234(.?)56/.exec(format
)) !== null) { // If value is formatted to 2 decimals
1526 else if ((result
= /1(.?)234(.?)6/.exec(format
)) !== null) { // If value is formatted to 1 decimal
1529 else if ((result
= /1(.?)235/.exec(format
)) !== null) { // If value is formatted to zero decimals
1533 return 'Invalid format passed to CRM.formatMoney';
1535 separator
= result
[1];
1536 decimal = precision
? result
[2] : false;
1537 sign
= (value
< 0) ? '-' : '';
1538 //extracting the absolute value of the integer part of the number and converting to string
1539 i
= parseInt(value
= Math
.abs(value
).toFixed(2)) + '';
1540 j
= ((j
= i
.length
) > 3) ? j
% 3 : 0;
1541 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) : '');
1545 switch (precision
) {
1547 return format
.replace(/1.*234.*56/, result
);
1549 return format
.replace(/1.*234.*6/, result
);
1551 return format
.replace(/1.*235/, result
);
1555 CRM
.angRequires = function(name
) {
1556 return CRM
.angular
.requires
[name
] || [];
1559 CRM
.console = function(method
, title
, msg
) {
1560 if (window
.console
) {
1561 method
= $.isFunction(console
[method
]) ? method
: 'log';
1562 if (msg
=== undefined) {
1563 return console
[method
](title
);
1565 return console
[method
](title
, msg
);
1570 // Sugar methods for window.localStorage, with a fallback for older browsers
1571 var cacheItems
= {};
1573 get: function (name
, defaultValue
) {
1575 if (localStorage
.getItem('CRM' + name
) !== null) {
1576 return JSON
.parse(localStorage
.getItem('CRM' + name
));
1579 return cacheItems
[name
] === undefined ? defaultValue
: cacheItems
[name
];
1581 set: function (name
, value
) {
1583 localStorage
.setItem('CRM' + name
, JSON
.stringify(value
));
1585 cacheItems
[name
] = value
;
1587 clear: function(name
) {
1589 localStorage
.removeItem('CRM' + name
);
1591 delete cacheItems
[name
];
1597 // Determine if a user has a given permission.
1598 // @see CRM_Core_Resources::addPermissions
1599 CRM
.checkPerm = function(perm
) {
1600 return CRM
.permissions
&& CRM
.permissions
[perm
];
1603 // Round while preserving sigfigs
1604 CRM
.utils
.sigfig = function(n
, digits
) {
1605 var len
= ("" + n
).length
;
1606 var scale
= Math
.pow(10.0, len
-digits
);
1607 return Math
.round(n
/ scale
) * scale
;
1611 * Create a js Date object from a unix timestamp or a yyyy-mm-dd string
1615 CRM
.utils
.makeDate = function(input
) {
1616 switch (typeof input
) {
1618 // already a date object
1622 // convert iso format with or without dashes
1623 input
= input
.replace(/[- :]/g, '');
1624 var output
= $.datepicker
.parseDate('yymmdd', input
.substr(0, 8));
1625 if (input
.length
=== 14) {
1627 parseInt(input
.substr(8, 2), 10),
1628 parseInt(input
.substr(10, 2), 10),
1629 parseInt(input
.substr(12, 2), 10)
1635 // convert unix timestamp
1636 return new Date(input
* 1000);
1638 throw 'Invalid input passed to CRM.utils.makeDate';
1642 * Format a date (and optionally time) for output to the user
1644 * @param {string|int|Date} input
1645 * Input may be a js Date object, a unix timestamp or a 'yyyy-mm-dd' string
1646 * @param {string|null} dateFormat
1647 * A string like 'yy-mm-dd' or null to use the system default
1648 * @param {int|bool} timeFormat
1649 * Leave empty to omit time from the output (default)
1650 * Or pass 12, 24, or true to use the system default for 12/24hr format
1653 CRM
.utils
.formatDate = function(input
, dateFormat
, timeFormat
) {
1657 var date
= CRM
.utils
.makeDate(input
),
1658 output
= $.datepicker
.formatDate(dateFormat
|| CRM
.config
.dateInputFormat
, date
);
1660 var hour
= date
.getHours(),
1661 min
= date
.getMinutes(),
1663 if (timeFormat
=== 12 || (timeFormat
=== true && !CRM
.config
.timeIs24Hr
)) {
1664 suf
= ' ' + (hour
< 12 ? ts('AM') : ts('PM'));
1665 if (hour
=== 0 || hour
> 12) {
1666 hour
= Math
.abs(hour
- 12);
1668 } else if (hour
< 10) {
1671 output
+= ' ' + hour
+ ':' + (min
< 10 ? '0' : '') + min
+ suf
;
1676 // Used to set appropriate text color for a given background
1677 CRM
.utils
.colorContrast = function (hexcolor
) {
1678 hexcolor
= hexcolor
.replace(/[ #]/g, '');
1679 var r
= parseInt(hexcolor
.substr(0, 2), 16),
1680 g
= parseInt(hexcolor
.substr(2, 2), 16),
1681 b
= parseInt(hexcolor
.substr(4, 2), 16),
1682 yiq
= ((r
* 299) + (g
* 587) + (b
* 114)) / 1000;
1683 return (yiq
>= 128) ? 'black' : 'white';
1686 // CVE-2015-9251 - Prevent auto-execution of scripts when no explicit dataType was provided
1687 $.ajaxPrefilter(function(s
) {
1688 if (s
.crossDomain
) {
1689 s
.contents
.script
= false;
1693 // 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.
1694 $.htmlPrefilter = function(html
) {
1695 // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
1696 // their XML equivalent: e.g., "<div />" to "<div></div>". This is
1697 // problematic for several reasons, including that it's vulnerable to XSS
1698 // attacks. However, since this was jQuery's behavior for many years, many
1699 // Drupal modules and jQuery plugins may be relying on it. Therefore, we
1700 // preserve that behavior, but for a limited set of tags only, that we believe
1701 // to not be vulnerable. This is the set of HTML tags that satisfy all of the
1702 // following conditions:
1703 // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
1704 // appear in that list, then we don't want to mess with it here either.
1705 // @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
1706 // - A normal element (not a void, template, text, or foreign element).
1707 // @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
1708 // - An element that is still defined by the current HTML specification
1709 // (not a deprecated element), because we do not want to rely on how
1710 // browsers parse deprecated elements.
1711 // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
1712 // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
1713 // designed for fragments, not entire documents.
1714 // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
1715 // regular expression, it didn't match on colgroup, and we don't want to
1716 // introduce a behavior change for that.
1717 var selfClosingTagsToReplace
= [
1718 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
1719 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
1720 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
1721 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
1722 'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
1723 'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
1724 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
1725 'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
1726 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
1727 'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
1730 // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
1731 // two expressions makes it easier to target <a/> without also targeting
1732 // every tag that starts with "a".
1733 var xhtmlRegExpGroup
= '(' + selfClosingTagsToReplace
.join('|') + ')';
1734 var whitespace
= '[\\x20\\t\\r\\n\\f]';
1735 var rxhtmlTagWithoutSpaceOrAttributes
= new RegExp('<' + xhtmlRegExpGroup
+ '\\/>', 'gi');
1736 var rxhtmlTagWithSpaceAndMaybeAttributes
= new RegExp('<' + xhtmlRegExpGroup
+ '(' + whitespace
+ '[^>]*)\\/>', 'gi');
1738 // jQuery 3.5 also fixed a vulnerability for when </select> appears within
1739 // an <option> or <optgroup>, but it did that in local code that we can't
1740 // backport directly. Instead, we filter such cases out. To do so, we need to
1741 // determine when jQuery would otherwise invoke the vulnerable code, which it
1742 // uses this regular expression to determine. The regular expression changed
1743 // for version 3.0.0 and changed again for 3.4.0.
1744 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
1745 // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
1746 // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
1747 var rtagName
= /<([\w:]+)/;
1749 // The regular expression that jQuery uses to determine which self-closing
1750 // tags to expand to open and close tags. This is vulnerable, because it
1751 // matches all tag names except the few excluded ones. We only use this
1752 // expression for determining vulnerability. The expression changed for
1753 // version 3, but we only need to check for vulnerability in versions 1 and 2,
1754 // so we use the expression from those versions.
1755 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
1756 var rxhtmlTag
= /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
1758 // This is how jQuery determines the first tag in the HTML.
1759 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
1760 var tag
= ( rtagName
.exec( html
) || [ "", "" ] )[ 1 ].toLowerCase();
1762 // It is not valid HTML for <option> or <optgroup> to have <select> as
1763 // either a descendant or sibling, and attempts to inject one can cause
1764 // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
1765 // possible XSS attack, reject the entire string.
1766 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
1767 if ((tag
=== 'option' || tag
=== 'optgroup') && html
.match(/<\/?select/i)) {
1771 // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
1772 // the tags in the `selfClosingTagsToReplace` list defined above.
1773 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
1774 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
1775 html
= html
.replace(rxhtmlTagWithoutSpaceOrAttributes
, "<$1></$1>");
1776 html
= html
.replace(rxhtmlTagWithSpaceAndMaybeAttributes
, "<$1$2></$1>");