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();
149 * Old submit-once function. Will be removed soon.
152 function submitOnce(obj
, formId
, procText
) {
153 // if named button clicked, change text
154 if (obj
.value
!= null) {
155 cj('input[name=' + obj
.name
+ ']').val(procText
+ " ...");
157 cj(obj
).closest('form').attr('data-warn-changes', 'false');
158 if (document
.getElementById
) { // disable submit button for newer browsers
159 cj('input[name=' + obj
.name
+ ']').attr("disabled", true);
160 document
.getElementById(formId
).submit();
163 else { // for older browsers
164 if (submitcount
== 0) {
169 alert("Your request is currently being processed ... Please wait.");
176 * Function to show / hide the row in optionFields
178 * @param index string, element whose innerHTML is to hide else will show the hidden row.
180 function showHideRow(index
) {
182 cj('tr#optionField_' + index
).hide();
183 if (cj('table#optionField tr:hidden:first').length
) {
184 cj('div#optionFieldLink').show();
188 cj('table#optionField tr:hidden:first').show();
189 if (!cj('table#optionField tr:hidden:last').length
) {
190 cj('div#optionFieldLink').hide();
196 /* jshint ignore:end */
198 if (!CRM
.utils
) CRM
.utils
= {};
199 if (!CRM
.strings
) CRM
.strings
= {};
200 if (!CRM
.vars
) CRM
.vars
= {};
202 (function ($, _
, undefined) {
204 /* jshint validthis: true */
206 // Theme classes for unattached elements
207 $.fn
.select2
.defaults
.dropdownCssClass
= $.ui
.dialog
.prototype.options
.dialogClass
= 'crm-container';
209 // https://github.com/ivaynberg/select2/pull/2090
210 $.fn
.select2
.defaults
.width
= 'resolve';
212 // Workaround for https://github.com/ivaynberg/select2/issues/1246
213 $.ui
.dialog
.prototype._allowInteraction = function(e
) {
214 return !!$(e
.target
).closest('.ui-dialog, .ui-datepicker, .select2-drop, .cke_dialog, #civicrm-menu').length
;
217 // Implements jQuery hook.prop
218 $.propHooks
.disabled
= {
219 set: function (el
, value
, name
) {
220 // Sync button enabled status with wrapper css
221 if ($(el
).is('span.crm-button > input.crm-form-submit')) {
222 $(el
).parent().toggleClass('crm-button-disabled', !!value
);
224 // Sync button enabled status with dialog button
225 if ($(el
).is('.ui-dialog input.crm-form-submit')) {
226 $(el
).closest('.ui-dialog').find('.ui-dialog-buttonset button[data-identifier='+ $(el
).attr('name') +']').prop('disabled', value
);
228 if ($(el
).is('.crm-form-date-wrapper .crm-hidden-date')) {
229 $(el
).siblings().prop('disabled', value
);
234 var scriptsLoaded
= {};
235 CRM
.loadScript = function(url
, appendCacheCode
) {
236 if (!scriptsLoaded
[url
]) {
237 var script
= document
.createElement('script'),
239 if (appendCacheCode
!== false) {
240 src
+= (_
.includes(url
, '?') ? '&r=' : '?r=') + CRM
.config
.resourceCacheCode
;
242 scriptsLoaded
[url
] = $.Deferred();
243 script
.onload = function () {
244 // Give the script time to execute
245 window
.setTimeout(function () {
246 if (window
.jQuery
=== CRM
.$ && CRM
.CMSjQuery
) {
247 window
.jQuery
= CRM
.CMSjQuery
;
249 scriptsLoaded
[url
].resolve();
252 // Make jQuery global available while script is loading
253 if (window
.jQuery
!== CRM
.$) {
254 CRM
.CMSjQuery
= window
.jQuery
;
255 window
.jQuery
= CRM
.$;
258 document
.getElementsByTagName("head")[0].appendChild(script
);
260 return scriptsLoaded
[url
];
264 * Populate a select list, overwriting the existing options except for the placeholder.
265 * @param select jquery selector - 1 or more select elements
266 * @param options array in format returned by api.getoptions
267 * @param placeholder string|bool - new placeholder or false (default) to keep the old one
268 * @param value string|array - will silently update the element with new value without triggering change
270 CRM
.utils
.setOptions = function(select
, options
, placeholder
, value
) {
271 $(select
).each(function() {
274 val
= value
|| $elect
.val() || [],
275 opts
= placeholder
|| placeholder
=== '' ? '' : '[value!=""]';
276 $elect
.find('option' + opts
).remove();
277 var newOptions
= CRM
.utils
.renderOptions(options
, val
);
278 if (typeof placeholder
=== 'string') {
279 if ($elect
.is('[multiple]')) {
280 select
.attr('placeholder', placeholder
);
282 newOptions
= '<option value="">' + placeholder
+ '</option>' + newOptions
;
285 $elect
.append(newOptions
);
287 $elect
.trigger('crmOptionsUpdated', $.extend({}, options
)).trigger('change');
293 * Render an option list
294 * @param options {array}
295 * @param val {string} default value
296 * @param escapeHtml {bool}
299 CRM
.utils
.renderOptions = function(options
, val
, escapeHtml
) {
301 esc
= escapeHtml
=== false ? _
.identity
: _
.escape
;
302 if (!$.isArray(val
)) {
305 _
.each(options
, function(option
) {
306 if (option
.children
) {
307 rendered
+= '<optgroup label="' + esc(option
.value
) + '">' +
308 CRM
.utils
.renderOptions(option
.children
, val
) +
311 var selected
= ($.inArray('' + option
.key
, val
) > -1) ? 'selected="selected"' : '';
312 rendered
+= '<option value="' + esc(option
.key
) + '"' + selected
+ '>' + esc(option
.value
) + '</option>';
318 function chainSelect() {
319 var $form
= $(this).closest('form'),
320 $target
= $('select[data-name="' + $(this).data('target') + '"]', $form
),
321 data
= $target
.data(),
323 $target
.prop('disabled', true);
324 if ($target
.is('select.crm-chain-select-control')) {
325 $('select[data-name="' + $target
.data('target') + '"]', $form
).prop('disabled', true).blur();
327 if (!(val
&& val
.length
)) {
328 CRM
.utils
.setOptions($target
.blur(), [], data
.emptyPrompt
);
330 $target
.addClass('loading');
331 $.getJSON(CRM
.url(data
.callback
), {_value
: val
}, function(vals
) {
332 $target
.prop('disabled', false).removeClass('loading');
333 CRM
.utils
.setOptions($target
, vals
|| [], (vals
&& vals
.length
? data
.selectPrompt
: data
.nonePrompt
));
339 * Compare Form Input values against cached initial value.
341 * @return {Boolean} true if changes have been made.
343 CRM
.utils
.initialValueChanged = function(el
) {
345 $(':input:visible, .select2-container:visible+:input:hidden', el
).not('[type=submit], [type=button], .crm-action-menu, :disabled').each(function () {
347 initialValue
= $(this).data('crm-initial-value'),
348 currentValue
= $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val();
349 // skip change of value for submit buttons
350 if (initialValue
!== undefined && !_
.isEqual(initialValue
, currentValue
)) {
358 * This provides defaults for ui.dialog which either need to be calculated or are different from global defaults
363 CRM
.utils
.adjustDialogDefaults = function(settings
) {
364 settings
= $.extend({width
: '65%', height
: '65%', modal
: true}, settings
|| {});
365 // Support relative height
366 if (typeof settings
.height
=== 'string' && settings
.height
.indexOf('%') > 0) {
367 settings
.height
= parseInt($(window
).height() * (parseFloat(settings
.height
)/100), 10);
369 // Responsive adjustment - increase percent width on small screens
370 if (typeof settings
.width
=== 'string' && settings
.width
.indexOf('%') > 0) {
371 var screenWidth
= $(window
).width(),
372 percentage
= parseInt(settings
.width
.replace('%', ''), 10),
373 gap
= 100-percentage
;
374 if (screenWidth
< 701) {
375 settings
.width
= '100%';
377 else if (screenWidth
< 1400) {
378 settings
.width
= '' + parseInt(percentage
+gap
-((screenWidth
- 700)/7*(gap)/100), 10) + '%';
384 function formatCrmSelect2(row
) {
385 var icon
= row
.icon
|| $(row
.element
).data('icon'),
386 color
= row
.color
|| $(row
.element
).data('color'),
387 description
= row
.description
|| $(row
.element
).data('description'),
390 ret
+= '<i class="crm-i ' + icon
+ '" aria-hidden="true"></i> ';
393 ret
+= '<span class="crm-select-item-color" style="background-color: ' + color
+ '"></span> ';
395 return ret
+ _
.escape(row
.text
) + (description
? '<div class="crm-select2-row-description"><p>' + _
.escape(description
) + '</p></div>' : '');
399 * Helper to generate an icon with alt text.
401 * See also smarty `{icon}` and CRM_Core_Page::crmIcon() functions
404 * The Font Awesome icon class to use.
406 * Alt text to display.
407 * @param mixed condition
408 * This will only display if this is truthy.
411 * The formatted icon markup.
413 CRM
.utils
.formatIcon = function (icon
, text
, condition
) {
414 if (typeof condition
!== 'undefined' && !condition
) {
420 text
= _
.escape(text
);
421 title
= ' title="' + text
+ '"';
422 sr
= '<span class="sr-only">' + text
+ '</span>';
424 return '<i class="crm-i ' + icon
+ '"' + title
+ ' aria-hidden="true"></i>' + sr
;
428 * Wrapper for select2 initialization function; supplies defaults
429 * @param options object
431 $.fn
.crmSelect2 = function(options
) {
432 if (options
=== 'destroy') {
433 return $(this).each(function() {
435 .removeClass('crm-ajax-select')
440 return $(this).each(function () {
445 allowClear
: !$el
.hasClass('required'),
446 formatResult
: formatCrmSelect2
,
447 formatSelection
: formatCrmSelect2
449 // quickform doesn't support optgroups so here's a hack :(
450 $('option[value^=crm_optgroup]', this).each(function () {
451 $(this).nextUntil('option[value^=crm_optgroup]').wrapAll('<optgroup label="' + $(this).text() + '" />');
455 // quickform does not support disabled option, so yet another hack to
456 // add disabled property for option values
457 $('option[value^=crm_disabled_opt]', this).attr('disabled', 'disabled');
459 // Placeholder icon - total hack hikacking the escapeMarkup function but select2 3.5 dosn't have any other callbacks for this :(
460 if ($el
.is('[class*=fa-]')) {
461 settings
.escapeMarkup = function (m
) {
462 var out
= _
.escape(m
),
463 placeholder
= settings
.placeholder
|| $el
.data('placeholder') || $el
.attr('placeholder') || $('option[value=""]', $el
).text();
464 if (m
.length
&& placeholder
=== m
) {
465 iconClass
= $el
.attr('class').match(/(fa-\S*)/)[1];
466 out
= '<i class="crm-i ' + iconClass
+ '" aria-hidden="true"></i> ' + out
;
472 // Use description as title for each option
473 $el
.on('select2-loaded.crmSelect2', function() {
474 $('.crm-select2-row-description', '#select2-drop').each(function() {
475 $(this).closest('.select2-result-label').attr('title', $(this).text());
479 // Defaults for single-selects
480 if ($el
.is('select:not([multiple])')) {
481 settings
.minimumResultsForSearch
= 10;
482 if ($('option:first', this).val() === '') {
483 settings
.placeholderOption
= 'first';
486 $.extend(settings
, $el
.data('select-params') || {}, options
|| {});
488 $el
.addClass('crm-ajax-select');
490 $el
.select2(settings
);
495 * @see CRM_Core_Form::addEntityRef for docs
496 * @param options object
498 $.fn
.crmEntityRef = function(options
) {
499 if (options
=== 'destroy') {
500 return $(this).each(function() {
501 var entity
= $(this).data('api-entity') || '';
504 .removeClass('crm-form-entityref crm-' + _
.kebabCase(entity
) + '-ref')
505 .crmSelect2('destroy');
508 options
= options
|| {};
509 options
.select
= options
.select
|| {};
510 return $(this).each(function() {
512 $el
= $(this).off('.crmEntity'),
513 entity
= options
.entity
|| $el
.data('api-entity') || 'Contact',
515 // Legacy: fix entity name if passed in as snake case
516 if (entity
.charAt(0).toUpperCase() !== entity
.charAt(0)) {
517 entity
= _
.capitalize(_
.camelCase(entity
));
519 $el
.data('api-entity', entity
);
520 $el
.data('select-params', $.extend({}, $el
.data('select-params') || {}, options
.select
));
521 $el
.data('api-params', $.extend(true, {}, $el
.data('api-params') || {}, options
.api
));
522 $el
.data('create-links', options
.create
|| $el
.data('create-links'));
523 $el
.addClass('crm-form-entityref crm-' + _
.kebabCase(entity
) + '-ref');
525 // Use select2 ajax helper instead of CRM.api3 because it provides more value
527 url
: CRM
.url('civicrm/ajax/rest'),
529 data: function (input
, page_num
) {
530 var params
= getEntityRefApiParams($el
);
531 params
.input
= input
;
532 params
.page_num
= page_num
;
534 entity
: $el
.data('api-entity'),
536 json
: JSON
.stringify(params
)
539 results: function(data
) {
540 return {more
: data
.more_results
, results
: data
.values
|| []};
543 minimumInputLength
: 1,
544 formatResult
: CRM
.utils
.formatSelect2Result
,
545 formatSelection
: formatEntityRefSelection
,
546 escapeMarkup
: _
.identity
,
547 initSelection: function($el
, callback
) {
549 multiple
= !!$el
.data('select-params').multiple
,
551 stored
= $el
.data('entity-value') || [];
555 // If we already have this data, just return it
556 if (!_
.xor(val
.split(','), _
.pluck(stored
, 'id')).length
) {
557 callback(multiple
? stored
: stored
[0]);
559 var params
= $.extend({}, $el
.data('api-params') || {}, {id
: val
});
560 CRM
.api3($el
.data('api-entity'), 'getlist', params
).done(function(result
) {
561 callback(multiple
? result
.values
: result
.values
[0]);
562 // Trigger change (store data to avoid an infinite loop of lookups)
563 $el
.data('entity-value', result
.values
).trigger('change');
568 // Create new items inline - works for tags
569 if ($el
.data('create-links') && entity
=== 'Tag') {
570 selectParams
.createSearchChoice = function(term
, data
) {
571 if (!_
.findKey(data
, {label
: term
})) {
572 return {id
: "0", term
: term
, label
: term
+ ' (' + ts('new tag') + ')'};
575 selectParams
.tokenSeparators
= [','];
576 selectParams
.createSearchChoicePosition
= 'bottom';
577 $el
.on('select2-selecting.crmEntity', function(e
) {
580 e
.object
.label
= e
.object
.term
;
581 CRM
.api3(entity
, 'create', $.extend({name
: e
.object
.term
}, $el
.data('api-params').params
|| {}))
582 .done(function(created
) {
584 val
= $el
.select2('val'),
585 data
= $el
.select2('data'),
586 item
= {id
: created
.id
, label
: e
.object
.term
};
588 $el
.select2('data', item
, true);
590 else if ($.isArray(val
) && $.inArray("0", val
) > -1) {
591 _
.remove(data
, {id
: "0"});
593 $el
.select2('data', data
, true);
600 selectParams
.formatInputTooShort = function() {
601 var txt
= $el
.data('select-params').formatInputTooShort
|| $.fn
.select2
.defaults
.formatInputTooShort
.call(this);
602 txt
+= entityRefFiltersMarkup($el
) + renderEntityRefCreateLinks($el
);
605 selectParams
.formatNoMatches = function() {
606 var txt
= $el
.data('select-params').formatNoMatches
|| $.fn
.select2
.defaults
.formatNoMatches
;
607 txt
+= entityRefFiltersMarkup($el
) + renderEntityRefCreateLinks($el
);
610 $el
.on('select2-open.crmEntity', function() {
614 .on('click.crmEntity', 'a.crm-add-entity', function(e
) {
615 var extra
= $el
.data('api-params').extra
,
616 formUrl
= $(this).attr('href') + '&returnExtra=display_name,sort_name' + (extra
? (',' + extra
) : '');
617 $el
.select2('close');
618 CRM
.loadForm(formUrl
, {
619 dialog
: {width
: '50%', height
: 220}
620 }).on('crmFormSuccess', function(e
, data
) {
621 if (data
.status
=== 'success' && data
.id
) {
622 if (!data
.crmMessages
) {
623 CRM
.status(ts('%1 Created', {1: data
.label
|| data
.extra
.display_name
}));
625 data
.label
= data
.label
|| data
.extra
.sort_name
;
626 if ($el
.select2('container').hasClass('select2-container-multi')) {
627 var selection
= $el
.select2('data');
628 selection
.push(data
);
629 $el
.select2('data', selection
, true);
631 $el
.select2('data', data
, true);
637 .on('change.crmEntity', '.crm-entityref-filter-value', function() {
638 var filter
= $el
.data('user-filter') || {};
639 filter
.value
= $(this).val();
640 $(this).toggleClass('active', !!filter
.value
);
641 $el
.data('user-filter', filter
);
642 if (filter
.value
&& $(this).is('select')) {
643 // Once a filter has been chosen, rerender create links and refocus the search box
644 $el
.select2('close');
647 $('.crm-entityref-links', '#select2-drop').replaceWith(renderEntityRefCreateLinks($el
));
650 .on('change.crmEntity', 'select.crm-entityref-filter-key', function() {
651 var filter
= {key
: $(this).val()};
652 $(this).toggleClass('active', !!filter
.key
);
653 $el
.data('user-filter', filter
);
654 renderEntityRefFilterValue($el
);
655 $('.crm-entityref-filter-key', '#select2-drop').focus();
659 $el
.crmSelect2($.extend(settings
, $el
.data('select-params'), selectParams
));
664 * Combine api-params with user-filter
668 function getEntityRefApiParams($el
) {
670 params
= $.extend({params
: {}}, $el
.data('api-params') || {}),
671 // Prevent original data from being modified - $.extend and _.clone don't cut it, they pass nested objects by reference!
672 combined
= _
.cloneDeep(params
),
673 filter
= $.extend({}, $el
.data('user-filter') || {});
674 if (filter
.key
&& filter
.value
) {
675 // Fieldname may be prefixed with joins
676 var fieldName
= _
.last(filter
.key
.split('.'));
677 // Special case for contact type/sub-type combo
678 if (fieldName
=== 'contact_type' && (filter
.value
.indexOf('__') > 0)) {
679 combined
.params
[filter
.key
] = filter
.value
.split('__')[0];
680 combined
.params
[filter
.key
.replace('contact_type', 'contact_sub_type')] = filter
.value
.split('__')[1];
682 // Allow json-encoded api filters e.g. {"BETWEEN":[123,456]}
683 combined
.params
[filter
.key
] = filter
.value
.charAt(0) === '{' ? $.parseJSON(filter
.value
) : filter
.value
;
689 CRM
.utils
.copyAttributes = function ($source
, $target
, attributes
) {
690 _
.each(attributes
, function(name
) {
691 if ($source
.attr(name
) !== undefined) {
692 $target
.attr(name
, $source
.attr(name
));
697 CRM
.utils
.formatSelect2Result = function (row
) {
698 var markup
= '<div class="crm-select2-row">';
699 if (row
.image
!== undefined) {
700 markup
+= '<div class="crm-select2-image"><img src="' + row
.image
+ '"/></div>';
702 else if (row
.icon_class
) {
703 markup
+= '<div class="crm-select2-icon"><div class="crm-icon ' + row
.icon_class
+ '-icon"></div></div>';
705 markup
+= '<div><div class="crm-select2-row-label '+(row
.label_class
|| '')+'">' +
706 (row
.color
? '<span class="crm-select-item-color" style="background-color: ' + row
.color
+ '"></span> ' : '') +
707 _
.escape((row
.prefix
!== undefined ? row
.prefix
+ ' ' : '') + row
.label
+ (row
.suffix
!== undefined ? ' ' + row
.suffix
: '')) +
709 '<div class="crm-select2-row-description">';
710 $.each(row
.description
|| [], function(k
, text
) {
711 markup
+= '<p>' + _
.escape(text
) + '</p> ';
713 markup
+= '</div></div></div>';
717 function formatEntityRefSelection(row
) {
718 return (row
.color
? '<span class="crm-select-item-color" style="background-color: ' + row
.color
+ '"></span> ' : '') +
719 _
.escape((row
.prefix
!== undefined ? row
.prefix
+ ' ' : '') + row
.label
+ (row
.suffix
!== undefined ? ' ' + row
.suffix
: ''));
722 function renderEntityRefCreateLinks($el
) {
724 createLinks
= $el
.data('create-links'),
725 params
= getEntityRefApiParams($el
).params
,
726 entity
= $el
.data('api-entity'),
727 markup
= '<div class="crm-entityref-links">';
728 if (!createLinks
|| (createLinks
=== true && !CRM
.config
.entityRef
.links
[entity
])) {
731 if (createLinks
=== true) {
732 createLinks
= params
.contact_type
? _
.where(CRM
.config
.entityRef
.links
[entity
], {type
: params
.contact_type
}) : CRM
.config
.entityRef
.links
[entity
];
734 _
.each(createLinks
, function(link
) {
735 markup
+= ' <a class="crm-add-entity crm-hover-button" href="' + link
.url
+ '">' +
736 '<i class="crm-i ' + (link
.icon
|| 'fa-plus-circle') + '" aria-hidden="true"></i> ' +
737 _
.escape(link
.label
) + '</a>';
743 function getEntityRefFilters($el
) {
745 entity
= $el
.data('api-entity'),
746 filters
= CRM
.config
.entityRef
.filters
[entity
] || [],
747 params
= $.extend({params
: {}}, $el
.data('api-params') || {}).params
,
749 _
.each(filters
, function(filter
) {
750 _
.defaults(filter
, {type
: 'select', 'attributes': {}, entity
: entity
});
751 if (!params
[filter
.key
]) {
752 // Filter out options if params don't match its condition
753 if (filter
.condition
&& !_
.isMatch(params
, _
.pick(filter
.condition
, _
.keys(params
)))) {
758 else if (filter
.key
== 'contact_type' && typeof params
.contact_sub_type
=== 'undefined') {
766 * Provide markup for entity ref filters
768 function entityRefFiltersMarkup($el
) {
770 filters
= getEntityRefFilters($el
),
771 filter
= $el
.data('user-filter') || {},
772 filterSpec
= filter
.key
? _
.find(filters
, {key
: filter
.key
}) : null;
773 if (!filters
.length
) {
776 var markup
= '<div class="crm-entityref-filters">' +
777 '<select class="crm-entityref-filter-key' + (filter
.key
? ' active' : '') + '">' +
778 '<option value="">' + _
.escape(ts('Refine search...')) + '</option>' +
779 CRM
.utils
.renderOptions(filters
, filter
.key
) +
780 '</select>' + entityRefFilterValueMarkup($el
, filter
, filterSpec
) + '</div>';
785 * Provide markup for entity ref filter value field
787 function entityRefFilterValueMarkup($el
, filter
, filterSpec
) {
791 attributes
= _
.cloneDeep(filterSpec
.attributes
);
792 if (filterSpec
.type
!== 'select') {
793 attributes
.type
= filterSpec
.type
;
794 attributes
.value
= typeof filter
.value
!== 'undefined' ? filter
.value
: '';
796 attributes
.class = 'crm-entityref-filter-value' + (filter
.value
? ' active' : '');
797 $.each(attributes
, function (attr
, val
) {
798 attrs
+= ' ' + attr
+ '="' + val
+ '"';
800 if (filterSpec
.type
=== 'select') {
801 var fieldName
= _
.last(filter
.key
.split('.')),
802 options
= [{key
: '', value
: ts('- select -')}];
803 if (filterSpec
.options
) {
804 options
= options
.concat(getEntityRefFilterOptions(fieldName
, $el
, filterSpec
));
806 markup
= '<select' + attrs
+ '>' + CRM
.utils
.renderOptions(options
, filter
.value
) + '</select>';
808 markup
= '<input' + attrs
+ '/>';
815 * Render the entity ref filter value field
817 function renderEntityRefFilterValue($el
) {
819 filter
= $el
.data('user-filter') || {},
820 filterSpec
= filter
.key
? _
.find(getEntityRefFilters($el
), {key
: filter
.key
}) : null,
821 $keyField
= $('.crm-entityref-filter-key', '#select2-drop'),
824 $('.crm-entityref-filter-value', '#select2-drop').remove();
825 $valField
= $(entityRefFilterValueMarkup($el
, filter
, filterSpec
));
826 $keyField
.after($valField
);
827 if (filterSpec
.type
=== 'select') {
828 loadEntityRefFilterOptions(filter
, filterSpec
, $valField
, $el
);
831 $('.crm-entityref-filter-value', '#select2-drop').hide().val('').change();
836 * Fetch options for a filter from cache or ajax api
838 function loadEntityRefFilterOptions(filter
, filterSpec
, $valField
, $el
) {
839 // Fieldname may be prefixed with joins - strip those out
840 var fieldName
= _
.last(filter
.key
.split('.'));
841 if (filterSpec
.options
) {
842 CRM
.utils
.setOptions($valField
, getEntityRefFilterOptions(fieldName
, $el
, filterSpec
), false, filter
.value
);
845 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', true);
846 CRM
.api3(filterSpec
.entity
, 'getoptions', {field
: fieldName
, context
: 'search', sequential
: 1})
847 .done(function(result
) {
848 var entity
= $el
.data('api-entity').toLowerCase();
849 // Store options globally so we don't have to look them up again
850 filterSpec
.options
= result
.values
;
851 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', false);
852 CRM
.utils
.setOptions($valField
, getEntityRefFilterOptions(fieldName
, $el
, filterSpec
), false, filter
.value
);
856 function getEntityRefFilterOptions(fieldName
, $el
, filterSpec
) {
857 var values
= _
.cloneDeep(filterSpec
.options
),
858 params
= $.extend({params
: {}}, $el
.data('api-params') || {}).params
;
859 if (fieldName
=== 'contact_type' && params
.contact_type
) {
860 values
= _
.remove(values
, function(option
) {
861 return option
.key
.indexOf(params
.contact_type
+ '__') === 0;
867 //CRM-15598 - Override url validator method to allow relative url's (e.g. /index.htm)
868 $.validator
.addMethod("url", function(value
, element
) {
869 if (/^\//.test(value
)) {
870 // Relative url: prepend dummy path for validation.
871 value
= 'http://domain.tld' + value
;
873 // From jQuery Validation Plugin v1.12.0
874 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
);
878 * Wrapper for jQuery validate initialization function; supplies defaults
880 $.fn
.crmValidate = function(params
) {
881 return $(this).each(function () {
882 var validator
= $(this).validate();
884 validator
.settings
= $.extend({}, validator
.settings
, CRM
.validate
._defaults
, CRM
.validate
.params
);
885 // Call our custom validation handler.
886 $(validator
.currentForm
).on("invalid-form.validate", validator
.settings
.invalidHandler
);
887 // Call any post-initialization callbacks
888 if (CRM
.validate
.functions
&& CRM
.validate
.functions
.length
) {
889 $.each(CRM
.validate
.functions
, function(i
, func
) {
899 function submitOnceForm(e
) {
900 if (e
.isDefaultPrevented()) {
903 if (_
.contains(submitted
, e
.target
)) {
906 submitted
.push(e
.target
);
907 // Spin submit button icon
908 if (submitButton
&& $(submitButton
, e
.target
).length
) {
910 if ($(e
.target
).closest('.ui-dialog .crm-ajax-container')) {
911 var identifier
= $(submitButton
).attr('name') || $(submitButton
).attr('href');
913 submitButton
= $(e
.target
).closest('.ui-dialog').find('button[data-identifier="' + identifier
+ '"]')[0] || submitButton
;
916 var $icon
= $(submitButton
).siblings('.crm-i').add('.crm-i, .ui-icon', submitButton
);
917 $icon
.data('origClass', $icon
.attr('class')).removeClass().addClass('crm-i crm-submit-icon fa-spinner fa-pulse');
921 // If form fails validation, restore button icon and reset the submitted array
922 function submitFormInvalid(form
) {
924 $('.crm-i.crm-submit-icon').each(function() {
925 if ($(this).data('origClass')) {
926 $(this).removeClass().addClass($(this).data('origClass'));
931 // Initialize widgets
933 .on('crmLoad', function(e
) {
934 $('table.row-highlight', e
.target
)
935 .off('.rowHighlight')
936 .on('change.rowHighlight', 'input.select-row, input.select-rows', function (e
, data
) {
937 var filter
, $table
= $(this).closest('table');
938 if ($(this).hasClass('select-rows')) {
939 filter
= $(this).prop('checked') ? ':not(:checked)' : ':checked';
940 $('input.select-row' + filter
, $table
).prop('checked', $(this).prop('checked')).trigger('change', 'master-selected');
943 $(this).closest('tr').toggleClass('crm-row-selected', $(this).prop('checked'));
944 if (data
!== 'master-selected') {
945 $('input.select-rows', $table
).prop('checked', $(".select-row:not(':checked')", $table
).length
< 1);
949 .find('input.select-row:checked').parents('tr').addClass('crm-row-selected');
950 $('.crm-sortable-list', e
.target
).sortable();
951 $('table.crm-sortable', e
.target
).DataTable();
952 $('table.crm-ajax-table', e
.target
).each(function() {
955 script
= CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmAjaxTable.js',
956 $accordion
= $table
.closest('.crm-accordion-wrapper.collapsed, .crm-collapsible.collapsed');
957 // For tables hidden by collapsed accordions, wait.
958 if ($accordion
.length
) {
959 $accordion
.one('crmAccordion:open', function() {
960 CRM
.loadScript(script
).done(function() {
961 $table
.crmAjaxTable();
965 CRM
.loadScript(script
).done(function() {
966 $table
.crmAjaxTable();
970 if ($("input:radio[name=radio_ts]").length
== 1) {
971 $("input:radio[name=radio_ts]").prop("checked", true);
973 $('.crm-select2:not(.select2-offscreen, .select2-container)', e
.target
).crmSelect2();
974 $('.crm-form-entityref:not(.select2-offscreen, .select2-container)', e
.target
).crmEntityRef();
975 $('select.crm-chain-select-control', e
.target
).off('.chainSelect').on('change.chainSelect', chainSelect
);
976 $('.crm-form-text[data-crm-datepicker]', e
.target
).each(function() {
977 $(this).crmDatepicker($(this).data('crmDatepicker'));
979 $('.crm-editable', e
.target
).not('thead *').each(function() {
981 CRM
.loadScript(CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmEditable.js').done(function() {
985 // Cache Form Input initial values
986 $('form[data-warn-changes] :input', e
.target
).each(function() {
987 $(this).data('crm-initial-value', $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val());
989 $('textarea.crm-form-wysiwyg', e
.target
).each(function() {
990 if ($(this).hasClass("collapsed")) {
991 CRM
.wysiwyg
.createCollapsed(this);
993 CRM
.wysiwyg
.create(this);
996 // Submit once handlers
997 $('form[data-submit-once]', e
.target
)
998 .submit(submitOnceForm
)
999 .on('invalid-form', submitFormInvalid
);
1000 $('form[data-submit-once] input[type=submit]', e
.target
).click(function(e
) {
1001 submitButton
= e
.target
;
1004 .on('dialogopen', function(e
) {
1005 var $el
= $(e
.target
);
1006 $('body').addClass('ui-dialog-open');
1007 // Modal dialogs should disable scrollbars
1008 if ($el
.dialog('option', 'modal')) {
1009 $el
.addClass('modal-dialog');
1010 $('body').css({overflow
: 'hidden'});
1012 $el
.parent().find('.ui-dialog-titlebar .ui-icon-closethick').removeClass('ui-icon-closethick').addClass('fa-times');
1013 // Add resize button
1014 if ($el
.parent().hasClass('crm-container') && $el
.dialog('option', 'resizable')) {
1015 $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}));
1016 $('.crm-dialog-titlebar-resize', $el
.parent()).click(function(e
) {
1017 if ($el
.data('origSize')) {
1018 $el
.dialog('option', $el
.data('origSize'));
1019 $el
.data('origSize', null);
1020 $(this).button('option', 'icons', {primary
: 'fa-expand'});
1022 var menuHeight
= $('#civicrm-menu').outerHeight();
1023 if ($('body').hasClass('crm-menubar-below-cms-menu')) {
1024 menuHeight
+= $('#civicrm-menu').offset().top
;
1026 $el
.data('origSize', {
1027 position
: {my
: 'center', at
: 'center center+' + (menuHeight
/ 2), of: window
},
1028 width
: $el
.dialog('option', 'width'),
1029 height
: $el
.dialog('option', 'height')
1031 $el
.dialog('option', {width
: '100%', height
: ($(window
).height() - menuHeight
), position
: {my
: "top", at
: "top+"+menuHeight
, of: window
}});
1032 $(this).button('option', 'icons', {primary
: 'fa-compress'});
1034 $el
.trigger('dialogresize');
1039 .on('dialogclose', function(e
) {
1040 // Restore scrollbars when closing modal
1041 if ($('.ui-dialog .modal-dialog:visible').not(e
.target
).length
< 1) {
1042 $('body').css({overflow
: ''});
1044 if ($('.ui-dialog-content:visible').not(e
.target
).length
< 1) {
1045 $('body').removeClass('ui-dialog-open');
1048 .on('submit', function(e
) {
1049 // CRM-14353 - disable changes warn when submitting a form
1050 $('[data-warn-changes]').attr('data-warn-changes', 'false');
1053 // CRM-14353 - Warn of unsaved changes for forms which have opted in
1054 window
.onbeforeunload = function() {
1055 if (CRM
.utils
.initialValueChanged($('form[data-warn-changes=true]:visible'))) {
1056 return ts('You have unsaved changes.');
1060 $.fn
.crmtooltip = function () {
1062 .on('mouseover', 'a.crm-summary-link:not(.crm-processed)', function (e
) {
1063 $(this).addClass('crm-processed crm-tooltip-active');
1064 var topDistance
= e
.pageY
- $(window
).scrollTop();
1065 if (topDistance
< 300 || topDistance
< $(this).children('.crm-tooltip-wrapper').height()) {
1066 $(this).addClass('crm-tooltip-down');
1068 if (!$(this).children('.crm-tooltip-wrapper').length
) {
1069 $(this).append('<div class="crm-tooltip-wrapper"><div class="crm-tooltip"></div></div>');
1070 $(this).children().children('.crm-tooltip')
1071 .html('<div class="crm-loading-element"></div>')
1075 .on('mouseout', 'a.crm-summary-link', function () {
1076 $(this).removeClass('crm-processed crm-tooltip-active crm-tooltip-down');
1078 .on('click', 'a.crm-summary-link', false);
1081 var helpDisplay
, helpPrevious
;
1082 // Non-ajax example:
1083 // CRM.help('Example title', 'Here is some text to describe this example');
1084 // Ajax example (will load help id "foo" from templates/CRM/bar.tpl):
1085 // CRM.help('Example title', {id: 'foo', file: 'CRM/bar'});
1086 CRM
.help = function (title
, params
, url
) {
1087 var ajax
= typeof params
!== 'string';
1088 if (helpDisplay
&& helpDisplay
.close
) {
1089 // If the same link is clicked twice, just close the display
1090 if (helpDisplay
.isOpen
&& _
.isEqual(helpPrevious
, params
)) {
1091 helpDisplay
.close();
1094 helpDisplay
.close();
1096 helpPrevious
= _
.cloneDeep(params
);
1097 helpDisplay
= CRM
.alert(ajax
? '...' : params
, title
, 'crm-help ' + (ajax
? 'crm-msg-loading' : 'info'), {expires
: 0});
1100 url
= CRM
.url('civicrm/ajax/inline');
1101 params
.class_name
= 'CRM_Core_Page_Inline_Help';
1102 params
.type
= 'page';
1107 success: function (data
) {
1108 $('#crm-notification-container .crm-help .notify-content:last').html(data
);
1109 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('info');
1111 error: function () {
1112 $('#crm-notification-container .crm-help .notify-content:last').html('Unable to load help file.');
1113 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('error');
1119 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1121 CRM
.status = function(options
, deferred
) {
1122 // 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.
1123 if (typeof options
=== 'string') {
1124 return CRM
.status({start
: options
, success
: options
, error
: options
})[deferred
=== 'error' ? 'reject' : 'resolve']();
1126 var opts
= $.extend({
1127 start
: ts('Saving...'),
1128 success
: ts('Saved'),
1129 error: function(data
) {
1130 var msg
= $.isPlainObject(data
) && data
.error_message
;
1131 CRM
.alert(msg
|| ts('Sorry an error occurred and your information was not saved'), ts('Error'), 'error');
1134 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>')
1136 $msg
.css('min-width', $msg
.width());
1137 function handle(status
, data
) {
1138 var endMsg
= typeof(opts
[status
]) === 'function' ? opts
[status
](data
) : opts
[status
];
1140 $msg
.removeClass('status-start').addClass('status-' + status
).find('.crm-status-box-msg').text(endMsg
);
1141 window
.setTimeout(function() {
1142 $msg
.fadeOut('slow', function() {
1150 return (deferred
|| new $.Deferred())
1151 .done(function(data
) {
1152 // If the server returns an error msg call the error handler
1153 var status
= $.isPlainObject(data
) && (data
.is_error
|| data
.status
=== 'error') ? 'error' : 'success';
1154 handle(status
, data
);
1156 .fail(function(data
) {
1157 handle('error', data
);
1161 // Convert an Angular promise to a jQuery promise
1162 CRM
.toJqPromise = function(aPromise
) {
1163 var jqDeferred
= $.Deferred();
1165 function(data
) { jqDeferred
.resolve(data
); },
1166 function(data
) { jqDeferred
.reject(data
); }
1167 // should we also handle progress events?
1169 return jqDeferred
.promise();
1172 CRM
.toAPromise = function($q
, jqPromise
) {
1173 var aDeferred
= $q
.defer();
1175 function(data
) { aDeferred
.resolve(data
); },
1176 function(data
) { aDeferred
.reject(data
); }
1177 // should we also handle progress events?
1179 return aDeferred
.promise
;
1183 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1185 CRM
.alert = function (text
, title
, type
, options
) {
1186 type
= type
|| 'alert';
1187 title
= title
|| '';
1188 options
= options
|| {};
1189 if ($('#crm-notification-container').length
) {
1195 // By default, don't expire errors and messages containing links
1197 expires
: (type
== 'error' || text
.indexOf('<a ') > -1) ? 0 : (text
? 10000 : 5000),
1200 options
= $.extend(extra
, options
);
1201 options
.expires
= (options
.expires
=== false || !CRM
.config
.allowAlertAutodismissal
) ? 0 : parseInt(options
.expires
, 10);
1202 if (options
.unique
&& options
.unique
!== '0') {
1203 $('#crm-notification-container .ui-notify-message').each(function () {
1204 if (title
=== $('h1', this).html() && text
=== $('.notify-content', this).html()) {
1205 $('.icon.ui-notify-close', this).click();
1209 return $('#crm-notification-container').notify('create', params
, options
);
1213 text
= title
+ "\n" + text
;
1215 // strip html tags as they are not parsed in standard alerts
1216 alert($("<div/>").html(text
).text());
1222 * Close whichever alert contains the given node
1226 CRM
.closeAlertByChild = function (node
) {
1227 $(node
).closest('.ui-notify-message').find('.icon.ui-notify-close').click();
1231 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1233 CRM
.confirm = function (options
) {
1234 var dialog
, url
, msg
, buttons
= [], settings
= {
1235 title
: ts('Confirm'),
1236 message
: ts('Are you sure you want to continue?'),
1241 dialogClass
: 'crm-container crm-confirm',
1242 close: function () {
1243 $(this).dialog('destroy').remove();
1250 if (options
&& options
.url
) {
1251 settings
.resizable
= true;
1252 settings
.height
= '50%';
1254 $.extend(settings
, ($.isFunction(options
) ? arguments
[1] : options
) || {});
1255 settings
= CRM
.utils
.adjustDialogDefaults(settings
);
1256 if (!settings
.buttons
&& $.isPlainObject(settings
.options
)) {
1257 $.each(settings
.options
, function(op
, label
) {
1261 icons
: {primary
: op
=== 'no' ? 'fa-times' : 'fa-check'},
1263 var event
= $.Event('crmConfirm:' + op
);
1264 $(this).trigger(event
);
1265 if (!event
.isDefaultPrevented()) {
1266 dialog
.dialog('close');
1271 // Order buttons so that "no" goes on the right-hand side
1272 settings
.buttons
= _
.sortBy(buttons
, 'data-op').reverse();
1275 msg
= url
? '' : settings
.message
;
1276 delete settings
.options
;
1277 delete settings
.message
;
1278 delete settings
.url
;
1279 dialog
= $('<div class="crm-confirm-dialog"></div>').html(msg
|| '').dialog(settings
);
1280 if ($.isFunction(options
)) {
1281 dialog
.on('crmConfirm:yes', options
);
1284 CRM
.loadPage(url
, {target
: dialog
});
1287 dialog
.trigger('crmLoad');
1292 /** provides a local copy of ts for a domain */
1293 CRM
.ts = function(domain
) {
1294 return function(message
, options
) {
1296 options
= $.extend(options
|| {}, {domain
: domain
});
1298 return ts(message
, options
);
1302 CRM
.addStrings = function(domain
, strings
) {
1303 var bucket
= (domain
== 'civicrm' ? 'strings' : 'strings::' + domain
);
1304 CRM
[bucket
] = CRM
[bucket
] || {};
1305 _
.extend(CRM
[bucket
], strings
);
1309 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1311 $.fn
.crmError = function (text
, title
, options
) {
1312 title
= title
|| '';
1314 options
= options
|| {};
1319 if ($(this).length
) {
1321 var label
= $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
1323 label
.addClass('crm-error');
1324 var $label
= label
.clone();
1325 if (text
=== '' && $('.crm-marker', $label
).length
> 0) {
1326 text
= $('.crm-marker', $label
).attr('title');
1328 $('.crm-marker', $label
).remove();
1329 title
= $label
.text();
1332 $(this).addClass('crm-error');
1334 var msg
= CRM
.alert(text
, title
, 'error', $.extend(extra
, options
));
1335 if ($(this).length
) {
1337 setTimeout(function () {
1338 ele
.one('change', function () {
1339 if (msg
&& msg
.close
) msg
.close();
1340 ele
.removeClass('error');
1341 label
.removeClass('crm-error');
1348 // Display system alerts through js notifications
1349 function messagesFromMarkup() {
1350 $('div.messages:visible', this).not('.help').not('.no-popup').each(function () {
1351 var text
, title
= '';
1352 $(this).removeClass('status messages');
1353 var type
= $(this).attr('class').split(' ')[0] || 'alert';
1354 type
= type
.replace('crm-', '');
1355 $('.icon', this).remove();
1356 if ($('.msg-text', this).length
> 0) {
1357 text
= $('.msg-text', this).html();
1358 title
= $('.msg-title', this).html();
1361 text
= $(this).html();
1363 var options
= $(this).data('options') || {};
1365 // Duplicates were already removed server-side
1366 options
.unique
= false;
1367 CRM
.alert(text
, title
, type
, options
);
1369 // Handle qf form errors
1370 $('form :input.error', this).one('blur', function() {
1371 $('.ui-notify-message.error a.ui-notify-close').click();
1372 $(this).removeClass('error');
1373 $(this).next('span.crm-error').remove();
1374 $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]')
1375 .removeClass('crm-error')
1376 .find('.crm-error').removeClass('crm-error');
1381 * Improve blockUI when used with jQuery dialog
1383 var originalBlock
= $.fn
.block
,
1384 originalUnblock
= $.fn
.unblock
;
1386 $.fn
.block = function(opts
) {
1387 if ($(this).is('.ui-dialog-content')) {
1388 originalBlock
.call($(this).parents('.ui-dialog'), opts
);
1391 return originalBlock
.call(this, opts
);
1393 $.fn
.unblock = function(opts
) {
1394 if ($(this).is('.ui-dialog-content')) {
1395 originalUnblock
.call($(this).parents('.ui-dialog'), opts
);
1398 return originalUnblock
.call(this, opts
);
1401 // Preprocess all CRM ajax calls to display messages
1402 $(document
).ajaxSuccess(function(event
, xhr
, settings
) {
1404 if ((!settings
.dataType
|| settings
.dataType
== 'json') && xhr
.responseText
) {
1405 var response
= $.parseJSON(xhr
.responseText
);
1406 if (typeof(response
.crmMessages
) == 'object') {
1407 $.each(response
.crmMessages
, function(n
, msg
) {
1408 CRM
.alert(msg
.text
, msg
.title
, msg
.type
, msg
.options
);
1411 if (response
.backtrace
) {
1412 CRM
.console('log', response
.backtrace
);
1414 if (typeof response
.deprecated
=== 'string') {
1415 CRM
.console('warn', response
.deprecated
);
1419 // Ignore errors thrown by parseJSON
1424 $.blockUI
.defaults
.message
= null;
1425 $.blockUI
.defaults
.ignoreIfBlocked
= true;
1427 if ($('#crm-container').hasClass('crm-public')) {
1428 $.fn
.select2
.defaults
.dropdownCssClass
= $.ui
.dialog
.prototype.options
.dialogClass
= 'crm-container crm-public';
1431 // Trigger crmLoad on initial content for consistency. It will also be triggered for ajax-loaded content.
1432 $('.crm-container').trigger('crmLoad');
1434 if ($('#crm-notification-container').length
) {
1435 // Initialize notifications
1436 $('#crm-notification-container').notify();
1437 messagesFromMarkup
.call($('#crm-container'));
1441 // bind the event for image popup
1442 .on('click', 'a.crm-image-popup', function(e
) {
1444 title
: ts('Preview'),
1446 // Prevent overlap with the menubar
1447 maxHeight
: $(window
).height() - 30,
1448 position
: {my
: 'center', at
: 'center center+15', of: window
},
1449 message
: '<div class="crm-custom-image-popup"><img style="max-width: 100%" src="' + $(this).attr('href') + '"></div>',
1455 .on('click', function (event
) {
1456 $('.btn-slide-active').removeClass('btn-slide-active').find('.panel').hide();
1457 if ($(event
.target
).is('.btn-slide')) {
1458 $(event
.target
).addClass('btn-slide-active').find('.panel').show();
1462 // Handle clear button for form elements
1463 .on('click', 'a.crm-clear-link', function() {
1464 $(this).css({visibility
: 'hidden'}).siblings('.crm-form-radio:checked').prop('checked', false).trigger('change', ['crmClear']);
1465 $(this).siblings('input:text').val('').trigger('change', ['crmClear']);
1468 .on('change keyup', 'input.crm-form-radio:checked, input[allowclear=1]', function(e
, context
) {
1469 if (context
!== 'crmClear' && ($(this).is(':checked') || ($(this).is('[allowclear=1]') && $(this).val()))) {
1470 $(this).siblings('.crm-clear-link').css({visibility
: ''});
1472 if (context
!== 'crmClear' && $(this).is('[allowclear=1]') && $(this).val() === '') {
1473 $(this).siblings('.crm-clear-link').css({visibility
: 'hidden'});
1477 // Allow normal clicking of links within accordions
1478 .on('click.crmAccordions', 'div.crm-accordion-header a, .collapsible-title a', function (e
) {
1479 e
.stopPropagation();
1481 // Handle accordions
1482 .on('click.crmAccordions', '.crm-accordion-header, .crm-collapsible .collapsible-title', function (e
) {
1483 var action
= 'open';
1484 if ($(this).parent().hasClass('collapsed')) {
1485 $(this).next().css('display', 'none').slideDown(200);
1488 $(this).next().css('display', 'block').slideUp(200);
1491 $(this).parent().toggleClass('collapsed').trigger('crmAccordion:' + action
);
1499 * Collapse or expand an accordion
1502 $.fn
.crmAccordionToggle = function (speed
) {
1503 $(this).each(function () {
1504 var action
= 'open';
1505 if ($(this).hasClass('collapsed')) {
1506 $('.crm-accordion-body', this).first().css('display', 'none').slideDown(speed
);
1509 $('.crm-accordion-body', this).first().css('display', 'block').slideUp(speed
);
1512 $(this).toggleClass('collapsed').trigger('crmAccordion:' + action
);
1517 * Clientside currency formatting
1518 * @param number value
1519 * @param [optional] boolean onlyNumber - if true, we return formatted amount without currency sign
1520 * @param [optional] string format - currency representation of the number 1234.56
1523 var currencyTemplate
;
1524 CRM
.formatMoney = function(value
, onlyNumber
, format
) {
1525 var precision
, decimal, separator
, sign
, i
, j
, result
;
1526 if (value
=== 'init' && format
) {
1527 currencyTemplate
= format
;
1530 format
= format
|| currencyTemplate
;
1531 if ((result
= /1(.?)234(.?)56/.exec(format
)) !== null) { // If value is formatted to 2 decimals
1534 else if ((result
= /1(.?)234(.?)6/.exec(format
)) !== null) { // If value is formatted to 1 decimal
1537 else if ((result
= /1(.?)235/.exec(format
)) !== null) { // If value is formatted to zero decimals
1541 return 'Invalid format passed to CRM.formatMoney';
1543 separator
= result
[1];
1544 decimal = precision
? result
[2] : false;
1545 sign
= (value
< 0) ? '-' : '';
1546 //extracting the absolute value of the integer part of the number and converting to string
1547 i
= parseInt(value
= Math
.abs(value
).toFixed(2)) + '';
1548 j
= ((j
= i
.length
) > 3) ? j
% 3 : 0;
1549 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) : '');
1553 switch (precision
) {
1555 return format
.replace(/1.*234.*56/, result
);
1557 return format
.replace(/1.*234.*6/, result
);
1559 return format
.replace(/1.*235/, result
);
1563 CRM
.angRequires = function(name
) {
1564 return CRM
.angular
.requires
[name
] || [];
1567 CRM
.console = function(method
, title
, msg
) {
1568 if (window
.console
) {
1569 method
= $.isFunction(console
[method
]) ? method
: 'log';
1570 if (msg
=== undefined) {
1571 return console
[method
](title
);
1573 return console
[method
](title
, msg
);
1578 // Sugar methods for window.localStorage, with a fallback for older browsers
1579 var cacheItems
= {};
1581 get: function (name
, defaultValue
) {
1583 if (localStorage
.getItem('CRM' + name
) !== null) {
1584 return JSON
.parse(localStorage
.getItem('CRM' + name
));
1587 return cacheItems
[name
] === undefined ? defaultValue
: cacheItems
[name
];
1589 set: function (name
, value
) {
1591 localStorage
.setItem('CRM' + name
, JSON
.stringify(value
));
1593 cacheItems
[name
] = value
;
1595 clear: function(name
) {
1597 localStorage
.removeItem('CRM' + name
);
1599 delete cacheItems
[name
];
1605 // Determine if a user has a given permission.
1606 // @see CRM_Core_Resources::addPermissions
1607 CRM
.checkPerm = function(perm
) {
1608 return CRM
.permissions
&& CRM
.permissions
[perm
];
1611 // Round while preserving sigfigs
1612 CRM
.utils
.sigfig = function(n
, digits
) {
1613 var len
= ("" + n
).length
;
1614 var scale
= Math
.pow(10.0, len
-digits
);
1615 return Math
.round(n
/ scale
) * scale
;
1619 * Create a js Date object from a unix timestamp or a yyyy-mm-dd string
1623 CRM
.utils
.makeDate = function(input
) {
1624 switch (typeof input
) {
1626 // already a date object
1630 // convert iso format with or without dashes
1631 input
= input
.replace(/[- :]/g, '');
1632 var output
= $.datepicker
.parseDate('yymmdd', input
.substr(0, 8));
1633 if (input
.length
=== 14) {
1635 parseInt(input
.substr(8, 2), 10),
1636 parseInt(input
.substr(10, 2), 10),
1637 parseInt(input
.substr(12, 2), 10)
1643 // convert unix timestamp
1644 return new Date(input
* 1000);
1646 throw 'Invalid input passed to CRM.utils.makeDate';
1650 * Format a date (and optionally time) for output to the user
1652 * @param {string|int|Date} input
1653 * Input may be a js Date object, a unix timestamp or a 'yyyy-mm-dd' string
1654 * @param {string|null} dateFormat
1655 * A string like 'yy-mm-dd' or null to use the system default
1656 * @param {int|bool} timeFormat
1657 * Leave empty to omit time from the output (default)
1658 * Or pass 12, 24, or true to use the system default for 12/24hr format
1661 CRM
.utils
.formatDate = function(input
, dateFormat
, timeFormat
) {
1665 var date
= CRM
.utils
.makeDate(input
),
1666 output
= $.datepicker
.formatDate(dateFormat
|| CRM
.config
.dateInputFormat
, date
);
1668 var hour
= date
.getHours(),
1669 min
= date
.getMinutes(),
1671 if (timeFormat
=== 12 || (timeFormat
=== true && !CRM
.config
.timeIs24Hr
)) {
1672 suf
= ' ' + (hour
< 12 ? ts('AM') : ts('PM'));
1673 if (hour
=== 0 || hour
> 12) {
1674 hour
= Math
.abs(hour
- 12);
1676 } else if (hour
< 10) {
1679 output
+= ' ' + hour
+ ':' + (min
< 10 ? '0' : '') + min
+ suf
;
1684 // Used to set appropriate text color for a given background
1685 CRM
.utils
.colorContrast = function (hexcolor
) {
1686 hexcolor
= hexcolor
.replace(/[ #]/g, '');
1687 var r
= parseInt(hexcolor
.substr(0, 2), 16),
1688 g
= parseInt(hexcolor
.substr(2, 2), 16),
1689 b
= parseInt(hexcolor
.substr(4, 2), 16),
1690 yiq
= ((r
* 299) + (g
* 587) + (b
* 114)) / 1000;
1691 return (yiq
>= 128) ? 'black' : 'white';
1694 // CVE-2015-9251 - Prevent auto-execution of scripts when no explicit dataType was provided
1695 $.ajaxPrefilter(function(s
) {
1696 if (s
.crossDomain
) {
1697 s
.contents
.script
= false;
1701 // 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.
1702 $.htmlPrefilter = function(html
) {
1703 // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
1704 // their XML equivalent: e.g., "<div />" to "<div></div>". This is
1705 // problematic for several reasons, including that it's vulnerable to XSS
1706 // attacks. However, since this was jQuery's behavior for many years, many
1707 // Drupal modules and jQuery plugins may be relying on it. Therefore, we
1708 // preserve that behavior, but for a limited set of tags only, that we believe
1709 // to not be vulnerable. This is the set of HTML tags that satisfy all of the
1710 // following conditions:
1711 // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
1712 // appear in that list, then we don't want to mess with it here either.
1713 // @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
1714 // - A normal element (not a void, template, text, or foreign element).
1715 // @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
1716 // - An element that is still defined by the current HTML specification
1717 // (not a deprecated element), because we do not want to rely on how
1718 // browsers parse deprecated elements.
1719 // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
1720 // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
1721 // designed for fragments, not entire documents.
1722 // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
1723 // regular expression, it didn't match on colgroup, and we don't want to
1724 // introduce a behavior change for that.
1725 var selfClosingTagsToReplace
= [
1726 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
1727 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
1728 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
1729 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
1730 'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
1731 'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
1732 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
1733 'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
1734 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
1735 'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
1738 // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
1739 // two expressions makes it easier to target <a/> without also targeting
1740 // every tag that starts with "a".
1741 var xhtmlRegExpGroup
= '(' + selfClosingTagsToReplace
.join('|') + ')';
1742 var whitespace
= '[\\x20\\t\\r\\n\\f]';
1743 var rxhtmlTagWithoutSpaceOrAttributes
= new RegExp('<' + xhtmlRegExpGroup
+ '\\/>', 'gi');
1744 var rxhtmlTagWithSpaceAndMaybeAttributes
= new RegExp('<' + xhtmlRegExpGroup
+ '(' + whitespace
+ '[^>]*)\\/>', 'gi');
1746 // jQuery 3.5 also fixed a vulnerability for when </select> appears within
1747 // an <option> or <optgroup>, but it did that in local code that we can't
1748 // backport directly. Instead, we filter such cases out. To do so, we need to
1749 // determine when jQuery would otherwise invoke the vulnerable code, which it
1750 // uses this regular expression to determine. The regular expression changed
1751 // for version 3.0.0 and changed again for 3.4.0.
1752 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
1753 // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
1754 // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
1755 var rtagName
= /<([\w:]+)/;
1757 // The regular expression that jQuery uses to determine which self-closing
1758 // tags to expand to open and close tags. This is vulnerable, because it
1759 // matches all tag names except the few excluded ones. We only use this
1760 // expression for determining vulnerability. The expression changed for
1761 // version 3, but we only need to check for vulnerability in versions 1 and 2,
1762 // so we use the expression from those versions.
1763 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
1764 var rxhtmlTag
= /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
1766 // This is how jQuery determines the first tag in the HTML.
1767 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
1768 var tag
= ( rtagName
.exec( html
) || [ "", "" ] )[ 1 ].toLowerCase();
1770 // It is not valid HTML for <option> or <optgroup> to have <select> as
1771 // either a descendant or sibling, and attempts to inject one can cause
1772 // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
1773 // possible XSS attack, reject the entire string.
1774 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
1775 if ((tag
=== 'option' || tag
=== 'optgroup') && html
.match(/<\/?select/i)) {
1779 // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
1780 // the tags in the `selfClosingTagsToReplace` list defined above.
1781 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
1782 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
1783 html
= html
.replace(rxhtmlTagWithoutSpaceOrAttributes
, "<$1></$1>");
1784 html
= html
.replace(rxhtmlTagWithSpaceAndMaybeAttributes
, "<$1$2></$1>");