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 (typeof placeholder
=== 'string') {
253 if ($elect
.is('[multiple]')) {
254 select
.attr('placeholder', placeholder
);
256 newOptions
= '<option value="">' + placeholder
+ '</option>' + newOptions
;
259 $elect
.append(newOptions
);
261 $elect
.trigger('crmOptionsUpdated', $.extend({}, options
)).trigger('change');
267 * Render an option list
268 * @param options {array}
269 * @param val {string} default value
270 * @param escapeHtml {bool}
273 CRM
.utils
.renderOptions = function(options
, val
, escapeHtml
) {
275 esc
= escapeHtml
=== false ? _
.identity
: _
.escape
;
276 if (!$.isArray(val
)) {
279 _
.each(options
, function(option
) {
280 if (option
.children
) {
281 rendered
+= '<optgroup label="' + esc(option
.value
) + '">' +
282 CRM
.utils
.renderOptions(option
.children
, val
) +
285 var selected
= ($.inArray('' + option
.key
, val
) > -1) ? 'selected="selected"' : '';
286 rendered
+= '<option value="' + esc(option
.key
) + '"' + selected
+ '>' + esc(option
.value
) + '</option>';
292 function chainSelect() {
293 var $form
= $(this).closest('form'),
294 $target
= $('select[data-name="' + $(this).data('target') + '"]', $form
),
295 data
= $target
.data(),
297 $target
.prop('disabled', true);
298 if ($target
.is('select.crm-chain-select-control')) {
299 $('select[data-name="' + $target
.data('target') + '"]', $form
).prop('disabled', true).blur();
301 if (!(val
&& val
.length
)) {
302 CRM
.utils
.setOptions($target
.blur(), [], data
.emptyPrompt
);
304 $target
.addClass('loading');
305 $.getJSON(CRM
.url(data
.callback
), {_value
: val
}, function(vals
) {
306 $target
.prop('disabled', false).removeClass('loading');
307 CRM
.utils
.setOptions($target
, vals
|| [], (vals
&& vals
.length
? data
.selectPrompt
: data
.nonePrompt
));
313 * Compare Form Input values against cached initial value.
315 * @return {Boolean} true if changes have been made.
317 CRM
.utils
.initialValueChanged = function(el
) {
319 $(':input:visible, .select2-container:visible+:input:hidden', el
).not('[type=submit], [type=button], .crm-action-menu, :disabled').each(function () {
321 initialValue
= $(this).data('crm-initial-value'),
322 currentValue
= $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val();
323 // skip change of value for submit buttons
324 if (initialValue
!== undefined && !_
.isEqual(initialValue
, currentValue
)) {
332 * This provides defaults for ui.dialog which either need to be calculated or are different from global defaults
337 CRM
.utils
.adjustDialogDefaults = function(settings
) {
338 settings
= $.extend({width
: '65%', height
: '65%', modal
: true}, settings
|| {});
339 // Support relative height
340 if (typeof settings
.height
=== 'string' && settings
.height
.indexOf('%') > 0) {
341 settings
.height
= parseInt($(window
).height() * (parseFloat(settings
.height
)/100), 10);
343 // Responsive adjustment - increase percent width on small screens
344 if (typeof settings
.width
=== 'string' && settings
.width
.indexOf('%') > 0) {
345 var screenWidth
= $(window
).width(),
346 percentage
= parseInt(settings
.width
.replace('%', ''), 10),
347 gap
= 100-percentage
;
348 if (screenWidth
< 701) {
349 settings
.width
= '100%';
351 else if (screenWidth
< 1400) {
352 settings
.width
= '' + parseInt(percentage
+gap
-((screenWidth
- 700)/7*(gap)/100), 10) + '%';
358 function formatCrmSelect2(row
) {
359 var icon
= row
.icon
|| $(row
.element
).data('icon'),
360 color
= row
.color
|| $(row
.element
).data('color'),
361 description
= row
.description
|| $(row
.element
).data('description'),
364 ret
+= '<i class="crm-i ' + icon
+ '" aria-hidden="true"></i> ';
367 ret
+= '<span class="crm-select-item-color" style="background-color: ' + color
+ '"></span> ';
369 return ret
+ _
.escape(row
.text
) + (description
? '<div class="crm-select2-row-description"><p>' + _
.escape(description
) + '</p></div>' : '');
373 * Helper to generate an icon with alt text.
375 * See also smarty `{icon}` and CRM_Core_Page::crmIcon() functions
378 * The Font Awesome icon class to use.
380 * Alt text to display.
381 * @param mixed condition
382 * This will only display if this is truthy.
385 * The formatted icon markup.
387 CRM
.utils
.formatIcon = function (icon
, text
, condition
) {
388 if (typeof condition
!== 'undefined' && !condition
) {
394 text
= _
.escape(text
);
395 title
= ' title="' + text
+ '"';
396 sr
= '<span class="sr-only">' + text
+ '</span>';
398 return '<i class="crm-i ' + icon
+ '"' + title
+ ' aria-hidden="true"></i>' + sr
;
402 * Wrapper for select2 initialization function; supplies defaults
403 * @param options object
405 $.fn
.crmSelect2 = function(options
) {
406 if (options
=== 'destroy') {
407 return $(this).each(function() {
409 .removeClass('crm-ajax-select')
414 return $(this).each(function () {
419 allowClear
: !$el
.hasClass('required'),
420 formatResult
: formatCrmSelect2
,
421 formatSelection
: formatCrmSelect2
423 // quickform doesn't support optgroups so here's a hack :(
424 $('option[value^=crm_optgroup]', this).each(function () {
425 $(this).nextUntil('option[value^=crm_optgroup]').wrapAll('<optgroup label="' + $(this).text() + '" />');
429 // quickform does not support disabled option, so yet another hack to
430 // add disabled property for option values
431 $('option[value^=crm_disabled_opt]', this).attr('disabled', 'disabled');
433 // Placeholder icon - total hack hikacking the escapeMarkup function but select2 3.5 dosn't have any other callbacks for this :(
434 if ($el
.is('[class*=fa-]')) {
435 settings
.escapeMarkup = function (m
) {
436 var out
= _
.escape(m
),
437 placeholder
= settings
.placeholder
|| $el
.data('placeholder') || $el
.attr('placeholder') || $('option[value=""]', $el
).text();
438 if (m
.length
&& placeholder
=== m
) {
439 iconClass
= $el
.attr('class').match(/(fa-\S*)/)[1];
440 out
= '<i class="crm-i ' + iconClass
+ '" aria-hidden="true"></i> ' + out
;
446 // Use description as title for each option
447 $el
.on('select2-loaded.crmSelect2', function() {
448 $('.crm-select2-row-description', '#select2-drop').each(function() {
449 $(this).closest('.select2-result-label').attr('title', $(this).text());
453 // Defaults for single-selects
454 if ($el
.is('select:not([multiple])')) {
455 settings
.minimumResultsForSearch
= 10;
456 if ($('option:first', this).val() === '') {
457 settings
.placeholderOption
= 'first';
460 $.extend(settings
, $el
.data('select-params') || {}, options
|| {});
462 $el
.addClass('crm-ajax-select');
464 $el
.select2(settings
);
469 * @see CRM_Core_Form::addEntityRef for docs
470 * @param options object
472 $.fn
.crmEntityRef = function(options
) {
473 if (options
=== 'destroy') {
474 return $(this).each(function() {
475 var entity
= $(this).data('api-entity') || '';
478 .removeClass('crm-form-entityref crm-' + _
.kebabCase(entity
) + '-ref')
479 .crmSelect2('destroy');
482 options
= options
|| {};
483 options
.select
= options
.select
|| {};
484 return $(this).each(function() {
486 $el
= $(this).off('.crmEntity'),
487 entity
= options
.entity
|| $el
.data('api-entity') || 'Contact',
489 // Legacy: fix entity name if passed in as snake case
490 if (entity
.charAt(0).toUpperCase() !== entity
.charAt(0)) {
491 entity
= _
.capitalize(_
.camelCase(entity
));
493 $el
.data('api-entity', entity
);
494 $el
.data('select-params', $.extend({}, $el
.data('select-params') || {}, options
.select
));
495 $el
.data('api-params', $.extend(true, {}, $el
.data('api-params') || {}, options
.api
));
496 $el
.data('create-links', options
.create
|| $el
.data('create-links'));
497 $el
.addClass('crm-form-entityref crm-' + _
.kebabCase(entity
) + '-ref');
499 // Use select2 ajax helper instead of CRM.api3 because it provides more value
501 url
: CRM
.url('civicrm/ajax/rest'),
503 data: function (input
, page_num
) {
504 var params
= getEntityRefApiParams($el
);
505 params
.input
= input
;
506 params
.page_num
= page_num
;
508 entity
: $el
.data('api-entity'),
510 json
: JSON
.stringify(params
)
513 results: function(data
) {
514 return {more
: data
.more_results
, results
: data
.values
|| []};
517 minimumInputLength
: 1,
518 formatResult
: CRM
.utils
.formatSelect2Result
,
519 formatSelection
: formatEntityRefSelection
,
520 escapeMarkup
: _
.identity
,
521 initSelection: function($el
, callback
) {
523 multiple
= !!$el
.data('select-params').multiple
,
525 stored
= $el
.data('entity-value') || [];
529 // If we already have this data, just return it
530 if (!_
.xor(val
.split(','), _
.pluck(stored
, 'id')).length
) {
531 callback(multiple
? stored
: stored
[0]);
533 var params
= $.extend({}, $el
.data('api-params') || {}, {id
: val
});
534 CRM
.api3($el
.data('api-entity'), 'getlist', params
).done(function(result
) {
535 callback(multiple
? result
.values
: result
.values
[0]);
536 // Trigger change (store data to avoid an infinite loop of lookups)
537 $el
.data('entity-value', result
.values
).trigger('change');
542 // Create new items inline - works for tags
543 if ($el
.data('create-links') && entity
=== 'Tag') {
544 selectParams
.createSearchChoice = function(term
, data
) {
545 if (!_
.findKey(data
, {label
: term
})) {
546 return {id
: "0", term
: term
, label
: term
+ ' (' + ts('new tag') + ')'};
549 selectParams
.tokenSeparators
= [','];
550 selectParams
.createSearchChoicePosition
= 'bottom';
551 $el
.on('select2-selecting.crmEntity', function(e
) {
554 e
.object
.label
= e
.object
.term
;
555 CRM
.api3(entity
, 'create', $.extend({name
: e
.object
.term
}, $el
.data('api-params').params
|| {}))
556 .done(function(created
) {
558 val
= $el
.select2('val'),
559 data
= $el
.select2('data'),
560 item
= {id
: created
.id
, label
: e
.object
.term
};
562 $el
.select2('data', item
, true);
564 else if ($.isArray(val
) && $.inArray("0", val
) > -1) {
565 _
.remove(data
, {id
: "0"});
567 $el
.select2('data', data
, true);
574 selectParams
.formatInputTooShort = function() {
575 var txt
= $el
.data('select-params').formatInputTooShort
|| $.fn
.select2
.defaults
.formatInputTooShort
.call(this);
576 txt
+= entityRefFiltersMarkup($el
) + renderEntityRefCreateLinks($el
);
579 selectParams
.formatNoMatches = function() {
580 var txt
= $el
.data('select-params').formatNoMatches
|| $.fn
.select2
.defaults
.formatNoMatches
;
581 txt
+= entityRefFiltersMarkup($el
) + renderEntityRefCreateLinks($el
);
584 $el
.on('select2-open.crmEntity', function() {
588 .on('click.crmEntity', 'a.crm-add-entity', function(e
) {
589 var extra
= $el
.data('api-params').extra
,
590 formUrl
= $(this).attr('href') + '&returnExtra=display_name,sort_name' + (extra
? (',' + extra
) : '');
591 $el
.select2('close');
592 CRM
.loadForm(formUrl
, {
593 dialog
: {width
: '50%', height
: 220}
594 }).on('crmFormSuccess', function(e
, data
) {
595 if (data
.status
=== 'success' && data
.id
) {
596 if (!data
.crmMessages
) {
597 CRM
.status(ts('%1 Created', {1: data
.label
|| data
.extra
.display_name
}));
599 data
.label
= data
.label
|| data
.extra
.sort_name
;
600 if ($el
.select2('container').hasClass('select2-container-multi')) {
601 var selection
= $el
.select2('data');
602 selection
.push(data
);
603 $el
.select2('data', selection
, true);
605 $el
.select2('data', data
, true);
611 .on('change.crmEntity', '.crm-entityref-filter-value', function() {
612 var filter
= $el
.data('user-filter') || {};
613 filter
.value
= $(this).val();
614 $(this).toggleClass('active', !!filter
.value
);
615 $el
.data('user-filter', filter
);
616 if (filter
.value
&& $(this).is('select')) {
617 // Once a filter has been chosen, rerender create links and refocus the search box
618 $el
.select2('close');
621 $('.crm-entityref-links', '#select2-drop').replaceWith(renderEntityRefCreateLinks($el
));
624 .on('change.crmEntity', 'select.crm-entityref-filter-key', function() {
625 var filter
= {key
: $(this).val()};
626 $(this).toggleClass('active', !!filter
.key
);
627 $el
.data('user-filter', filter
);
628 renderEntityRefFilterValue($el
);
629 $('.crm-entityref-filter-key', '#select2-drop').focus();
633 $el
.crmSelect2($.extend(settings
, $el
.data('select-params'), selectParams
));
638 * Combine api-params with user-filter
642 function getEntityRefApiParams($el
) {
644 params
= $.extend({params
: {}}, $el
.data('api-params') || {}),
645 // Prevent original data from being modified - $.extend and _.clone don't cut it, they pass nested objects by reference!
646 combined
= _
.cloneDeep(params
),
647 filter
= $.extend({}, $el
.data('user-filter') || {});
648 if (filter
.key
&& filter
.value
) {
649 // Fieldname may be prefixed with joins
650 var fieldName
= _
.last(filter
.key
.split('.'));
651 // Special case for contact type/sub-type combo
652 if (fieldName
=== 'contact_type' && (filter
.value
.indexOf('__') > 0)) {
653 combined
.params
[filter
.key
] = filter
.value
.split('__')[0];
654 combined
.params
[filter
.key
.replace('contact_type', 'contact_sub_type')] = filter
.value
.split('__')[1];
656 // Allow json-encoded api filters e.g. {"BETWEEN":[123,456]}
657 combined
.params
[filter
.key
] = filter
.value
.charAt(0) === '{' ? $.parseJSON(filter
.value
) : filter
.value
;
663 CRM
.utils
.copyAttributes = function ($source
, $target
, attributes
) {
664 _
.each(attributes
, function(name
) {
665 if ($source
.attr(name
) !== undefined) {
666 $target
.attr(name
, $source
.attr(name
));
671 CRM
.utils
.formatSelect2Result = function (row
) {
672 var markup
= '<div class="crm-select2-row">';
673 if (row
.image
!== undefined) {
674 markup
+= '<div class="crm-select2-image"><img src="' + row
.image
+ '"/></div>';
676 else if (row
.icon_class
) {
677 markup
+= '<div class="crm-select2-icon"><div class="crm-icon ' + row
.icon_class
+ '-icon"></div></div>';
679 markup
+= '<div><div class="crm-select2-row-label '+(row
.label_class
|| '')+'">' +
680 (row
.color
? '<span class="crm-select-item-color" style="background-color: ' + row
.color
+ '"></span> ' : '') +
681 _
.escape((row
.prefix
!== undefined ? row
.prefix
+ ' ' : '') + row
.label
+ (row
.suffix
!== undefined ? ' ' + row
.suffix
: '')) +
683 '<div class="crm-select2-row-description">';
684 $.each(row
.description
|| [], function(k
, text
) {
685 markup
+= '<p>' + _
.escape(text
) + '</p> ';
687 markup
+= '</div></div></div>';
691 function formatEntityRefSelection(row
) {
692 return (row
.color
? '<span class="crm-select-item-color" style="background-color: ' + row
.color
+ '"></span> ' : '') +
693 _
.escape((row
.prefix
!== undefined ? row
.prefix
+ ' ' : '') + row
.label
+ (row
.suffix
!== undefined ? ' ' + row
.suffix
: ''));
696 function renderEntityRefCreateLinks($el
) {
698 createLinks
= $el
.data('create-links'),
699 params
= getEntityRefApiParams($el
).params
,
700 entity
= $el
.data('api-entity'),
701 markup
= '<div class="crm-entityref-links">';
702 if (!createLinks
|| (createLinks
=== true && !CRM
.config
.entityRef
.links
[entity
])) {
705 if (createLinks
=== true) {
706 createLinks
= params
.contact_type
? _
.where(CRM
.config
.entityRef
.links
[entity
], {type
: params
.contact_type
}) : CRM
.config
.entityRef
.links
[entity
];
708 _
.each(createLinks
, function(link
) {
709 markup
+= ' <a class="crm-add-entity crm-hover-button" href="' + link
.url
+ '">' +
710 '<i class="crm-i ' + (link
.icon
|| 'fa-plus-circle') + '" aria-hidden="true"></i> ' +
711 _
.escape(link
.label
) + '</a>';
717 function getEntityRefFilters($el
) {
719 entity
= $el
.data('api-entity'),
720 filters
= CRM
.config
.entityRef
.filters
[entity
] || [],
721 params
= $.extend({params
: {}}, $el
.data('api-params') || {}).params
,
723 _
.each(filters
, function(filter
) {
724 _
.defaults(filter
, {type
: 'select', 'attributes': {}, entity
: entity
});
725 if (!params
[filter
.key
]) {
726 // Filter out options if params don't match its condition
727 if (filter
.condition
&& !_
.isMatch(params
, _
.pick(filter
.condition
, _
.keys(params
)))) {
732 else if (filter
.key
== 'contact_type' && typeof params
.contact_sub_type
=== 'undefined') {
740 * Provide markup for entity ref filters
742 function entityRefFiltersMarkup($el
) {
744 filters
= getEntityRefFilters($el
),
745 filter
= $el
.data('user-filter') || {},
746 filterSpec
= filter
.key
? _
.find(filters
, {key
: filter
.key
}) : null;
747 if (!filters
.length
) {
750 var markup
= '<div class="crm-entityref-filters">' +
751 '<select class="crm-entityref-filter-key' + (filter
.key
? ' active' : '') + '">' +
752 '<option value="">' + _
.escape(ts('Refine search...')) + '</option>' +
753 CRM
.utils
.renderOptions(filters
, filter
.key
) +
754 '</select>' + entityRefFilterValueMarkup($el
, filter
, filterSpec
) + '</div>';
759 * Provide markup for entity ref filter value field
761 function entityRefFilterValueMarkup($el
, filter
, filterSpec
) {
765 attributes
= _
.cloneDeep(filterSpec
.attributes
);
766 if (filterSpec
.type
!== 'select') {
767 attributes
.type
= filterSpec
.type
;
768 attributes
.value
= typeof filter
.value
!== 'undefined' ? filter
.value
: '';
770 attributes
.class = 'crm-entityref-filter-value' + (filter
.value
? ' active' : '');
771 $.each(attributes
, function (attr
, val
) {
772 attrs
+= ' ' + attr
+ '="' + val
+ '"';
774 if (filterSpec
.type
=== 'select') {
775 var fieldName
= _
.last(filter
.key
.split('.')),
776 options
= [{key
: '', value
: ts('- select -')}];
777 if (filterSpec
.options
) {
778 options
= options
.concat(getEntityRefFilterOptions(fieldName
, $el
, filterSpec
));
780 markup
= '<select' + attrs
+ '>' + CRM
.utils
.renderOptions(options
, filter
.value
) + '</select>';
782 markup
= '<input' + attrs
+ '/>';
789 * Render the entity ref filter value field
791 function renderEntityRefFilterValue($el
) {
793 filter
= $el
.data('user-filter') || {},
794 filterSpec
= filter
.key
? _
.find(getEntityRefFilters($el
), {key
: filter
.key
}) : null,
795 $keyField
= $('.crm-entityref-filter-key', '#select2-drop'),
798 $('.crm-entityref-filter-value', '#select2-drop').remove();
799 $valField
= $(entityRefFilterValueMarkup($el
, filter
, filterSpec
));
800 $keyField
.after($valField
);
801 if (filterSpec
.type
=== 'select') {
802 loadEntityRefFilterOptions(filter
, filterSpec
, $valField
, $el
);
805 $('.crm-entityref-filter-value', '#select2-drop').hide().val('').change();
810 * Fetch options for a filter from cache or ajax api
812 function loadEntityRefFilterOptions(filter
, filterSpec
, $valField
, $el
) {
813 // Fieldname may be prefixed with joins - strip those out
814 var fieldName
= _
.last(filter
.key
.split('.'));
815 if (filterSpec
.options
) {
816 CRM
.utils
.setOptions($valField
, getEntityRefFilterOptions(fieldName
, $el
, filterSpec
), false, filter
.value
);
819 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', true);
820 CRM
.api3(filterSpec
.entity
, 'getoptions', {field
: fieldName
, context
: 'search', sequential
: 1})
821 .done(function(result
) {
822 var entity
= $el
.data('api-entity').toLowerCase();
823 // Store options globally so we don't have to look them up again
824 filterSpec
.options
= result
.values
;
825 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', false);
826 CRM
.utils
.setOptions($valField
, getEntityRefFilterOptions(fieldName
, $el
, filterSpec
), false, filter
.value
);
830 function getEntityRefFilterOptions(fieldName
, $el
, filterSpec
) {
831 var values
= _
.cloneDeep(filterSpec
.options
),
832 params
= $.extend({params
: {}}, $el
.data('api-params') || {}).params
;
833 if (fieldName
=== 'contact_type' && params
.contact_type
) {
834 values
= _
.remove(values
, function(option
) {
835 return option
.key
.indexOf(params
.contact_type
+ '__') === 0;
841 //CRM-15598 - Override url validator method to allow relative url's (e.g. /index.htm)
842 $.validator
.addMethod("url", function(value
, element
) {
843 if (/^\//.test(value
)) {
844 // Relative url: prepend dummy path for validation.
845 value
= 'http://domain.tld' + value
;
847 // From jQuery Validation Plugin v1.12.0
848 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
);
852 * Wrapper for jQuery validate initialization function; supplies defaults
854 $.fn
.crmValidate = function(params
) {
855 return $(this).each(function () {
856 var validator
= $(this).validate();
858 validator
.settings
= $.extend({}, validator
.settings
, CRM
.validate
._defaults
, CRM
.validate
.params
);
859 // Call our custom validation handler.
860 $(validator
.currentForm
).on("invalid-form.validate", validator
.settings
.invalidHandler
);
861 // Call any post-initialization callbacks
862 if (CRM
.validate
.functions
&& CRM
.validate
.functions
.length
) {
863 $.each(CRM
.validate
.functions
, function(i
, func
) {
873 function submitOnceForm(e
) {
874 if (e
.isDefaultPrevented()) {
877 if (_
.contains(submitted
, e
.target
)) {
880 submitted
.push(e
.target
);
881 // Spin submit button icon
882 if (submitButton
&& $(submitButton
, e
.target
).length
) {
884 if ($(e
.target
).closest('.ui-dialog .crm-ajax-container')) {
885 var identifier
= $(submitButton
).attr('name') || $(submitButton
).attr('href');
887 submitButton
= $(e
.target
).closest('.ui-dialog').find('button[data-identifier="' + identifier
+ '"]')[0] || submitButton
;
890 var $icon
= $(submitButton
).siblings('.crm-i').add('.crm-i, .ui-icon', submitButton
);
891 $icon
.data('origClass', $icon
.attr('class')).removeClass().addClass('crm-i crm-submit-icon fa-spinner fa-pulse');
895 // If form fails validation, restore button icon and reset the submitted array
896 function submitFormInvalid(form
) {
898 $('.crm-i.crm-submit-icon').each(function() {
899 if ($(this).data('origClass')) {
900 $(this).removeClass().addClass($(this).data('origClass'));
905 // Initialize widgets
907 .on('crmLoad', function(e
) {
908 $('table.row-highlight', e
.target
)
909 .off('.rowHighlight')
910 .on('change.rowHighlight', 'input.select-row, input.select-rows', function (e
, data
) {
911 var filter
, $table
= $(this).closest('table');
912 if ($(this).hasClass('select-rows')) {
913 filter
= $(this).prop('checked') ? ':not(:checked)' : ':checked';
914 $('input.select-row' + filter
, $table
).prop('checked', $(this).prop('checked')).trigger('change', 'master-selected');
917 $(this).closest('tr').toggleClass('crm-row-selected', $(this).prop('checked'));
918 if (data
!== 'master-selected') {
919 $('input.select-rows', $table
).prop('checked', $(".select-row:not(':checked')", $table
).length
< 1);
923 .find('input.select-row:checked').parents('tr').addClass('crm-row-selected');
924 $('.crm-sortable-list', e
.target
).sortable();
925 $('table.crm-sortable', e
.target
).DataTable();
926 $('table.crm-ajax-table', e
.target
).each(function() {
929 script
= CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmAjaxTable.js',
930 $accordion
= $table
.closest('.crm-accordion-wrapper.collapsed, .crm-collapsible.collapsed');
931 // For tables hidden by collapsed accordions, wait.
932 if ($accordion
.length
) {
933 $accordion
.one('crmAccordion:open', function() {
934 CRM
.loadScript(script
).done(function() {
935 $table
.crmAjaxTable();
939 CRM
.loadScript(script
).done(function() {
940 $table
.crmAjaxTable();
944 if ($("input:radio[name=radio_ts]").length
== 1) {
945 $("input:radio[name=radio_ts]").prop("checked", true);
947 $('.crm-select2:not(.select2-offscreen, .select2-container)', e
.target
).crmSelect2();
948 $('.crm-form-entityref:not(.select2-offscreen, .select2-container)', e
.target
).crmEntityRef();
949 $('select.crm-chain-select-control', e
.target
).off('.chainSelect').on('change.chainSelect', chainSelect
);
950 $('.crm-form-text[data-crm-datepicker]', e
.target
).each(function() {
951 $(this).crmDatepicker($(this).data('crmDatepicker'));
953 $('.crm-editable', e
.target
).not('thead *').each(function() {
955 CRM
.loadScript(CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmEditable.js').done(function() {
959 // Cache Form Input initial values
960 $('form[data-warn-changes] :input', e
.target
).each(function() {
961 $(this).data('crm-initial-value', $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val());
963 $('textarea.crm-form-wysiwyg', e
.target
).each(function() {
964 if ($(this).hasClass("collapsed")) {
965 CRM
.wysiwyg
.createCollapsed(this);
967 CRM
.wysiwyg
.create(this);
970 // Submit once handlers
971 $('form[data-submit-once]', e
.target
)
972 .submit(submitOnceForm
)
973 .on('invalid-form', submitFormInvalid
);
974 $('form[data-submit-once] button[type=submit]', e
.target
).click(function(e
) {
975 submitButton
= e
.target
;
978 .on('dialogopen', function(e
) {
979 var $el
= $(e
.target
);
980 $('body').addClass('ui-dialog-open');
981 // Modal dialogs should disable scrollbars
982 if ($el
.dialog('option', 'modal')) {
983 $el
.addClass('modal-dialog');
984 $('body').css({overflow
: 'hidden'});
986 $el
.parent().find('.ui-dialog-titlebar .ui-icon-closethick').removeClass('ui-icon-closethick').addClass('fa-times');
988 if ($el
.parent().hasClass('crm-container') && $el
.dialog('option', 'resizable')) {
989 $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}));
990 $('.crm-dialog-titlebar-resize', $el
.parent()).click(function(e
) {
991 if ($el
.data('origSize')) {
992 $el
.dialog('option', $el
.data('origSize'));
993 $el
.data('origSize', null);
994 $(this).button('option', 'icons', {primary
: 'fa-expand'});
996 var menuHeight
= $('#civicrm-menu').outerHeight();
997 if ($('body').hasClass('crm-menubar-below-cms-menu')) {
998 menuHeight
+= $('#civicrm-menu').offset().top
;
1000 $el
.data('origSize', {
1001 position
: {my
: 'center', at
: 'center center+' + (menuHeight
/ 2), of: window
},
1002 width
: $el
.dialog('option', 'width'),
1003 height
: $el
.dialog('option', 'height')
1005 $el
.dialog('option', {width
: '100%', height
: ($(window
).height() - menuHeight
), position
: {my
: "top", at
: "top+"+menuHeight
, of: window
}});
1006 $(this).button('option', 'icons', {primary
: 'fa-compress'});
1008 $el
.trigger('dialogresize');
1013 .on('dialogclose', function(e
) {
1014 // Restore scrollbars when closing modal
1015 if ($('.ui-dialog .modal-dialog:visible').not(e
.target
).length
< 1) {
1016 $('body').css({overflow
: ''});
1018 if ($('.ui-dialog-content:visible').not(e
.target
).length
< 1) {
1019 $('body').removeClass('ui-dialog-open');
1022 .on('submit', function(e
) {
1023 // CRM-14353 - disable changes warn when submitting a form
1024 $('[data-warn-changes]').attr('data-warn-changes', 'false');
1027 // CRM-14353 - Warn of unsaved changes for forms which have opted in
1028 window
.onbeforeunload = function() {
1029 if (CRM
.utils
.initialValueChanged($('form[data-warn-changes=true]:visible'))) {
1030 return ts('You have unsaved changes.');
1034 $.fn
.crmtooltip = function () {
1036 .on('mouseover', 'a.crm-summary-link:not(.crm-processed)', function (e
) {
1037 $(this).addClass('crm-processed crm-tooltip-active');
1038 var topDistance
= e
.pageY
- $(window
).scrollTop();
1039 if (topDistance
< 300 || topDistance
< $(this).children('.crm-tooltip-wrapper').height()) {
1040 $(this).addClass('crm-tooltip-down');
1042 if (!$(this).children('.crm-tooltip-wrapper').length
) {
1043 $(this).append('<div class="crm-tooltip-wrapper"><div class="crm-tooltip"></div></div>');
1044 $(this).children().children('.crm-tooltip')
1045 .html('<div class="crm-loading-element"></div>')
1049 .on('mouseout', 'a.crm-summary-link', function () {
1050 $(this).removeClass('crm-processed crm-tooltip-active crm-tooltip-down');
1052 .on('click', 'a.crm-summary-link', false);
1055 var helpDisplay
, helpPrevious
;
1056 // Non-ajax example:
1057 // CRM.help('Example title', 'Here is some text to describe this example');
1058 // Ajax example (will load help id "foo" from templates/CRM/bar.tpl):
1059 // CRM.help('Example title', {id: 'foo', file: 'CRM/bar'});
1060 CRM
.help = function (title
, params
, url
) {
1061 var ajax
= typeof params
!== 'string';
1062 if (helpDisplay
&& helpDisplay
.close
) {
1063 // If the same link is clicked twice, just close the display
1064 if (helpDisplay
.isOpen
&& _
.isEqual(helpPrevious
, params
)) {
1065 helpDisplay
.close();
1068 helpDisplay
.close();
1070 helpPrevious
= _
.cloneDeep(params
);
1071 helpDisplay
= CRM
.alert(ajax
? '...' : params
, title
, 'crm-help ' + (ajax
? 'crm-msg-loading' : 'info'), {expires
: 0});
1074 url
= CRM
.url('civicrm/ajax/inline');
1075 params
.class_name
= 'CRM_Core_Page_Inline_Help';
1076 params
.type
= 'page';
1081 success: function (data
) {
1082 $('#crm-notification-container .crm-help .notify-content:last').html(data
);
1083 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('info');
1085 error: function () {
1086 $('#crm-notification-container .crm-help .notify-content:last').html('Unable to load help file.');
1087 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('error');
1093 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1095 CRM
.status = function(options
, deferred
) {
1096 // 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.
1097 if (typeof options
=== 'string') {
1098 return CRM
.status({start
: options
, success
: options
, error
: options
})[deferred
=== 'error' ? 'reject' : 'resolve']();
1100 var opts
= $.extend({
1101 start
: ts('Saving...'),
1102 success
: ts('Saved'),
1103 error: function(data
) {
1104 var msg
= $.isPlainObject(data
) && data
.error_message
;
1105 CRM
.alert(msg
|| ts('Sorry an error occurred and your information was not saved'), ts('Error'), 'error');
1108 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>')
1110 $msg
.css('min-width', $msg
.width());
1111 function handle(status
, data
) {
1112 var endMsg
= typeof(opts
[status
]) === 'function' ? opts
[status
](data
) : opts
[status
];
1114 $msg
.removeClass('status-start').addClass('status-' + status
).find('.crm-status-box-msg').text(endMsg
);
1115 window
.setTimeout(function() {
1116 $msg
.fadeOut('slow', function() {
1124 return (deferred
|| new $.Deferred())
1125 .done(function(data
) {
1126 // If the server returns an error msg call the error handler
1127 var status
= $.isPlainObject(data
) && (data
.is_error
|| data
.status
=== 'error') ? 'error' : 'success';
1128 handle(status
, data
);
1130 .fail(function(data
) {
1131 handle('error', data
);
1135 // Convert an Angular promise to a jQuery promise
1136 CRM
.toJqPromise = function(aPromise
) {
1137 var jqDeferred
= $.Deferred();
1139 function(data
) { jqDeferred
.resolve(data
); },
1140 function(data
) { jqDeferred
.reject(data
); }
1141 // should we also handle progress events?
1143 return jqDeferred
.promise();
1146 CRM
.toAPromise = function($q
, jqPromise
) {
1147 var aDeferred
= $q
.defer();
1149 function(data
) { aDeferred
.resolve(data
); },
1150 function(data
) { aDeferred
.reject(data
); }
1151 // should we also handle progress events?
1153 return aDeferred
.promise
;
1157 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1159 CRM
.alert = function (text
, title
, type
, options
) {
1160 type
= type
|| 'alert';
1161 title
= title
|| '';
1162 options
= options
|| {};
1163 if ($('#crm-notification-container').length
) {
1169 // By default, don't expire errors and messages containing links
1171 expires
: (type
== 'error' || text
.indexOf('<a ') > -1) ? 0 : (text
? 10000 : 5000),
1174 options
= $.extend(extra
, options
);
1175 options
.expires
= (options
.expires
=== false || !CRM
.config
.allowAlertAutodismissal
) ? 0 : parseInt(options
.expires
, 10);
1176 if (options
.unique
&& options
.unique
!== '0') {
1177 $('#crm-notification-container .ui-notify-message').each(function () {
1178 if (title
=== $('h1', this).html() && text
=== $('.notify-content', this).html()) {
1179 $('.icon.ui-notify-close', this).click();
1183 return $('#crm-notification-container').notify('create', params
, options
);
1187 text
= title
+ "\n" + text
;
1189 // strip html tags as they are not parsed in standard alerts
1190 alert($("<div/>").html(text
).text());
1196 * Close whichever alert contains the given node
1200 CRM
.closeAlertByChild = function (node
) {
1201 $(node
).closest('.ui-notify-message').find('.icon.ui-notify-close').click();
1205 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1207 CRM
.confirm = function (options
) {
1208 var dialog
, url
, msg
, buttons
= [], settings
= {
1209 title
: ts('Confirm'),
1210 message
: ts('Are you sure you want to continue?'),
1215 dialogClass
: 'crm-container crm-confirm',
1216 close: function () {
1217 $(this).dialog('destroy').remove();
1224 if (options
&& options
.url
) {
1225 settings
.resizable
= true;
1226 settings
.height
= '50%';
1228 $.extend(settings
, ($.isFunction(options
) ? arguments
[1] : options
) || {});
1229 settings
= CRM
.utils
.adjustDialogDefaults(settings
);
1230 if (!settings
.buttons
&& $.isPlainObject(settings
.options
)) {
1231 $.each(settings
.options
, function(op
, label
) {
1235 icons
: {primary
: op
=== 'no' ? 'fa-times' : 'fa-check'},
1237 var event
= $.Event('crmConfirm:' + op
);
1238 $(this).trigger(event
);
1239 if (!event
.isDefaultPrevented()) {
1240 dialog
.dialog('close');
1245 // Order buttons so that "no" goes on the right-hand side
1246 settings
.buttons
= _
.sortBy(buttons
, 'data-op').reverse();
1249 msg
= url
? '' : settings
.message
;
1250 delete settings
.options
;
1251 delete settings
.message
;
1252 delete settings
.url
;
1253 dialog
= $('<div class="crm-confirm-dialog"></div>').html(msg
|| '').dialog(settings
);
1254 if ($.isFunction(options
)) {
1255 dialog
.on('crmConfirm:yes', options
);
1258 CRM
.loadPage(url
, {target
: dialog
});
1261 dialog
.trigger('crmLoad');
1266 /** provides a local copy of ts for a domain */
1267 CRM
.ts = function(domain
) {
1268 return function(message
, options
) {
1270 options
= $.extend(options
|| {}, {domain
: domain
});
1272 return ts(message
, options
);
1276 CRM
.addStrings = function(domain
, strings
) {
1277 var bucket
= (domain
== 'civicrm' ? 'strings' : 'strings::' + domain
);
1278 CRM
[bucket
] = CRM
[bucket
] || {};
1279 _
.extend(CRM
[bucket
], strings
);
1283 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1285 $.fn
.crmError = function (text
, title
, options
) {
1286 title
= title
|| '';
1288 options
= options
|| {};
1293 if ($(this).length
) {
1295 var label
= $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
1297 label
.addClass('crm-error');
1298 var $label
= label
.clone();
1299 if (text
=== '' && $('.crm-marker', $label
).length
> 0) {
1300 text
= $('.crm-marker', $label
).attr('title');
1302 $('.crm-marker', $label
).remove();
1303 title
= $label
.text();
1306 $(this).addClass('crm-error');
1308 var msg
= CRM
.alert(text
, title
, 'error', $.extend(extra
, options
));
1309 if ($(this).length
) {
1311 setTimeout(function () {
1312 ele
.one('change', function () {
1313 if (msg
&& msg
.close
) msg
.close();
1314 ele
.removeClass('error');
1315 label
.removeClass('crm-error');
1322 // Display system alerts through js notifications
1323 function messagesFromMarkup() {
1324 $('div.messages:visible', this).not('.help').not('.no-popup').each(function () {
1325 var text
, title
= '';
1326 $(this).removeClass('status messages');
1327 var type
= $(this).attr('class').split(' ')[0] || 'alert';
1328 type
= type
.replace('crm-', '');
1329 $('.icon', this).remove();
1330 if ($('.msg-text', this).length
> 0) {
1331 text
= $('.msg-text', this).html();
1332 title
= $('.msg-title', this).html();
1335 text
= $(this).html();
1337 var options
= $(this).data('options') || {};
1339 // Duplicates were already removed server-side
1340 options
.unique
= false;
1341 CRM
.alert(text
, title
, type
, options
);
1343 // Handle qf form errors
1344 $('form :input.error', this).one('blur', function() {
1345 $('.ui-notify-message.error a.ui-notify-close').click();
1346 $(this).removeClass('error');
1347 $(this).next('span.crm-error').remove();
1348 $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]')
1349 .removeClass('crm-error')
1350 .find('.crm-error').removeClass('crm-error');
1355 * Improve blockUI when used with jQuery dialog
1357 var originalBlock
= $.fn
.block
,
1358 originalUnblock
= $.fn
.unblock
;
1360 $.fn
.block = function(opts
) {
1361 if ($(this).is('.ui-dialog-content')) {
1362 originalBlock
.call($(this).parents('.ui-dialog'), opts
);
1365 return originalBlock
.call(this, opts
);
1367 $.fn
.unblock = function(opts
) {
1368 if ($(this).is('.ui-dialog-content')) {
1369 originalUnblock
.call($(this).parents('.ui-dialog'), opts
);
1372 return originalUnblock
.call(this, opts
);
1375 // Preprocess all CRM ajax calls to display messages
1376 $(document
).ajaxSuccess(function(event
, xhr
, settings
) {
1378 if ((!settings
.dataType
|| settings
.dataType
== 'json') && xhr
.responseText
) {
1379 var response
= $.parseJSON(xhr
.responseText
);
1380 if (typeof(response
.crmMessages
) == 'object') {
1381 $.each(response
.crmMessages
, function(n
, msg
) {
1382 CRM
.alert(msg
.text
, msg
.title
, msg
.type
, msg
.options
);
1385 if (response
.backtrace
) {
1386 CRM
.console('log', response
.backtrace
);
1388 if (typeof response
.deprecated
=== 'string') {
1389 CRM
.console('warn', response
.deprecated
);
1393 // Ignore errors thrown by parseJSON
1398 $.blockUI
.defaults
.message
= null;
1399 $.blockUI
.defaults
.ignoreIfBlocked
= true;
1401 if ($('#crm-container').hasClass('crm-public')) {
1402 $.fn
.select2
.defaults
.dropdownCssClass
= $.ui
.dialog
.prototype.options
.dialogClass
= 'crm-container crm-public';
1405 // Trigger crmLoad on initial content for consistency. It will also be triggered for ajax-loaded content.
1406 $('.crm-container').trigger('crmLoad');
1408 if ($('#crm-notification-container').length
) {
1409 // Initialize notifications
1410 $('#crm-notification-container').notify();
1411 messagesFromMarkup
.call($('#crm-container'));
1415 // bind the event for image popup
1416 .on('click', 'a.crm-image-popup', function(e
) {
1418 title
: ts('Preview'),
1420 // Prevent overlap with the menubar
1421 maxHeight
: $(window
).height() - 30,
1422 position
: {my
: 'center', at
: 'center center+15', of: window
},
1423 message
: '<div class="crm-custom-image-popup"><img style="max-width: 100%" src="' + $(this).attr('href') + '"></div>',
1429 .on('click', function (event
) {
1430 $('.btn-slide-active').removeClass('btn-slide-active').find('.panel').hide();
1431 if ($(event
.target
).is('.btn-slide')) {
1432 $(event
.target
).addClass('btn-slide-active').find('.panel').show();
1436 // Handle clear button for form elements
1437 .on('click', 'a.crm-clear-link', function() {
1438 $(this).css({visibility
: 'hidden'}).siblings('.crm-form-radio:checked').prop('checked', false).trigger('change', ['crmClear']);
1439 $(this).siblings('input:text').val('').trigger('change', ['crmClear']);
1442 .on('change keyup', 'input.crm-form-radio:checked, input[allowclear=1]', function(e
, context
) {
1443 if (context
!== 'crmClear' && ($(this).is(':checked') || ($(this).is('[allowclear=1]') && $(this).val()))) {
1444 $(this).siblings('.crm-clear-link').css({visibility
: ''});
1446 if (context
!== 'crmClear' && $(this).is('[allowclear=1]') && $(this).val() === '') {
1447 $(this).siblings('.crm-clear-link').css({visibility
: 'hidden'});
1451 // Allow normal clicking of links within accordions
1452 .on('click.crmAccordions', 'div.crm-accordion-header a, .collapsible-title a', function (e
) {
1453 e
.stopPropagation();
1455 // Handle accordions
1456 .on('click.crmAccordions', '.crm-accordion-header, .crm-collapsible .collapsible-title', function (e
) {
1457 var action
= 'open';
1458 if ($(this).parent().hasClass('collapsed')) {
1459 $(this).next().css('display', 'none').slideDown(200);
1462 $(this).next().css('display', 'block').slideUp(200);
1465 $(this).parent().toggleClass('collapsed').trigger('crmAccordion:' + action
);
1473 * Collapse or expand an accordion
1476 $.fn
.crmAccordionToggle = function (speed
) {
1477 $(this).each(function () {
1478 var action
= 'open';
1479 if ($(this).hasClass('collapsed')) {
1480 $('.crm-accordion-body', this).first().css('display', 'none').slideDown(speed
);
1483 $('.crm-accordion-body', this).first().css('display', 'block').slideUp(speed
);
1486 $(this).toggleClass('collapsed').trigger('crmAccordion:' + action
);
1491 * Clientside currency formatting
1492 * @param number value
1493 * @param [optional] boolean onlyNumber - if true, we return formatted amount without currency sign
1494 * @param [optional] string format - currency representation of the number 1234.56
1497 var currencyTemplate
;
1498 CRM
.formatMoney = function(value
, onlyNumber
, format
) {
1499 var precision
, decimal, separator
, sign
, i
, j
, result
;
1500 if (value
=== 'init' && format
) {
1501 currencyTemplate
= format
;
1504 format
= format
|| currencyTemplate
;
1505 if ((result
= /1(.?)234(.?)56/.exec(format
)) !== null) { // If value is formatted to 2 decimals
1508 else if ((result
= /1(.?)234(.?)6/.exec(format
)) !== null) { // If value is formatted to 1 decimal
1511 else if ((result
= /1(.?)235/.exec(format
)) !== null) { // If value is formatted to zero decimals
1515 return 'Invalid format passed to CRM.formatMoney';
1517 separator
= result
[1];
1518 decimal = precision
? result
[2] : false;
1519 sign
= (value
< 0) ? '-' : '';
1520 //extracting the absolute value of the integer part of the number and converting to string
1521 i
= parseInt(value
= Math
.abs(value
).toFixed(2)) + '';
1522 j
= ((j
= i
.length
) > 3) ? j
% 3 : 0;
1523 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) : '');
1527 switch (precision
) {
1529 return format
.replace(/1.*234.*56/, result
);
1531 return format
.replace(/1.*234.*6/, result
);
1533 return format
.replace(/1.*235/, result
);
1537 CRM
.angRequires = function(name
) {
1538 return CRM
.angular
.requires
[name
] || [];
1541 CRM
.console = function(method
, title
, msg
) {
1542 if (window
.console
) {
1543 method
= $.isFunction(console
[method
]) ? method
: 'log';
1544 if (msg
=== undefined) {
1545 return console
[method
](title
);
1547 return console
[method
](title
, msg
);
1552 // Sugar methods for window.localStorage, with a fallback for older browsers
1553 var cacheItems
= {};
1555 get: function (name
, defaultValue
) {
1557 if (localStorage
.getItem('CRM' + name
) !== null) {
1558 return JSON
.parse(localStorage
.getItem('CRM' + name
));
1561 return cacheItems
[name
] === undefined ? defaultValue
: cacheItems
[name
];
1563 set: function (name
, value
) {
1565 localStorage
.setItem('CRM' + name
, JSON
.stringify(value
));
1567 cacheItems
[name
] = value
;
1569 clear: function(name
) {
1571 localStorage
.removeItem('CRM' + name
);
1573 delete cacheItems
[name
];
1579 // Determine if a user has a given permission.
1580 // @see CRM_Core_Resources::addPermissions
1581 CRM
.checkPerm = function(perm
) {
1582 return CRM
.permissions
&& CRM
.permissions
[perm
];
1585 // Round while preserving sigfigs
1586 CRM
.utils
.sigfig = function(n
, digits
) {
1587 var len
= ("" + n
).length
;
1588 var scale
= Math
.pow(10.0, len
-digits
);
1589 return Math
.round(n
/ scale
) * scale
;
1593 * Create a js Date object from a unix timestamp or a yyyy-mm-dd string
1597 CRM
.utils
.makeDate = function(input
) {
1598 switch (typeof input
) {
1600 // already a date object
1604 // convert iso format with or without dashes
1605 input
= input
.replace(/[- :]/g, '');
1606 var output
= $.datepicker
.parseDate('yymmdd', input
.substr(0, 8));
1607 if (input
.length
=== 14) {
1609 parseInt(input
.substr(8, 2), 10),
1610 parseInt(input
.substr(10, 2), 10),
1611 parseInt(input
.substr(12, 2), 10)
1617 // convert unix timestamp
1618 return new Date(input
* 1000);
1620 throw 'Invalid input passed to CRM.utils.makeDate';
1624 * Format a date (and optionally time) for output to the user
1626 * @param {string|int|Date} input
1627 * Input may be a js Date object, a unix timestamp or a 'yyyy-mm-dd' string
1628 * @param {string|null} dateFormat
1629 * A string like 'yy-mm-dd' or null to use the system default
1630 * @param {int|bool} timeFormat
1631 * Leave empty to omit time from the output (default)
1632 * Or pass 12, 24, or true to use the system default for 12/24hr format
1635 CRM
.utils
.formatDate = function(input
, dateFormat
, timeFormat
) {
1639 var date
= CRM
.utils
.makeDate(input
),
1640 output
= $.datepicker
.formatDate(dateFormat
|| CRM
.config
.dateInputFormat
, date
);
1642 var hour
= date
.getHours(),
1643 min
= date
.getMinutes(),
1645 if (timeFormat
=== 12 || (timeFormat
=== true && !CRM
.config
.timeIs24Hr
)) {
1646 suf
= ' ' + (hour
< 12 ? ts('AM') : ts('PM'));
1647 if (hour
=== 0 || hour
> 12) {
1648 hour
= Math
.abs(hour
- 12);
1650 } else if (hour
< 10) {
1653 output
+= ' ' + hour
+ ':' + (min
< 10 ? '0' : '') + min
+ suf
;
1658 // Used to set appropriate text color for a given background
1659 CRM
.utils
.colorContrast = function (hexcolor
) {
1660 hexcolor
= hexcolor
.replace(/[ #]/g, '');
1661 var r
= parseInt(hexcolor
.substr(0, 2), 16),
1662 g
= parseInt(hexcolor
.substr(2, 2), 16),
1663 b
= parseInt(hexcolor
.substr(4, 2), 16),
1664 yiq
= ((r
* 299) + (g
* 587) + (b
* 114)) / 1000;
1665 return (yiq
>= 128) ? 'black' : 'white';
1668 // CVE-2015-9251 - Prevent auto-execution of scripts when no explicit dataType was provided
1669 $.ajaxPrefilter(function(s
) {
1670 if (s
.crossDomain
) {
1671 s
.contents
.script
= false;
1675 // 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.
1676 $.htmlPrefilter = function(html
) {
1677 // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
1678 // their XML equivalent: e.g., "<div />" to "<div></div>". This is
1679 // problematic for several reasons, including that it's vulnerable to XSS
1680 // attacks. However, since this was jQuery's behavior for many years, many
1681 // Drupal modules and jQuery plugins may be relying on it. Therefore, we
1682 // preserve that behavior, but for a limited set of tags only, that we believe
1683 // to not be vulnerable. This is the set of HTML tags that satisfy all of the
1684 // following conditions:
1685 // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
1686 // appear in that list, then we don't want to mess with it here either.
1687 // @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
1688 // - A normal element (not a void, template, text, or foreign element).
1689 // @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
1690 // - An element that is still defined by the current HTML specification
1691 // (not a deprecated element), because we do not want to rely on how
1692 // browsers parse deprecated elements.
1693 // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
1694 // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
1695 // designed for fragments, not entire documents.
1696 // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
1697 // regular expression, it didn't match on colgroup, and we don't want to
1698 // introduce a behavior change for that.
1699 var selfClosingTagsToReplace
= [
1700 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
1701 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
1702 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
1703 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
1704 'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
1705 'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
1706 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
1707 'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
1708 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
1709 'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
1712 // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
1713 // two expressions makes it easier to target <a/> without also targeting
1714 // every tag that starts with "a".
1715 var xhtmlRegExpGroup
= '(' + selfClosingTagsToReplace
.join('|') + ')';
1716 var whitespace
= '[\\x20\\t\\r\\n\\f]';
1717 var rxhtmlTagWithoutSpaceOrAttributes
= new RegExp('<' + xhtmlRegExpGroup
+ '\\/>', 'gi');
1718 var rxhtmlTagWithSpaceAndMaybeAttributes
= new RegExp('<' + xhtmlRegExpGroup
+ '(' + whitespace
+ '[^>]*)\\/>', 'gi');
1720 // jQuery 3.5 also fixed a vulnerability for when </select> appears within
1721 // an <option> or <optgroup>, but it did that in local code that we can't
1722 // backport directly. Instead, we filter such cases out. To do so, we need to
1723 // determine when jQuery would otherwise invoke the vulnerable code, which it
1724 // uses this regular expression to determine. The regular expression changed
1725 // for version 3.0.0 and changed again for 3.4.0.
1726 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
1727 // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
1728 // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
1729 var rtagName
= /<([\w:]+)/;
1731 // The regular expression that jQuery uses to determine which self-closing
1732 // tags to expand to open and close tags. This is vulnerable, because it
1733 // matches all tag names except the few excluded ones. We only use this
1734 // expression for determining vulnerability. The expression changed for
1735 // version 3, but we only need to check for vulnerability in versions 1 and 2,
1736 // so we use the expression from those versions.
1737 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
1738 var rxhtmlTag
= /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
1740 // This is how jQuery determines the first tag in the HTML.
1741 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
1742 var tag
= ( rtagName
.exec( html
) || [ "", "" ] )[ 1 ].toLowerCase();
1744 // It is not valid HTML for <option> or <optgroup> to have <select> as
1745 // either a descendant or sibling, and attempts to inject one can cause
1746 // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
1747 // possible XSS attack, reject the entire string.
1748 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
1749 if ((tag
=== 'option' || tag
=== 'optgroup') && html
.match(/<\/?select/i)) {
1753 // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
1754 // the tags in the `selfClosingTagsToReplace` list defined above.
1755 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
1756 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
1757 html
= html
.replace(rxhtmlTagWithoutSpaceOrAttributes
, "<$1></$1>");
1758 html
= html
.replace(rxhtmlTagWithSpaceAndMaybeAttributes
, "<$1$2></$1>");