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