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, .ck-balloon-panel, #civicrm-menu').length
;
191 // Implements jQuery hook.prop
192 $.propHooks
.disabled
= {
193 set: function (el
, value
, name
) {
194 // Sync button enabled status with wrapper css
195 if ($(el
).is('.crm-button.crm-form-submit')) {
196 $(el
).parent().toggleClass('crm-button-disabled', !!value
);
198 // Sync button enabled status with dialog button
199 if ($(el
).is('.ui-dialog input.crm-form-submit')) {
200 $(el
).closest('.ui-dialog').find('.ui-dialog-buttonset button[data-identifier='+ $(el
).attr('name') +']').prop('disabled', value
);
202 if ($(el
).is('.crm-form-date-wrapper .crm-hidden-date')) {
203 $(el
).siblings().prop('disabled', value
);
208 var scriptsLoaded
= {};
209 CRM
.loadScript = function(url
, appendCacheCode
) {
210 if (!scriptsLoaded
[url
]) {
211 var script
= document
.createElement('script'),
213 if (appendCacheCode
!== false) {
214 src
+= (_
.includes(url
, '?') ? '&r=' : '?r=') + CRM
.config
.resourceCacheCode
;
216 scriptsLoaded
[url
] = $.Deferred();
217 script
.onload = function () {
218 // Give the script time to execute
219 window
.setTimeout(function () {
220 if (window
.jQuery
=== CRM
.$ && CRM
.CMSjQuery
) {
221 window
.jQuery
= CRM
.CMSjQuery
;
223 scriptsLoaded
[url
].resolve();
226 // Make jQuery global available while script is loading
227 if (window
.jQuery
!== CRM
.$) {
228 CRM
.CMSjQuery
= window
.jQuery
;
229 window
.jQuery
= CRM
.$;
232 document
.getElementsByTagName("head")[0].appendChild(script
);
234 return scriptsLoaded
[url
];
238 * Populate a select list, overwriting the existing options except for the placeholder.
239 * @param select jquery selector - 1 or more select elements
240 * @param options array in format returned by api.getoptions
241 * @param placeholder string|bool - new placeholder or false (default) to keep the old one
242 * @param value string|array - will silently update the element with new value without triggering change
244 CRM
.utils
.setOptions = function(select
, options
, placeholder
, value
) {
245 $(select
).each(function() {
248 val
= value
|| $elect
.val() || [],
249 opts
= placeholder
|| placeholder
=== '' ? '' : '[value!=""]';
250 $elect
.find('option' + opts
).remove();
251 var newOptions
= CRM
.utils
.renderOptions(options
, val
);
252 if (options
.length
== 0) {
253 $elect
.removeClass('required');
254 } else if ($elect
.hasClass('crm-field-required') && !$elect
.hasClass('required')) {
255 $elect
.addClass('required');
257 if (typeof placeholder
=== 'string') {
258 if ($elect
.is('[multiple]')) {
259 select
.attr('placeholder', placeholder
);
261 newOptions
= '<option value="">' + placeholder
+ '</option>' + newOptions
;
264 $elect
.append(newOptions
);
266 $elect
.trigger('crmOptionsUpdated', $.extend({}, options
)).trigger('change');
272 * Render an option list
273 * @param options {array}
274 * @param val {string} default value
275 * @param escapeHtml {bool}
278 CRM
.utils
.renderOptions = function(options
, val
, escapeHtml
) {
280 esc
= escapeHtml
=== false ? _
.identity
: _
.escape
;
281 if (!$.isArray(val
)) {
284 _
.each(options
, function(option
) {
285 if (option
.children
) {
286 rendered
+= '<optgroup label="' + esc(option
.value
) + '">' +
287 CRM
.utils
.renderOptions(option
.children
, val
) +
290 var selected
= ($.inArray('' + option
.key
, val
) > -1) ? 'selected="selected"' : '';
291 rendered
+= '<option value="' + esc(option
.key
) + '"' + selected
+ '>' + esc(option
.value
) + '</option>';
297 CRM
.utils
.getOptions = function(select
) {
299 $('option', select
).each(function() {
300 var option
= {key
: $(this).attr('value'), value
: $(this).text()};
301 if (option
.key
!== '') {
302 options
.push(option
);
308 function chainSelect() {
309 var $form
= $(this).closest('form'),
310 $target
= $('select[data-name="' + $(this).data('target') + '"]', $form
),
311 data
= $target
.data(),
313 $target
.prop('disabled', true);
314 if ($target
.is('select.crm-chain-select-control')) {
315 $('select[data-name="' + $target
.data('target') + '"]', $form
).prop('disabled', true).blur();
317 if (!(val
&& val
.length
)) {
318 CRM
.utils
.setOptions($target
.blur(), [], data
.emptyPrompt
);
320 $target
.addClass('loading');
321 $.getJSON(CRM
.url(data
.callback
), {_value
: val
}, function(vals
) {
322 $target
.prop('disabled', false).removeClass('loading');
323 CRM
.utils
.setOptions($target
, vals
|| [], (vals
&& vals
.length
? data
.selectPrompt
: data
.nonePrompt
));
329 * Compare Form Input values against cached initial value.
331 * @return {Boolean} true if changes have been made.
333 CRM
.utils
.initialValueChanged = function(el
) {
335 $(':input:visible, .select2-container:visible+:input:hidden', el
).not('[type=submit], [type=button], .crm-action-menu, :disabled').each(function () {
337 initialValue
= $(this).data('crm-initial-value'),
338 currentValue
= $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val();
339 // skip change of value for submit buttons
340 if (initialValue
!== undefined && !_
.isEqual(initialValue
, currentValue
)) {
348 * This provides defaults for ui.dialog which either need to be calculated or are different from global defaults
353 CRM
.utils
.adjustDialogDefaults = function(settings
) {
354 settings
= $.extend({width
: '65%', height
: '65%', modal
: true}, settings
|| {});
355 // Support relative height
356 if (typeof settings
.height
=== 'string' && settings
.height
.indexOf('%') > 0) {
357 settings
.height
= parseInt($(window
).height() * (parseFloat(settings
.height
)/100), 10);
359 // Responsive adjustment - increase percent width on small screens
360 if (typeof settings
.width
=== 'string' && settings
.width
.indexOf('%') > 0) {
361 var screenWidth
= $(window
).width(),
362 percentage
= parseInt(settings
.width
.replace('%', ''), 10),
363 gap
= 100-percentage
;
364 if (screenWidth
< 701) {
365 settings
.width
= '100%';
367 else if (screenWidth
< 1400) {
368 settings
.width
= '' + parseInt(percentage
+gap
-((screenWidth
- 700)/7*(gap)/100), 10) + '%';
371 if (settings
.dialogClass
&& !_
.includes(settings
.dialogClass
, 'crm-container')) {
372 settings
.dialogClass
+= ' crm-container';
377 function formatCrmSelect2(row
) {
378 var icon
= row
.icon
|| $(row
.element
).data('icon'),
379 color
= row
.color
|| $(row
.element
).data('color'),
380 description
= row
.description
|| $(row
.element
).data('description'),
383 ret
+= '<i class="crm-i ' + icon
+ '" aria-hidden="true"></i> ';
386 ret
+= '<span class="crm-select-item-color" style="background-color: ' + color
+ '"></span> ';
388 return ret
+ _
.escape(row
.text
) + (description
? '<div class="crm-select2-row-description"><p>' + _
.escape(description
) + '</p></div>' : '');
392 * Helper to generate an icon with alt text.
394 * See also smarty `{icon}` and CRM_Core_Page::crmIcon() functions
397 * The Font Awesome icon class to use.
399 * Alt text to display.
400 * @param mixed condition
401 * This will only display if this is truthy.
404 * The formatted icon markup.
406 CRM
.utils
.formatIcon = function (icon
, text
, condition
) {
407 if (typeof condition
!== 'undefined' && !condition
) {
413 text
= _
.escape(text
);
414 title
= ' title="' + text
+ '"';
415 sr
= '<span class="sr-only">' + text
+ '</span>';
417 return '<i class="crm-i ' + icon
+ '"' + title
+ ' aria-hidden="true"></i>' + sr
;
421 * Wrapper for select2 initialization function; supplies defaults
422 * @param options object
424 $.fn
.crmSelect2 = function(options
) {
425 if (options
=== 'destroy') {
426 return $(this).each(function() {
428 .removeClass('crm-ajax-select')
433 return $(this).each(function () {
438 allowClear
: !$el
.hasClass('required'),
439 formatResult
: formatCrmSelect2
,
440 formatSelection
: formatCrmSelect2
443 // quickform doesn't support optgroups so here's a hack :(
444 // Instead of using wrapAll or similar that repeatedly appends options to the group and redraw the page (=> very slow on large lists),
445 // build bulk HTML and insert in single shot
447 $('option[value^=crm_optgroup]', this).each(function () {
449 $(this).nextUntil('option[value^=crm_optgroup]').each(function () {
450 groupHtml
+= this.outerHTML
;
452 optGroups
[$(this).text()] = groupHtml
;
455 var replacedHtml
= '';
456 for (var groupLabel
in optGroups
) {
457 replacedHtml
+= '<optgroup label="' + groupLabel
+ '">' + optGroups
[groupLabel
] + '</optgroup>';
460 $el
.html(replacedHtml
);
463 // quickform does not support disabled option, so yet another hack to
464 // add disabled property for option values
465 $('option[value^=crm_disabled_opt]', this).attr('disabled', 'disabled');
467 // Placeholder icon - total hack hikacking the escapeMarkup function but select2 3.5 dosn't have any other callbacks for this :(
468 if ($el
.is('[class*=fa-]')) {
469 settings
.escapeMarkup = function (m
) {
470 var out
= _
.escape(m
),
471 placeholder
= settings
.placeholder
|| $el
.data('placeholder') || $el
.attr('placeholder') || $('option[value=""]', $el
).text();
472 if (m
.length
&& placeholder
=== m
) {
473 iconClass
= $el
.attr('class').match(/(fa-\S*)/)[1];
474 out
= '<i class="crm-i ' + iconClass
+ '" aria-hidden="true"></i> ' + out
;
481 .on('select2-loaded.crmSelect2', function() {
482 // Use description as title for each option
483 $('.crm-select2-row-description', '#select2-drop').each(function() {
484 $(this).closest('.select2-result-label').attr('title', $(this).text());
486 // Collapsible optgroups should be expanded when searching
487 if ($('#select2-drop.collapsible-optgroups-enabled .select2-search input.select2-input').val()) {
488 $('#select2-drop.collapsible-optgroups-enabled li.select2-result-with-children')
489 .addClass('optgroup-expanded');
492 // Handle collapsible optgroups
493 .on('select2-open', function(e
) {
494 var isCollapsible
= $(e
.target
).hasClass('collapsible-optgroups');
496 .off('.collapseOptionGroup')
497 .toggleClass('collapsible-optgroups-enabled', isCollapsible
);
500 .on('click.collapseOptionGroup', '.select2-result-with-children > .select2-result-label', function() {
501 $(this).parent().toggleClass('optgroup-expanded');
503 // If the first item in the list is an optgroup, expand it
504 .find('li.select2-result-with-children:first-child').addClass('optgroup-expanded');
507 .on('select2-close', function() {
508 $('#select2-drop').off('.collapseOptionGroup').removeClass('collapsible-optgroups-enabled');
511 // Defaults for single-selects
512 if ($el
.is('select:not([multiple])')) {
513 settings
.minimumResultsForSearch
= 10;
514 if ($('option:first', this).val() === '') {
515 settings
.placeholderOption
= 'first';
518 $.extend(settings
, $el
.data('select-params') || {}, options
|| {});
520 $el
.addClass('crm-ajax-select');
522 $el
.select2(settings
);
527 * @see CRM_Core_Form::addEntityRef for docs
528 * @param options object
530 $.fn
.crmEntityRef = function(options
) {
531 if (options
=== 'destroy') {
532 return $(this).each(function() {
533 var entity
= $(this).data('api-entity') || '';
536 .removeClass('crm-form-entityref crm-' + _
.kebabCase(entity
) + '-ref')
537 .crmSelect2('destroy');
540 options
= options
|| {};
541 options
.select
= options
.select
|| {};
542 return $(this).each(function() {
544 $el
= $(this).off('.crmEntity'),
545 entity
= options
.entity
|| $el
.data('api-entity') || 'Contact',
549 id
: 'user_contact_id',
550 label
: ts('Select Current User'),
551 icon
: 'fa-user-circle-o'
554 // Legacy: fix entity name if passed in as snake case
555 if (entity
.charAt(0).toUpperCase() !== entity
.charAt(0)) {
556 entity
= _
.capitalize(_
.camelCase(entity
));
558 $el
.data('api-entity', entity
);
559 $el
.data('select-params', $.extend({}, $el
.data('select-params') || {}, options
.select
));
560 $el
.data('api-params', $.extend(true, {}, $el
.data('api-params') || {}, options
.api
));
561 $el
.data('create-links', options
.create
|| $el
.data('create-links'));
562 var staticItems
= options
.static || $el
.data('static') || [];
563 _
.each(staticItems
, function(option
, i
) {
564 if (_
.isString(option
)) {
565 staticItems
[i
] = staticPresets
[option
];
569 function staticItemMarkup() {
570 if (!staticItems
.length
) {
573 var markup
= '<div class="crm-entityref-links crm-entityref-links-static">';
574 _
.each(staticItems
, function(link
) {
575 markup
+= ' <a class="crm-hover-button" href="#' + link
.id
+ '">' +
576 '<i class="crm-i ' + link
.icon
+ '" aria-hidden="true"></i> ' +
577 _
.escape(link
.label
) + '</a>';
583 $el
.addClass('crm-form-entityref crm-' + _
.kebabCase(entity
) + '-ref');
585 // Use select2 ajax helper instead of CRM.api3 because it provides more value
587 url
: CRM
.url('civicrm/ajax/rest'),
589 data: function (input
, page_num
) {
590 var params
= getEntityRefApiParams($el
);
591 params
.input
= input
;
592 params
.page_num
= page_num
;
594 entity
: $el
.data('api-entity'),
596 json
: JSON
.stringify(params
)
599 results: function(data
) {
600 return {more
: data
.more_results
, results
: data
.values
|| []};
603 minimumInputLength
: 1,
604 formatResult
: CRM
.utils
.formatSelect2Result
,
605 formatSelection
: formatEntityRefSelection
,
606 escapeMarkup
: _
.identity
,
607 initSelection: function($el
, callback
) {
609 multiple
= !!$el
.data('select-params').multiple
,
611 stored
= ($el
.data('entity-value') || []).concat(staticItems
);
615 var idsNeeded
= _
.difference(val
.split(','), _
.pluck(stored
, 'id'));
616 var existing
= _
.remove(stored
, function(item
) {
617 return _
.includes(val
.split(','), item
.id
);
619 // If we already have this data, just return it
620 if (!idsNeeded
.length
) {
621 callback(multiple
? existing
: existing
[0]);
623 var params
= $.extend({}, $el
.data('api-params') || {}, {id
: idsNeeded
.join(',')});
624 CRM
.api3($el
.data('api-entity'), 'getlist', params
).done(function(result
) {
625 callback(multiple
? result
.values
.concat(existing
) : result
.values
[0]);
626 // Trigger change (store data to avoid an infinite loop of lookups)
627 $el
.data('entity-value', result
.values
).trigger('change');
632 // Create new items inline - works for tags
633 if ($el
.data('create-links') && entity
=== 'Tag') {
634 selectParams
.createSearchChoice = function(term
, data
) {
635 if (!_
.findKey(data
, {label
: term
})) {
636 return {id
: "0", term
: term
, label
: term
+ ' (' + ts('new tag') + ')'};
639 selectParams
.tokenSeparators
= [','];
640 selectParams
.createSearchChoicePosition
= 'bottom';
641 $el
.on('select2-selecting.crmEntity', function(e
) {
644 e
.object
.label
= e
.object
.term
;
645 CRM
.api3(entity
, 'create', $.extend({name
: e
.object
.term
}, $el
.data('api-params').params
|| {}))
646 .done(function(created
) {
648 val
= $el
.select2('val'),
649 data
= $el
.select2('data'),
650 item
= {id
: created
.id
, label
: e
.object
.term
};
652 $el
.select2('data', item
, true);
654 else if ($.isArray(val
) && $.inArray("0", val
) > -1) {
655 _
.remove(data
, {id
: "0"});
657 $el
.select2('data', data
, true);
664 selectParams
.formatInputTooShort = function() {
665 var txt
= $el
.data('select-params').formatInputTooShort
|| $.fn
.select2
.defaults
.formatInputTooShort
.call(this);
666 txt
+= entityRefFiltersMarkup($el
) + staticItemMarkup() + renderEntityRefCreateLinks($el
);
669 selectParams
.formatNoMatches = function() {
670 var txt
= $el
.data('select-params').formatNoMatches
|| $.fn
.select2
.defaults
.formatNoMatches
;
671 txt
+= entityRefFiltersMarkup($el
) + renderEntityRefCreateLinks($el
);
674 $el
.on('select2-open.crmEntity', function() {
678 .on('click.crmEntity', 'a.crm-add-entity', function(e
) {
679 var extra
= $el
.data('api-params').extra
,
680 formUrl
= $(this).attr('href') + '&returnExtra=display_name,sort_name' + (extra
? (',' + extra
) : '');
681 $el
.select2('close');
682 CRM
.loadForm(formUrl
, {
683 dialog
: {width
: '50%', height
: 220}
684 }).on('crmFormSuccess', function(e
, data
) {
685 if (data
.status
=== 'success' && data
.id
) {
686 if (!data
.crmMessages
) {
687 CRM
.status(ts('%1 Created', {1: data
.label
|| data
.extra
.display_name
}));
689 data
.label
= data
.label
|| data
.extra
.sort_name
;
690 if ($el
.select2('container').hasClass('select2-container-multi')) {
691 var selection
= $el
.select2('data');
692 selection
.push(data
);
693 $el
.select2('data', selection
, true);
695 $el
.select2('data', data
, true);
701 .on('click.crmEntity', '.crm-entityref-links-static a', function(e
) {
702 var id
= $(this).attr('href').substr(1),
703 item
= _
.findWhere(staticItems
, {id
: id
});
704 $el
.select2('close');
705 if ($el
.select2('container').hasClass('select2-container-multi')) {
706 var selection
= $el
.select2('data');
707 if (!_
.findWhere(selection
, {id
: id
})) {
708 selection
.push(item
);
709 $el
.select2('data', selection
, true);
712 $el
.select2('data', item
, true);
716 .on('change.crmEntity', '.crm-entityref-filter-value', function() {
717 var filter
= $el
.data('user-filter') || {};
718 filter
.value
= $(this).val();
719 $(this).toggleClass('active', !!filter
.value
);
720 $el
.data('user-filter', filter
);
721 if (filter
.value
&& $(this).is('select')) {
722 // Once a filter has been chosen, rerender create links and refocus the search box
723 $el
.select2('close');
726 $('.crm-entityref-links-create', '#select2-drop').replaceWith(renderEntityRefCreateLinks($el
));
729 .on('change.crmEntity', 'select.crm-entityref-filter-key', function() {
730 var filter
= {key
: $(this).val()};
731 $(this).toggleClass('active', !!filter
.key
);
732 $el
.data('user-filter', filter
);
733 renderEntityRefFilterValue($el
);
734 $('.crm-entityref-filter-key', '#select2-drop').focus();
738 $el
.crmSelect2($.extend(settings
, $el
.data('select-params'), selectParams
));
743 * Combine api-params with user-filter
747 function getEntityRefApiParams($el
) {
749 params
= $.extend({params
: {}}, $el
.data('api-params') || {}),
750 // Prevent original data from being modified - $.extend and _.clone don't cut it, they pass nested objects by reference!
751 combined
= _
.cloneDeep(params
),
752 filter
= $.extend({}, $el
.data('user-filter') || {});
753 if (filter
.key
&& filter
.value
) {
754 // Fieldname may be prefixed with joins
755 var fieldName
= _
.last(filter
.key
.split('.'));
756 // Special case for contact type/sub-type combo
757 if (fieldName
=== 'contact_type' && (filter
.value
.indexOf('__') > 0)) {
758 combined
.params
[filter
.key
] = filter
.value
.split('__')[0];
759 combined
.params
[filter
.key
.replace('contact_type', 'contact_sub_type')] = filter
.value
.split('__')[1];
761 // Allow json-encoded api filters e.g. {"BETWEEN":[123,456]}
762 combined
.params
[filter
.key
] = filter
.value
.charAt(0) === '{' ? $.parseJSON(filter
.value
) : filter
.value
;
768 CRM
.utils
.copyAttributes = function ($source
, $target
, attributes
) {
769 _
.each(attributes
, function(name
) {
770 if ($source
.attr(name
) !== undefined) {
771 $target
.attr(name
, $source
.attr(name
));
776 CRM
.utils
.formatSelect2Result = function (row
) {
777 var markup
= '<div class="crm-select2-row">';
778 if (row
.image
!== undefined) {
779 markup
+= '<div class="crm-select2-image"><img src="' + row
.image
+ '"/></div>';
781 else if (row
.icon_class
) {
782 markup
+= '<div class="crm-select2-icon"><div class="crm-icon ' + row
.icon_class
+ '-icon"></div></div>';
784 markup
+= '<div><div class="crm-select2-row-label '+(row
.label_class
|| '')+'">' +
785 (row
.color
? '<span class="crm-select-item-color" style="background-color: ' + row
.color
+ '"></span> ' : '') +
786 (row
.icon
? '<i class="crm-i ' + row
.icon
+ '" aria-hidden="true"></i> ' : '') +
787 _
.escape((row
.prefix
!== undefined ? row
.prefix
+ ' ' : '') + row
.label
+ (row
.suffix
!== undefined ? ' ' + row
.suffix
: '')) +
789 '<div class="crm-select2-row-description">';
790 $.each(row
.description
|| [], function(k
, text
) {
791 markup
+= '<p>' + _
.escape(text
) + '</p> ';
793 markup
+= '</div></div></div>';
797 function formatEntityRefSelection(row
) {
798 return (row
.color
? '<span class="crm-select-item-color" style="background-color: ' + row
.color
+ '"></span> ' : '') +
799 _
.escape((row
.prefix
!== undefined ? row
.prefix
+ ' ' : '') + row
.label
+ (row
.suffix
!== undefined ? ' ' + row
.suffix
: ''));
802 function renderEntityRefCreateLinks($el
) {
804 createLinks
= $el
.data('create-links'),
805 params
= getEntityRefApiParams($el
).params
,
806 entity
= $el
.data('api-entity'),
807 markup
= '<div class="crm-entityref-links crm-entityref-links-create">';
808 if (!createLinks
|| (createLinks
=== true && !CRM
.config
.entityRef
.links
[entity
])) {
811 if (createLinks
=== true) {
812 createLinks
= params
.contact_type
? _
.where(CRM
.config
.entityRef
.links
[entity
], {type
: params
.contact_type
}) : CRM
.config
.entityRef
.links
[entity
];
814 _
.each(createLinks
, function(link
) {
815 markup
+= ' <a class="crm-add-entity crm-hover-button" href="' + link
.url
+ '">' +
816 '<i class="crm-i ' + (link
.icon
|| 'fa-plus-circle') + '" aria-hidden="true"></i> ' +
817 _
.escape(link
.label
) + '</a>';
823 function getEntityRefFilters($el
) {
825 entity
= $el
.data('api-entity'),
826 filters
= CRM
.config
.entityRef
.filters
[entity
] || [],
827 params
= $.extend({params
: {}}, $el
.data('api-params') || {}).params
,
829 _
.each(filters
, function(filter
) {
830 _
.defaults(filter
, {type
: 'select', 'attributes': {}, entity
: entity
});
831 if (!params
[filter
.key
]) {
832 // Filter out options if params don't match its condition
833 if (filter
.condition
&& !_
.isMatch(params
, _
.pick(filter
.condition
, _
.keys(params
)))) {
838 else if (filter
.key
== 'contact_type' && typeof params
.contact_sub_type
=== 'undefined') {
846 * Provide markup for entity ref filters
848 function entityRefFiltersMarkup($el
) {
850 filters
= getEntityRefFilters($el
),
851 filter
= $el
.data('user-filter') || {},
852 filterSpec
= filter
.key
? _
.find(filters
, {key
: filter
.key
}) : null;
853 if (!filters
.length
) {
856 var markup
= '<div class="crm-entityref-filters">' +
857 '<select class="crm-entityref-filter-key' + (filter
.key
? ' active' : '') + '">' +
858 '<option value="">' + _
.escape(ts('Refine search...')) + '</option>' +
859 CRM
.utils
.renderOptions(filters
, filter
.key
) +
860 '</select>' + entityRefFilterValueMarkup($el
, filter
, filterSpec
) + '</div>';
865 * Provide markup for entity ref filter value field
867 function entityRefFilterValueMarkup($el
, filter
, filterSpec
) {
871 attributes
= _
.cloneDeep(filterSpec
.attributes
);
872 if (filterSpec
.type
!== 'select') {
873 attributes
.type
= filterSpec
.type
;
874 attributes
.value
= typeof filter
.value
!== 'undefined' ? filter
.value
: '';
876 attributes
.class = 'crm-entityref-filter-value' + (filter
.value
? ' active' : '');
877 $.each(attributes
, function (attr
, val
) {
878 attrs
+= ' ' + attr
+ '="' + val
+ '"';
880 if (filterSpec
.type
=== 'select') {
881 var fieldName
= _
.last(filter
.key
.split('.')),
882 options
= [{key
: '', value
: ts('- select -')}];
883 if (filterSpec
.options
) {
884 options
= options
.concat(getEntityRefFilterOptions(fieldName
, $el
, filterSpec
));
886 markup
= '<select' + attrs
+ '>' + CRM
.utils
.renderOptions(options
, filter
.value
) + '</select>';
888 markup
= '<input' + attrs
+ '/>';
895 * Render the entity ref filter value field
897 function renderEntityRefFilterValue($el
) {
899 filter
= $el
.data('user-filter') || {},
900 filterSpec
= filter
.key
? _
.find(getEntityRefFilters($el
), {key
: filter
.key
}) : null,
901 $keyField
= $('.crm-entityref-filter-key', '#select2-drop'),
904 $('.crm-entityref-filter-value', '#select2-drop').remove();
905 $valField
= $(entityRefFilterValueMarkup($el
, filter
, filterSpec
));
906 $keyField
.after($valField
);
907 if (filterSpec
.type
=== 'select') {
908 loadEntityRefFilterOptions(filter
, filterSpec
, $valField
, $el
);
911 $('.crm-entityref-filter-value', '#select2-drop').hide().val('').change();
916 * Fetch options for a filter from cache or ajax api
918 function loadEntityRefFilterOptions(filter
, filterSpec
, $valField
, $el
) {
919 // Fieldname may be prefixed with joins - strip those out
920 var fieldName
= _
.last(filter
.key
.split('.'));
921 if (filterSpec
.options
) {
922 CRM
.utils
.setOptions($valField
, getEntityRefFilterOptions(fieldName
, $el
, filterSpec
), false, filter
.value
);
925 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', true);
926 CRM
.api3(filterSpec
.entity
, 'getoptions', {field
: fieldName
, context
: 'search', sequential
: 1})
927 .done(function(result
) {
928 var entity
= $el
.data('api-entity').toLowerCase();
929 // Store options globally so we don't have to look them up again
930 filterSpec
.options
= result
.values
;
931 $('.crm-entityref-filters select', '#select2-drop').prop('disabled', false);
932 CRM
.utils
.setOptions($valField
, getEntityRefFilterOptions(fieldName
, $el
, filterSpec
), false, filter
.value
);
936 function getEntityRefFilterOptions(fieldName
, $el
, filterSpec
) {
937 var values
= _
.cloneDeep(filterSpec
.options
),
938 params
= $.extend({params
: {}}, $el
.data('api-params') || {}).params
;
939 if (fieldName
=== 'contact_type' && params
.contact_type
) {
940 values
= _
.remove(values
, function(option
) {
941 return option
.key
.indexOf(params
.contact_type
+ '__') === 0;
947 //CRM-15598 - Override url validator method to allow relative url's (e.g. /index.htm)
948 $.validator
.addMethod("url", function(value
, element
) {
949 if (/^\//.test(value
)) {
950 // Relative url: prepend dummy path for validation.
951 value
= 'http://domain.tld' + value
;
953 // From jQuery Validation Plugin v1.12.0
954 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
);
958 * Wrapper for jQuery validate initialization function; supplies defaults
960 $.fn
.crmValidate = function(params
) {
961 return $(this).each(function () {
962 var validator
= $(this).validate();
964 validator
.settings
= $.extend({}, validator
.settings
, CRM
.validate
._defaults
, CRM
.validate
.params
);
965 // Call our custom validation handler.
966 $(validator
.currentForm
).on("invalid-form.validate", validator
.settings
.invalidHandler
);
967 // Call any post-initialization callbacks
968 if (CRM
.validate
.functions
&& CRM
.validate
.functions
.length
) {
969 $.each(CRM
.validate
.functions
, function(i
, func
) {
979 function submitOnceForm(e
) {
980 if (e
.isDefaultPrevented()) {
983 if (_
.contains(submitted
, e
.target
)) {
986 submitted
.push(e
.target
);
987 // Spin submit button icon
988 if (submitButton
&& $(submitButton
, e
.target
).length
) {
990 if ($(e
.target
).closest('.ui-dialog .crm-ajax-container')) {
991 var identifier
= $(submitButton
).attr('name') || $(submitButton
).attr('href');
993 submitButton
= $(e
.target
).closest('.ui-dialog').find('button[data-identifier="' + identifier
+ '"]')[0] || submitButton
;
996 var $icon
= $(submitButton
).siblings('.crm-i').add('.crm-i, .ui-icon', submitButton
);
997 $icon
.data('origClass', $icon
.attr('class')).removeClass().addClass('crm-i crm-submit-icon fa-spinner fa-pulse');
1001 // If form fails validation, restore button icon and reset the submitted array
1002 function submitFormInvalid(form
) {
1004 $('.crm-i.crm-submit-icon').each(function() {
1005 if ($(this).data('origClass')) {
1006 $(this).removeClass().addClass($(this).data('origClass'));
1011 // Initialize widgets
1013 .on('crmLoad', function(e
) {
1014 $('table.row-highlight', e
.target
)
1015 .off('.rowHighlight')
1016 .on('change.rowHighlight', 'input.select-row, input.select-rows', function (e
, data
) {
1017 var filter
, $table
= $(this).closest('table');
1018 if ($(this).hasClass('select-rows')) {
1019 filter
= $(this).prop('checked') ? ':not(:checked)' : ':checked';
1020 $('input.select-row' + filter
, $table
).prop('checked', $(this).prop('checked')).trigger('change', 'master-selected');
1023 $(this).closest('tr').toggleClass('crm-row-selected', $(this).prop('checked'));
1024 if (data
!== 'master-selected') {
1025 $('input.select-rows', $table
).prop('checked', $(".select-row:not(':checked')", $table
).length
< 1);
1029 .find('input.select-row:checked').parents('tr').addClass('crm-row-selected');
1030 $('.crm-sortable-list', e
.target
).sortable();
1031 $('table.crm-sortable', e
.target
).DataTable();
1032 $('table.crm-ajax-table', e
.target
).each(function() {
1035 script
= CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmAjaxTable.js',
1036 $accordion
= $table
.closest('.crm-accordion-wrapper.collapsed, .crm-collapsible.collapsed');
1037 // For tables hidden by collapsed accordions, wait.
1038 if ($accordion
.length
) {
1039 $accordion
.one('crmAccordion:open', function() {
1040 CRM
.loadScript(script
).done(function() {
1041 $table
.crmAjaxTable();
1045 CRM
.loadScript(script
).done(function() {
1046 $table
.crmAjaxTable();
1050 if ($("input:radio[name=radio_ts]").length
== 1) {
1051 $("input:radio[name=radio_ts]").prop("checked", true);
1053 $('.crm-select2:not(.select2-offscreen, .select2-container)', e
.target
).crmSelect2();
1054 $('.crm-form-entityref:not(.select2-offscreen, .select2-container)', e
.target
).crmEntityRef();
1055 $('select.crm-chain-select-control', e
.target
).off('.chainSelect').on('change.chainSelect', chainSelect
);
1056 $('.crm-form-text[data-crm-datepicker]', e
.target
).each(function() {
1057 $(this).crmDatepicker($(this).data('crmDatepicker'));
1059 $('.crm-editable', e
.target
).not('thead *').each(function() {
1061 CRM
.loadScript(CRM
.config
.resourceBase
+ 'js/jquery/jquery.crmEditable.js').done(function() {
1065 // Cache Form Input initial values
1066 $('form[data-warn-changes] :input', e
.target
).each(function() {
1067 $(this).data('crm-initial-value', $(this).is(':checkbox, :radio') ? $(this).prop('checked') : $(this).val());
1069 $('textarea.crm-form-wysiwyg', e
.target
).each(function() {
1070 if ($(this).hasClass("collapsed")) {
1071 CRM
.wysiwyg
.createCollapsed(this);
1073 CRM
.wysiwyg
.create(this);
1076 // Submit once handlers
1077 $('form[data-submit-once]', e
.target
)
1078 .submit(submitOnceForm
)
1079 .on('invalid-form', submitFormInvalid
);
1080 $('form[data-submit-once] button[type=submit]', e
.target
).click(function(e
) {
1081 submitButton
= e
.target
;
1084 .on('dialogopen', function(e
) {
1085 var $el
= $(e
.target
);
1086 $('body').addClass('ui-dialog-open');
1087 // Modal dialogs should disable scrollbars
1088 if ($el
.dialog('option', 'modal')) {
1089 $el
.addClass('modal-dialog');
1090 $('body').css({overflow
: 'hidden'});
1092 $el
.parent().find('.ui-dialog-titlebar .ui-icon-closethick').removeClass('ui-icon-closethick').addClass('fa-times');
1093 // Add resize button
1094 if ($el
.parent().hasClass('crm-container') && $el
.dialog('option', 'resizable')) {
1095 $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}));
1096 $('.crm-dialog-titlebar-resize', $el
.parent()).click(function(e
) {
1097 if ($el
.data('origSize')) {
1098 $el
.dialog('option', $el
.data('origSize'));
1099 $el
.data('origSize', null);
1100 $(this).button('option', 'icons', {primary
: 'fa-expand'});
1102 var menuHeight
= $('#civicrm-menu').outerHeight();
1103 if ($('body').hasClass('crm-menubar-below-cms-menu')) {
1104 menuHeight
+= $('#civicrm-menu').offset().top
;
1106 $el
.data('origSize', {
1107 position
: {my
: 'center', at
: 'center center+' + (menuHeight
/ 2), of: window
},
1108 width
: $el
.dialog('option', 'width'),
1109 height
: $el
.dialog('option', 'height')
1111 $el
.dialog('option', {width
: '100%', height
: ($(window
).height() - menuHeight
), position
: {my
: "top", at
: "top+"+menuHeight
, of: window
}});
1112 $(this).button('option', 'icons', {primary
: 'fa-compress'});
1114 $el
.trigger('dialogresize');
1119 .on('dialogclose', function(e
) {
1120 // Restore scrollbars when closing modal
1121 if ($('.ui-dialog .modal-dialog:visible').not(e
.target
).length
< 1) {
1122 $('body').css({overflow
: ''});
1124 if ($('.ui-dialog-content:visible').not(e
.target
).length
< 1) {
1125 $('body').removeClass('ui-dialog-open');
1128 .on('submit', function(e
) {
1129 // CRM-14353 - disable changes warn when submitting a form
1130 $('[data-warn-changes]').attr('data-warn-changes', 'false');
1133 // CRM-14353 - Warn of unsaved changes for forms which have opted in
1134 window
.onbeforeunload = function() {
1135 if (CRM
.utils
.initialValueChanged($('form[data-warn-changes=true]:visible'))) {
1136 return ts('You have unsaved changes.');
1140 $.fn
.crmtooltip = function () {
1141 var TOOLTIP_HIDE_DELAY
= 300;
1144 .on('mouseover', 'a.crm-summary-link:not(.crm-processed)', function (e
) {
1145 $(this).addClass('crm-processed crm-tooltip-active');
1146 var topDistance
= e
.pageY
- $(window
).scrollTop();
1147 if (topDistance
< 300 || topDistance
< $(this).children('.crm-tooltip-wrapper').height()) {
1148 $(this).addClass('crm-tooltip-down');
1150 if (!$(this).children('.crm-tooltip-wrapper').length
) {
1151 $(this).append('<div class="crm-tooltip-wrapper"><div class="crm-tooltip"></div></div>');
1152 $(this).children().children('.crm-tooltip')
1153 .html('<div class="crm-loading-element"></div>')
1157 .on('mouseleave', 'a.crm-summary-link', function () {
1158 var tooltipLink
= $(this);
1159 setTimeout(function () {
1160 if (tooltipLink
.filter(':hover').length
=== 0) {
1161 tooltipLink
.removeClass('crm-processed crm-tooltip-active crm-tooltip-down');
1163 }, TOOLTIP_HIDE_DELAY
);
1165 .on('click', 'a.crm-summary-link', false);
1168 var helpDisplay
, helpPrevious
;
1169 // Non-ajax example:
1170 // CRM.help('Example title', 'Here is some text to describe this example');
1171 // Ajax example (will load help id "foo" from templates/CRM/bar.tpl):
1172 // CRM.help('Example title', {id: 'foo', file: 'CRM/bar'});
1173 CRM
.help = function (title
, params
, url
) {
1174 var ajax
= typeof params
!== 'string';
1175 if (helpDisplay
&& helpDisplay
.close
) {
1176 // If the same link is clicked twice, just close the display
1177 if (helpDisplay
.isOpen
&& _
.isEqual(helpPrevious
, params
)) {
1178 helpDisplay
.close();
1181 helpDisplay
.close();
1183 helpPrevious
= _
.cloneDeep(params
);
1184 helpDisplay
= CRM
.alert(ajax
? '...' : params
, title
, 'crm-help ' + (ajax
? 'crm-msg-loading' : 'info'), {expires
: 0});
1187 url
= CRM
.url('civicrm/ajax/inline');
1188 params
.class_name
= 'CRM_Core_Page_Inline_Help';
1189 params
.type
= 'page';
1194 success: function (data
) {
1195 $('#crm-notification-container .crm-help .notify-content:last').html(data
);
1196 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('info');
1198 error: function () {
1199 $('#crm-notification-container .crm-help .notify-content:last').html('Unable to load help file.');
1200 $('#crm-notification-container .crm-help').removeClass('crm-msg-loading').addClass('error');
1206 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1208 CRM
.status = function(options
, deferred
) {
1209 // 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.
1210 if (typeof options
=== 'string') {
1211 return CRM
.status({start
: options
, success
: options
, error
: options
})[deferred
=== 'error' ? 'reject' : 'resolve']();
1213 var opts
= $.extend({
1214 start
: ts('Saving...'),
1215 success
: ts('Saved'),
1216 error: function(data
) {
1217 var msg
= $.isPlainObject(data
) && data
.error_message
;
1218 CRM
.alert(msg
|| ts('Sorry an error occurred and your information was not saved'), ts('Error'), 'error');
1221 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>')
1223 $msg
.css('min-width', $msg
.width());
1224 function handle(status
, data
) {
1225 var endMsg
= typeof(opts
[status
]) === 'function' ? opts
[status
](data
) : opts
[status
];
1227 $msg
.removeClass('status-start').addClass('status-' + status
).find('.crm-status-box-msg').text(endMsg
);
1228 window
.setTimeout(function() {
1229 $msg
.fadeOut('slow', function() {
1237 return (deferred
|| new $.Deferred())
1238 .done(function(data
) {
1239 // If the server returns an error msg call the error handler
1240 var status
= $.isPlainObject(data
) && (data
.is_error
|| data
.status
=== 'error') ? 'error' : 'success';
1241 handle(status
, data
);
1243 .fail(function(data
) {
1244 handle('error', data
);
1248 // Convert an Angular promise to a jQuery promise
1249 CRM
.toJqPromise = function(aPromise
) {
1250 var jqDeferred
= $.Deferred();
1252 function(data
) { jqDeferred
.resolve(data
); },
1253 function(data
) { jqDeferred
.reject(data
); }
1254 // should we also handle progress events?
1256 return jqDeferred
.promise();
1259 CRM
.toAPromise = function($q
, jqPromise
) {
1260 var aDeferred
= $q
.defer();
1262 function(data
) { aDeferred
.resolve(data
); },
1263 function(data
) { aDeferred
.reject(data
); }
1264 // should we also handle progress events?
1266 return aDeferred
.promise
;
1270 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1272 CRM
.alert = function (text
, title
, type
, options
) {
1273 type
= type
|| 'alert';
1274 title
= title
|| '';
1275 options
= options
|| {};
1276 if ($('#crm-notification-container').length
) {
1282 // By default, don't expire errors and messages containing links
1284 expires
: (type
== 'error' || text
.indexOf('<a ') > -1) ? 0 : (text
? 10000 : 5000),
1287 options
= $.extend(extra
, options
);
1288 options
.expires
= (options
.expires
=== false || !CRM
.config
.allowAlertAutodismissal
) ? 0 : parseInt(options
.expires
, 10);
1289 if (options
.unique
&& options
.unique
!== '0') {
1290 $('#crm-notification-container .ui-notify-message').each(function () {
1291 if (title
=== $('h1', this).html() && text
=== $('.notify-content', this).html()) {
1292 $('.icon.ui-notify-close', this).click();
1296 return $('#crm-notification-container').notify('create', params
, options
);
1300 text
= title
+ "\n" + text
;
1302 // strip html tags as they are not parsed in standard alerts
1303 alert($("<div/>").html(text
).text());
1309 * Close whichever alert contains the given node
1313 CRM
.closeAlertByChild = function (node
) {
1314 $(node
).closest('.ui-notify-message').find('.icon.ui-notify-close').click();
1318 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1320 CRM
.confirm = function (options
) {
1321 var dialog
, url
, msg
, buttons
= [], settings
= {
1322 title
: ts('Confirm'),
1323 message
: ts('Are you sure you want to continue?'),
1328 dialogClass
: 'crm-container crm-confirm',
1329 close: function () {
1330 $(this).dialog('destroy').remove();
1337 if (options
&& options
.url
) {
1338 settings
.resizable
= true;
1339 settings
.height
= '50%';
1341 $.extend(settings
, ($.isFunction(options
) ? arguments
[1] : options
) || {});
1342 settings
= CRM
.utils
.adjustDialogDefaults(settings
);
1343 if (!settings
.buttons
&& $.isPlainObject(settings
.options
)) {
1344 $.each(settings
.options
, function(op
, label
) {
1348 icons
: {primary
: op
=== 'no' ? 'fa-times' : 'fa-check'},
1350 var event
= $.Event('crmConfirm:' + op
);
1351 $(this).trigger(event
);
1352 if (!event
.isDefaultPrevented()) {
1353 dialog
.dialog('close');
1358 // Order buttons so that "no" goes on the right-hand side
1359 settings
.buttons
= _
.sortBy(buttons
, 'data-op').reverse();
1362 msg
= url
? '' : settings
.message
;
1363 delete settings
.options
;
1364 delete settings
.message
;
1365 delete settings
.url
;
1366 dialog
= $('<div class="crm-confirm-dialog"></div>').html(msg
|| '').dialog(settings
);
1367 if ($.isFunction(options
)) {
1368 dialog
.on('crmConfirm:yes', options
);
1371 CRM
.loadPage(url
, {target
: dialog
});
1374 dialog
.trigger('crmLoad');
1379 /** provides a local copy of ts for a domain */
1380 CRM
.ts = function(domain
) {
1381 return function(message
, options
) {
1383 options
= $.extend(options
|| {}, {domain
: domain
});
1385 return ts(message
, options
);
1389 CRM
.addStrings = function(domain
, strings
) {
1390 var bucket
= (domain
== 'civicrm' ? 'strings' : 'strings::' + domain
);
1391 CRM
[bucket
] = CRM
[bucket
] || {};
1392 _
.extend(CRM
[bucket
], strings
);
1396 * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
1398 $.fn
.crmError = function (text
, title
, options
) {
1399 title
= title
|| '';
1401 options
= options
|| {};
1406 if ($(this).length
) {
1408 label
= $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
1410 label
.addClass('crm-error');
1411 var $label
= label
.clone();
1412 if (text
=== '' && $('.crm-marker', $label
).length
> 0) {
1413 text
= $('.crm-marker', $label
).attr('title');
1415 $('.crm-marker', $label
).remove();
1416 title
= $label
.text();
1419 $(this).addClass('crm-error');
1421 var msg
= CRM
.alert(text
, title
, 'error', $.extend(extra
, options
));
1422 if ($(this).length
) {
1424 setTimeout(function () {
1425 ele
.one('change', function () {
1426 if (msg
&& msg
.close
) msg
.close();
1427 ele
.removeClass('crm-error');
1429 label
.removeClass('crm-error');
1437 // Display system alerts through js notifications
1438 function messagesFromMarkup() {
1439 $('div.messages:visible', this).not('.help').not('.no-popup').each(function () {
1440 var text
, title
= '';
1441 $(this).removeClass('status messages');
1442 var type
= $(this).attr('class').split(' ')[0] || 'alert';
1443 type
= type
.replace('crm-', '');
1444 $('.icon', this).remove();
1445 if ($('.msg-text', this).length
> 0) {
1446 text
= $('.msg-text', this).html();
1447 title
= $('.msg-title', this).html();
1450 text
= $(this).html();
1452 var options
= $(this).data('options') || {};
1454 // Duplicates were already removed server-side
1455 options
.unique
= false;
1456 CRM
.alert(text
, title
, type
, options
);
1458 // Handle qf form errors
1459 $('form :input.error', this).one('blur', function() {
1460 $('.ui-notify-message.error a.ui-notify-close').click();
1461 $(this).removeClass('error');
1462 $(this).next('span.crm-error').remove();
1463 $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]')
1464 .removeClass('crm-error')
1465 .find('.crm-error').removeClass('crm-error');
1470 * Improve blockUI when used with jQuery dialog
1472 var originalBlock
= $.fn
.block
,
1473 originalUnblock
= $.fn
.unblock
;
1475 $.fn
.block = function(opts
) {
1476 if ($(this).is('.ui-dialog-content')) {
1477 originalBlock
.call($(this).parents('.ui-dialog'), opts
);
1480 return originalBlock
.call(this, opts
);
1482 $.fn
.unblock = function(opts
) {
1483 if ($(this).is('.ui-dialog-content')) {
1484 originalUnblock
.call($(this).parents('.ui-dialog'), opts
);
1487 return originalUnblock
.call(this, opts
);
1490 // Preprocess all CRM ajax calls to display messages
1491 $(document
).ajaxSuccess(function(event
, xhr
, settings
) {
1493 if ((!settings
.dataType
|| settings
.dataType
== 'json') && xhr
.responseText
) {
1494 var response
= $.parseJSON(xhr
.responseText
);
1495 if (typeof(response
.crmMessages
) == 'object') {
1496 $.each(response
.crmMessages
, function(n
, msg
) {
1497 CRM
.alert(msg
.text
, msg
.title
, msg
.type
, msg
.options
);
1500 if (response
.backtrace
) {
1501 CRM
.console('log', response
.backtrace
);
1503 if (typeof response
.deprecated
=== 'string') {
1504 CRM
.console('warn', response
.deprecated
);
1508 // Ignore errors thrown by parseJSON
1513 $.blockUI
.defaults
.message
= null;
1514 $.blockUI
.defaults
.ignoreIfBlocked
= true;
1516 if ($('#crm-container').hasClass('crm-public')) {
1517 $.fn
.select2
.defaults
.dropdownCssClass
= $.ui
.dialog
.prototype.options
.dialogClass
= 'crm-container crm-public';
1520 // Trigger crmLoad on initial content for consistency. It will also be triggered for ajax-loaded content.
1521 $('.crm-container').trigger('crmLoad');
1523 if ($('#crm-notification-container').length
) {
1524 // Initialize notifications
1525 $('#crm-notification-container').notify();
1526 messagesFromMarkup
.call($('#crm-container'));
1530 // bind the event for image popup
1531 .on('click', 'a.crm-image-popup', function(e
) {
1533 title
: ts('Preview'),
1535 // Prevent overlap with the menubar
1536 maxHeight
: $(window
).height() - 30,
1537 position
: {my
: 'center', at
: 'center center+15', of: window
},
1538 message
: '<div class="crm-custom-image-popup"><img style="max-width: 100%" src="' + $(this).attr('href') + '"></div>',
1544 .on('click', function (event
) {
1545 $('.btn-slide-active').removeClass('btn-slide-active').find('.panel').hide();
1546 if ($(event
.target
).is('.btn-slide')) {
1547 $(event
.target
).addClass('btn-slide-active').find('.panel').show();
1551 // Handle clear button for form elements
1552 .on('click', 'a.crm-clear-link', function() {
1553 $(this).css({visibility
: 'hidden'}).siblings('.crm-form-radio:checked').prop('checked', false).trigger('change', ['crmClear']);
1554 $(this).siblings('input:text').val('').trigger('change', ['crmClear']);
1557 .on('change keyup', 'input.crm-form-radio:checked, input[allowclear=1]', function(e
, context
) {
1558 if (context
!== 'crmClear' && ($(this).is(':checked') || ($(this).is('[allowclear=1]') && $(this).val()))) {
1559 $(this).siblings('.crm-clear-link').css({visibility
: ''});
1561 if (context
!== 'crmClear' && $(this).is('[allowclear=1]') && $(this).val() === '') {
1562 $(this).siblings('.crm-clear-link').css({visibility
: 'hidden'});
1566 // Allow normal clicking of links within accordions
1567 .on('click.crmAccordions', 'div.crm-accordion-header a, .collapsible-title a', function (e
) {
1568 e
.stopPropagation();
1570 // Handle accordions
1571 .on('click.crmAccordions', '.crm-accordion-header, .crm-collapsible .collapsible-title', function (e
) {
1572 var action
= 'open';
1573 if ($(this).parent().hasClass('collapsed')) {
1574 $(this).next().css('display', 'none').slideDown(200);
1577 $(this).next().css('display', 'block').slideUp(200);
1580 $(this).parent().toggleClass('collapsed').trigger('crmAccordion:' + action
);
1588 * Collapse or expand an accordion
1591 $.fn
.crmAccordionToggle = function (speed
) {
1592 $(this).each(function () {
1593 var action
= 'open';
1594 if ($(this).hasClass('collapsed')) {
1595 $('.crm-accordion-body', this).first().css('display', 'none').slideDown(speed
);
1598 $('.crm-accordion-body', this).first().css('display', 'block').slideUp(speed
);
1601 $(this).toggleClass('collapsed').trigger('crmAccordion:' + action
);
1606 * Clientside currency formatting
1607 * @param number value
1608 * @param [optional] boolean onlyNumber - if true, we return formatted amount without currency sign
1609 * @param [optional] string format - currency representation of the number 1234.56
1612 var currencyTemplate
;
1613 CRM
.formatMoney = function(value
, onlyNumber
, format
) {
1614 var precision
, decimal, separator
, sign
, i
, j
, result
;
1615 if (value
=== 'init' && format
) {
1616 currencyTemplate
= format
;
1619 format
= format
|| currencyTemplate
;
1620 if ((result
= /1(.?)234(.?)56/.exec(format
)) !== null) { // If value is formatted to 2 decimals
1623 else if ((result
= /1(.?)234(.?)6/.exec(format
)) !== null) { // If value is formatted to 1 decimal
1626 else if ((result
= /1(.?)235/.exec(format
)) !== null) { // If value is formatted to zero decimals
1630 return 'Invalid format passed to CRM.formatMoney';
1632 separator
= result
[1];
1633 decimal = precision
? result
[2] : false;
1634 sign
= (value
< 0) ? '-' : '';
1635 //extracting the absolute value of the integer part of the number and converting to string
1636 i
= parseInt(value
= Math
.abs(value
).toFixed(2)) + '';
1637 j
= ((j
= i
.length
) > 3) ? j
% 3 : 0;
1638 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) : '');
1642 switch (precision
) {
1644 return format
.replace(/1.*234.*56/, result
);
1646 return format
.replace(/1.*234.*6/, result
);
1648 return format
.replace(/1.*235/, result
);
1652 CRM
.angRequires = function(name
) {
1653 return CRM
.angular
.requires
[name
] || [];
1656 CRM
.console = function(method
, title
, msg
) {
1657 if (window
.console
) {
1658 method
= $.isFunction(console
[method
]) ? method
: 'log';
1659 if (msg
=== undefined) {
1660 return console
[method
](title
);
1662 return console
[method
](title
, msg
);
1667 // Sugar methods for window.localStorage, with a fallback for older browsers
1668 var cacheItems
= {};
1670 get: function (name
, defaultValue
) {
1672 if (localStorage
.getItem('CRM' + name
) !== null) {
1673 return JSON
.parse(localStorage
.getItem('CRM' + name
));
1676 return cacheItems
[name
] === undefined ? defaultValue
: cacheItems
[name
];
1678 set: function (name
, value
) {
1680 localStorage
.setItem('CRM' + name
, JSON
.stringify(value
));
1682 cacheItems
[name
] = value
;
1684 clear: function(name
) {
1686 localStorage
.removeItem('CRM' + name
);
1688 delete cacheItems
[name
];
1694 // Determine if a user has a given permission.
1695 // @see CRM_Core_Resources::addPermissions
1696 CRM
.checkPerm = function(perm
) {
1697 return CRM
.permissions
&& CRM
.permissions
[perm
];
1700 // Round while preserving sigfigs
1701 CRM
.utils
.sigfig = function(n
, digits
) {
1702 var len
= ("" + n
).length
;
1703 var scale
= Math
.pow(10.0, len
-digits
);
1704 return Math
.round(n
/ scale
) * scale
;
1708 * Create a js Date object from a unix timestamp or a yyyy-mm-dd string
1712 CRM
.utils
.makeDate = function(input
) {
1713 switch (typeof input
) {
1715 // already a date object
1719 // convert iso format with or without dashes
1720 input
= input
.replace(/[- :]/g, '');
1721 var output
= $.datepicker
.parseDate('yymmdd', input
.substr(0, 8));
1722 if (input
.length
=== 14) {
1724 parseInt(input
.substr(8, 2), 10),
1725 parseInt(input
.substr(10, 2), 10),
1726 parseInt(input
.substr(12, 2), 10)
1732 // convert unix timestamp
1733 return new Date(input
* 1000);
1735 throw 'Invalid input passed to CRM.utils.makeDate';
1739 * Format a date (and optionally time) for output to the user
1741 * @param {string|int|Date} input
1742 * Input may be a js Date object, a unix timestamp or a 'yyyy-mm-dd' string
1743 * @param {string|null} dateFormat
1744 * A string like 'yy-mm-dd' or null to use the system default
1745 * @param {int|bool} timeFormat
1746 * Leave empty to omit time from the output (default)
1747 * Or pass 12, 24, or true to use the system default for 12/24hr format
1750 CRM
.utils
.formatDate = function(input
, dateFormat
, timeFormat
) {
1754 var date
= CRM
.utils
.makeDate(input
),
1755 output
= $.datepicker
.formatDate(dateFormat
|| CRM
.config
.dateInputFormat
, date
);
1757 var hour
= date
.getHours(),
1758 min
= date
.getMinutes(),
1760 if (timeFormat
=== 12 || (timeFormat
=== true && !CRM
.config
.timeIs24Hr
)) {
1761 suf
= ' ' + (hour
< 12 ? ts('AM') : ts('PM'));
1762 if (hour
=== 0 || hour
> 12) {
1763 hour
= Math
.abs(hour
- 12);
1765 } else if (hour
< 10) {
1768 output
+= ' ' + hour
+ ':' + (min
< 10 ? '0' : '') + min
+ suf
;
1773 // Used to set appropriate text color for a given background
1774 CRM
.utils
.colorContrast = function (hexcolor
) {
1775 hexcolor
= hexcolor
.replace(/[ #]/g, '');
1776 var r
= parseInt(hexcolor
.substr(0, 2), 16),
1777 g
= parseInt(hexcolor
.substr(2, 2), 16),
1778 b
= parseInt(hexcolor
.substr(4, 2), 16),
1779 yiq
= ((r
* 299) + (g
* 587) + (b
* 114)) / 1000;
1780 return (yiq
>= 128) ? 'black' : 'white';
1783 // CVE-2015-9251 - Prevent auto-execution of scripts when no explicit dataType was provided
1784 $.ajaxPrefilter(function(s
) {
1785 if (s
.crossDomain
) {
1786 s
.contents
.script
= false;
1790 // 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.
1791 $.htmlPrefilter = function(html
) {
1792 // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
1793 // their XML equivalent: e.g., "<div />" to "<div></div>". This is
1794 // problematic for several reasons, including that it's vulnerable to XSS
1795 // attacks. However, since this was jQuery's behavior for many years, many
1796 // Drupal modules and jQuery plugins may be relying on it. Therefore, we
1797 // preserve that behavior, but for a limited set of tags only, that we believe
1798 // to not be vulnerable. This is the set of HTML tags that satisfy all of the
1799 // following conditions:
1800 // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
1801 // appear in that list, then we don't want to mess with it here either.
1802 // @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
1803 // - A normal element (not a void, template, text, or foreign element).
1804 // @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
1805 // - An element that is still defined by the current HTML specification
1806 // (not a deprecated element), because we do not want to rely on how
1807 // browsers parse deprecated elements.
1808 // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
1809 // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
1810 // designed for fragments, not entire documents.
1811 // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
1812 // regular expression, it didn't match on colgroup, and we don't want to
1813 // introduce a behavior change for that.
1814 var selfClosingTagsToReplace
= [
1815 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
1816 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
1817 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
1818 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
1819 'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
1820 'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
1821 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
1822 'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
1823 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
1824 'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
1827 // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
1828 // two expressions makes it easier to target <a/> without also targeting
1829 // every tag that starts with "a".
1830 var xhtmlRegExpGroup
= '(' + selfClosingTagsToReplace
.join('|') + ')';
1831 var whitespace
= '[\\x20\\t\\r\\n\\f]';
1832 var rxhtmlTagWithoutSpaceOrAttributes
= new RegExp('<' + xhtmlRegExpGroup
+ '\\/>', 'gi');
1833 var rxhtmlTagWithSpaceAndMaybeAttributes
= new RegExp('<' + xhtmlRegExpGroup
+ '(' + whitespace
+ '[^>]*)\\/>', 'gi');
1835 // jQuery 3.5 also fixed a vulnerability for when </select> appears within
1836 // an <option> or <optgroup>, but it did that in local code that we can't
1837 // backport directly. Instead, we filter such cases out. To do so, we need to
1838 // determine when jQuery would otherwise invoke the vulnerable code, which it
1839 // uses this regular expression to determine. The regular expression changed
1840 // for version 3.0.0 and changed again for 3.4.0.
1841 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
1842 // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
1843 // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
1844 var rtagName
= /<([\w:]+)/;
1846 // The regular expression that jQuery uses to determine which self-closing
1847 // tags to expand to open and close tags. This is vulnerable, because it
1848 // matches all tag names except the few excluded ones. We only use this
1849 // expression for determining vulnerability. The expression changed for
1850 // version 3, but we only need to check for vulnerability in versions 1 and 2,
1851 // so we use the expression from those versions.
1852 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
1853 var rxhtmlTag
= /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
1855 // This is how jQuery determines the first tag in the HTML.
1856 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
1857 var tag
= ( rtagName
.exec( html
) || [ "", "" ] )[ 1 ].toLowerCase();
1859 // It is not valid HTML for <option> or <optgroup> to have <select> as
1860 // either a descendant or sibling, and attempts to inject one can cause
1861 // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
1862 // possible XSS attack, reject the entire string.
1863 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
1864 if ((tag
=== 'option' || tag
=== 'optgroup') && html
.match(/<\/?select/i)) {
1868 // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
1869 // the tags in the `selfClosingTagsToReplace` list defined above.
1870 // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
1871 // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
1872 html
= html
.replace(rxhtmlTagWithoutSpaceOrAttributes
, "<$1></$1>");
1873 html
= html
.replace(rxhtmlTagWithSpaceAndMaybeAttributes
, "<$1$2></$1>");