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