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