}
}
-/**
- * Function to change button text and disable one it is clicked
- * @deprecated
- * @param obj object - the button clicked
- * @param formID string - the id of the form being submitted
- * @param string procText - button text after user clicks it
- * @return bool
- */
var submitcount = 0;
-/* Changes button label on submit, and disables button after submit for newer browsers.
- Puts up alert for older browsers. */
-function submitOnce(obj, formId, procText) {
- // if named button clicked, change text
- if (obj.value != null) {
- cj('input[name=' + obj.name + ']').val(procText + " ...");
- }
- cj(obj).closest('form').attr('data-warn-changes', 'false');
- if (document.getElementById) { // disable submit button for newer browsers
- cj('input[name=' + obj.name + ']').attr("disabled", true);
- document.getElementById(formId).submit();
- return true;
- }
- else { // for older browsers
- if (submitcount == 0) {
- submitcount++;
- return true;
- }
- else {
- alert("Your request is currently being processed ... Please wait.");
- return false;
- }
- }
-}
/**
* Function to show / hide the row in optionFields
$.propHooks.disabled = {
set: function (el, value, name) {
// Sync button enabled status with wrapper css
- if ($(el).is('span.crm-button > input.crm-form-submit')) {
+ if ($(el).is('.crm-button.crm-form-submit')) {
$(el).parent().toggleClass('crm-button-disabled', !!value);
}
// Sync button enabled status with dialog button
};
var scriptsLoaded = {};
- CRM.loadScript = function(url) {
+ CRM.loadScript = function(url, appendCacheCode) {
if (!scriptsLoaded[url]) {
- var script = document.createElement('script');
+ var script = document.createElement('script'),
+ src = url;
+ if (appendCacheCode !== false) {
+ src += (_.includes(url, '?') ? '&r=' : '?r=') + CRM.config.resourceCacheCode;
+ }
scriptsLoaded[url] = $.Deferred();
script.onload = function () {
// Give the script time to execute
CRM.CMSjQuery = window.jQuery;
window.jQuery = CRM.$;
}
- script.src = url;
+ script.src = src;
document.getElementsByTagName("head")[0].appendChild(script);
}
return scriptsLoaded[url];
opts = placeholder || placeholder === '' ? '' : '[value!=""]';
$elect.find('option' + opts).remove();
var newOptions = CRM.utils.renderOptions(options, val);
+ if (options.length == 0) {
+ $elect.removeClass('required');
+ } else if ($elect.hasClass('crm-field-required') && !$elect.hasClass('required')) {
+ $elect.addClass('required');
+ }
if (typeof placeholder === 'string') {
if ($elect.is('[multiple]')) {
select.attr('placeholder', placeholder);
return rendered;
};
+ CRM.utils.getOptions = function(select) {
+ var options = [];
+ $('option', select).each(function() {
+ var option = {key: $(this).attr('value'), value: $(this).text()};
+ if (option.key !== '') {
+ options.push(option);
+ }
+ });
+ return options;
+ };
+
function chainSelect() {
var $form = $(this).closest('form'),
$target = $('select[data-name="' + $(this).data('target') + '"]', $form),
description = row.description || $(row.element).data('description'),
ret = '';
if (icon) {
- ret += '<i class="crm-i ' + icon + '"></i> ';
+ ret += '<i class="crm-i ' + icon + '" aria-hidden="true"></i> ';
}
if (color) {
ret += '<span class="crm-select-item-color" style="background-color: ' + color + '"></span> ';
return ret + _.escape(row.text) + (description ? '<div class="crm-select2-row-description"><p>' + _.escape(description) + '</p></div>' : '');
}
+ /**
+ * Helper to generate an icon with alt text.
+ *
+ * See also smarty `{icon}` and CRM_Core_Page::crmIcon() functions
+ *
+ * @param string icon
+ * The Font Awesome icon class to use.
+ * @param string text
+ * Alt text to display.
+ * @param mixed condition
+ * This will only display if this is truthy.
+ *
+ * @return string
+ * The formatted icon markup.
+ */
+ CRM.utils.formatIcon = function (icon, text, condition) {
+ if (typeof condition !== 'undefined' && !condition) {
+ return '';
+ }
+ var title = '';
+ var sr = '';
+ if (text) {
+ text = _.escape(text);
+ title = ' title="' + text + '"';
+ sr = '<span class="sr-only">' + text + '</span>';
+ }
+ return '<i class="crm-i ' + icon + '"' + title + ' aria-hidden="true"></i>' + sr;
+ };
+
/**
* Wrapper for select2 initialization function; supplies defaults
* @param options object
return $(this).each(function() {
$(this)
.removeClass('crm-ajax-select')
+ .off('.crmSelect2')
.select2('destroy');
});
}
};
// quickform doesn't support optgroups so here's a hack :(
+ // Instead of using wrapAll or similar that repeatedly appends options to the group and redraw the page (=> very slow on large lists),
+ // build bulk HTML and insert in single shot
var optGroups = {};
$('option[value^=crm_optgroup]', this).each(function () {
var groupHtml = '';
placeholder = settings.placeholder || $el.data('placeholder') || $el.attr('placeholder') || $('option[value=""]', $el).text();
if (m.length && placeholder === m) {
iconClass = $el.attr('class').match(/(fa-\S*)/)[1];
- out = '<i class="crm-i ' + iconClass + '"></i> ' + out;
+ out = '<i class="crm-i ' + iconClass + '" aria-hidden="true"></i> ' + out;
}
return out;
};
}
+ // Use description as title for each option
+ $el.on('select2-loaded.crmSelect2', function() {
+ $('.crm-select2-row-description', '#select2-drop').each(function() {
+ $(this).closest('.select2-result-label').attr('title', $(this).text());
+ });
+ });
+
// Defaults for single-selects
if ($el.is('select:not([multiple])')) {
settings.minimumResultsForSearch = 10;
var entity = $(this).data('api-entity') || '';
$(this)
.off('.crmEntity')
- .removeClass('crm-form-entityref crm-' + entity.toLowerCase() + '-ref')
+ .removeClass('crm-form-entityref crm-' + _.kebabCase(entity) + '-ref')
.crmSelect2('destroy');
});
}
return $(this).each(function() {
var
$el = $(this).off('.crmEntity'),
- entity = options.entity || $el.data('api-entity') || 'contact',
- selectParams = {};
+ entity = options.entity || $el.data('api-entity') || 'Contact',
+ selectParams = {},
+ staticPresets = {
+ user_contact_id: {
+ id: 'user_contact_id',
+ label: ts('Select Current User'),
+ icon: 'fa-user-circle-o'
+ }
+ };
+ // Legacy: fix entity name if passed in as snake case
+ if (entity.charAt(0).toUpperCase() !== entity.charAt(0)) {
+ entity = _.capitalize(_.camelCase(entity));
+ }
$el.data('api-entity', entity);
$el.data('select-params', $.extend({}, $el.data('select-params') || {}, options.select));
$el.data('api-params', $.extend(true, {}, $el.data('api-params') || {}, options.api));
$el.data('create-links', options.create || $el.data('create-links'));
- $el.addClass('crm-form-entityref crm-' + entity.toLowerCase() + '-ref');
+ var staticItems = options.static || $el.data('static') || [];
+ _.each(staticItems, function(option, i) {
+ if (_.isString(option)) {
+ staticItems[i] = staticPresets[option];
+ }
+ });
+
+ function staticItemMarkup() {
+ if (!staticItems.length) {
+ return '';
+ }
+ var markup = '<div class="crm-entityref-links crm-entityref-links-static">';
+ _.each(staticItems, function(link) {
+ markup += ' <a class="crm-hover-button" href="#' + link.id + '">' +
+ '<i class="crm-i ' + link.icon + '" aria-hidden="true"></i> ' +
+ _.escape(link.label) + '</a>';
+ });
+ markup += '</div>';
+ return markup;
+ }
+
+ $el.addClass('crm-form-entityref crm-' + _.kebabCase(entity) + '-ref');
var settings = {
// Use select2 ajax helper instead of CRM.api3 because it provides more value
ajax: {
var
multiple = !!$el.data('select-params').multiple,
val = $el.val(),
- stored = $el.data('entity-value') || [];
+ stored = ($el.data('entity-value') || []).concat(staticItems);
if (val === '') {
return;
}
+ var idsNeeded = _.difference(val.split(','), _.pluck(stored, 'id'));
+ var existing = _.remove(stored, function(item) {
+ return _.includes(val.split(','), item.id);
+ });
// If we already have this data, just return it
- if (!_.xor(val.split(','), _.pluck(stored, 'id')).length) {
- callback(multiple ? stored : stored[0]);
+ if (!idsNeeded.length) {
+ callback(multiple ? existing : existing[0]);
} else {
- var params = $.extend({}, $el.data('api-params') || {}, {id: val});
+ var params = $.extend({}, $el.data('api-params') || {}, {id: idsNeeded.join(',')});
CRM.api3($el.data('api-entity'), 'getlist', params).done(function(result) {
- callback(multiple ? result.values : result.values[0]);
+ callback(multiple ? result.values.concat(existing) : result.values[0]);
// Trigger change (store data to avoid an infinite loop of lookups)
$el.data('entity-value', result.values).trigger('change');
});
}
};
// Create new items inline - works for tags
- if ($el.data('create-links') && entity.toLowerCase() === 'tag') {
+ if ($el.data('create-links') && entity === 'Tag') {
selectParams.createSearchChoice = function(term, data) {
if (!_.findKey(data, {label: term})) {
return {id: "0", term: term, label: term + ' (' + ts('new tag') + ')'};
else {
selectParams.formatInputTooShort = function() {
var txt = $el.data('select-params').formatInputTooShort || $.fn.select2.defaults.formatInputTooShort.call(this);
- txt += entityRefFiltersMarkup($el) + renderEntityRefCreateLinks($el);
+ txt += entityRefFiltersMarkup($el) + staticItemMarkup() + renderEntityRefCreateLinks($el);
return txt;
};
selectParams.formatNoMatches = function() {
};
$el.on('select2-open.crmEntity', function() {
var $el = $(this);
- renderEntityRefFilterValue($el);
$('#select2-drop')
.off('.crmEntity')
.on('click.crmEntity', 'a.crm-add-entity', function(e) {
formUrl = $(this).attr('href') + '&returnExtra=display_name,sort_name' + (extra ? (',' + extra) : '');
$el.select2('close');
CRM.loadForm(formUrl, {
- dialog: {width: 500, height: 220}
+ dialog: {width: '50%', height: 220}
}).on('crmFormSuccess', function(e, data) {
if (data.status === 'success' && data.id) {
- data.label = data.extra.sort_name;
- CRM.status(ts('%1 Created', {1: data.extra.display_name}));
+ if (!data.crmMessages) {
+ CRM.status(ts('%1 Created', {1: data.label || data.extra.display_name}));
+ }
+ data.label = data.label || data.extra.sort_name;
if ($el.select2('container').hasClass('select2-container-multi')) {
var selection = $el.select2('data');
selection.push(data);
});
return false;
})
+ .on('click.crmEntity', '.crm-entityref-links-static a', function(e) {
+ var id = $(this).attr('href').substr(1),
+ item = _.findWhere(staticItems, {id: id});
+ $el.select2('close');
+ if ($el.select2('container').hasClass('select2-container-multi')) {
+ var selection = $el.select2('data');
+ if (!_.findWhere(selection, {id: id})) {
+ selection.push(item);
+ $el.select2('data', selection, true);
+ }
+ } else {
+ $el.select2('data', item, true);
+ }
+ return false;
+ })
.on('change.crmEntity', '.crm-entityref-filter-value', function() {
var filter = $el.data('user-filter') || {};
filter.value = $(this).val();
$(this).toggleClass('active', !!filter.value);
$el.data('user-filter', filter);
- if (filter.value) {
+ if (filter.value && $(this).is('select')) {
// Once a filter has been chosen, rerender create links and refocus the search box
$el.select2('close');
$el.select2('open');
} else {
- $('.crm-entityref-links', '#select2-drop').replaceWith(renderEntityRefCreateLinks($el));
+ $('.crm-entityref-links-create', '#select2-drop').replaceWith(renderEntityRefCreateLinks($el));
}
})
.on('change.crmEntity', 'select.crm-entityref-filter-key', function() {
return combined;
}
- function copyAttributes($source, $target, attributes) {
+ CRM.utils.copyAttributes = function ($source, $target, attributes) {
_.each(attributes, function(name) {
if ($source.attr(name) !== undefined) {
$target.attr(name, $source.attr(name));
}
});
- }
-
- /**
- * @see http://wiki.civicrm.org/confluence/display/CRMDOC/crmDatepicker
- */
- $.fn.crmDatepicker = function(options) {
- return $(this).each(function() {
- if ($(this).is('.crm-form-date-wrapper .crm-hidden-date')) {
- // Already initialized - destroy
- $(this)
- .off('.crmDatepicker')
- .css('display', '')
- .removeClass('crm-hidden-date')
- .siblings().remove();
- $(this).unwrap();
- }
- if (options === 'destroy') {
- return;
- }
- var
- $dataField = $(this).wrap('<span class="crm-form-date-wrapper" />'),
- settings = _.cloneDeep(options || {}),
- $dateField = $(),
- $timeField = $(),
- $clearLink = $(),
- hasDatepicker = settings.date !== false && settings.date !== 'yy',
- type = hasDatepicker ? 'text' : 'number';
-
- if (settings.allowClear !== undefined ? settings.allowClear : !$dataField.is('.required, [required]')) {
- $clearLink = $('<a class="crm-hover-button crm-clear-link" title="'+ _.escape(ts('Clear')) +'"><i class="crm-i fa-times"></i></a>')
- .insertAfter($dataField);
- }
- if (settings.time !== false) {
- $timeField = $('<input>').insertAfter($dataField);
- copyAttributes($dataField, $timeField, ['class', 'disabled']);
- $timeField
- .addClass('crm-form-text crm-form-time')
- .attr('placeholder', $dataField.attr('time-placeholder') === undefined ? ts('Time') : $dataField.attr('time-placeholder'))
- .attr('aria-label', $dataField.attr('time-placeholder') === undefined ? ts('Time') : $dataField.attr('time-placeholder'))
- .change(updateDataField)
- .timeEntry({
- spinnerImage: '',
- show24Hours: settings.time === true || settings.time === undefined ? CRM.config.timeIs24Hr : settings.time == '24'
- });
- }
- if (settings.date !== false) {
- // Render "number" field for year-only format, calendar popup for all other formats
- $dateField = $('<input type="' + type + '">').insertAfter($dataField);
- copyAttributes($dataField, $dateField, ['placeholder', 'style', 'class', 'disabled', 'aria-label']);
- $dateField.addClass('crm-form-' + type);
- if (hasDatepicker) {
- settings.minDate = settings.minDate ? CRM.utils.makeDate(settings.minDate) : null;
- settings.maxDate = settings.maxDate ? CRM.utils.makeDate(settings.maxDate) : null;
- settings.dateFormat = typeof settings.date === 'string' ? settings.date : CRM.config.dateInputFormat;
- settings.changeMonth = _.includes(settings.dateFormat, 'm');
- settings.changeYear = _.includes(settings.dateFormat, 'y');
- if (!settings.yearRange && settings.minDate !== null && settings.maxDate !== null) {
- settings.yearRange = '' + CRM.utils.formatDate(settings.minDate, 'yy') + ':' + CRM.utils.formatDate(settings.maxDate, 'yy');
- }
- $dateField.addClass('crm-form-date').datepicker(settings);
- } else {
- $dateField.attr('min', settings.minDate ? CRM.utils.formatDate(settings.minDate, 'yy') : '1000');
- $dateField.attr('max', settings.maxDate ? CRM.utils.formatDate(settings.maxDate, 'yy') : '4000');
- }
- $dateField.change(updateDataField);
- }
- // Rudimentary validation. TODO: Roll into use of jQUery validate and ui.datepicker.validation
- function isValidDate() {
- // FIXME: parseDate doesn't work with incomplete date formats; skip validation if no month, day or year in format
- var lowerFormat = settings.dateFormat.toLowerCase();
- if (lowerFormat.indexOf('y') < 0 || lowerFormat.indexOf('m') < 0 || !dateHasDay()) {
- return true;
- }
- try {
- $.datepicker.parseDate(settings.dateFormat, $dateField.val());
- return true;
- } catch (e) {
- return false;
- }
- }
-
- /**
- * Does the date format contain the day.
- *
- * @returns {boolean}
- */
- function dateHasDay() {
- var lowerFormat = settings.dateFormat.toLowerCase();
- if (lowerFormat.indexOf('d') < 0) {
- return false;
- }
- return true;
- }
- function updateInputFields(e, context) {
- var val = $dataField.val(),
- time = null;
- if (context !== 'userInput' && context !== 'crmClear') {
- if (hasDatepicker) {
- $dateField.datepicker('setDate', _.includes(val, '-') ? $.datepicker.parseDate('yy-mm-dd', val) : null);
- } else if ($dateField.length) {
- $dateField.val(val.slice(0, 4));
- }
- if ($timeField.length) {
- if (val.length === 8) {
- time = val;
- } else if (val.length === 19) {
- time = val.split(' ')[1];
- }
- $timeField.timeEntry('setTime', time);
- }
- }
- $clearLink.css('visibility', val ? 'visible' : 'hidden');
- }
- function updateDataField(e, context) {
- // The crmClear event wipes all the field values anyway, so no need to respond
- if (context !== 'crmClear') {
- var val = '';
- if ($dateField.val()) {
- if (hasDatepicker && isValidDate() && dateHasDay()) {
- val = $.datepicker.formatDate('yy-mm-dd', $dateField.datepicker('getDate'));
- $dateField.removeClass('crm-error');
- } else if (!hasDatepicker) {
- val = $dateField.val() + '-01-01';
- }
- else if (!dateHasDay()) {
- // This would be a Year-month date (yyyy-mm)
- // it could be argued it should not use a datepicker....
- val = $dateField.val() + '-01';
- } else {
- $dateField.addClass('crm-error');
- }
- }
- if ($timeField.val()) {
- val += (val ? ' ' : '') + $timeField.timeEntry('getTime').toTimeString().substr(0, 8);
- }
- $dataField.val(val).trigger('change', ['userInput']);
- }
- }
- $dataField.hide().addClass('crm-hidden-date').on('change.crmDatepicker', updateInputFields);
- updateInputFields();
- });
};
CRM.utils.formatSelect2Result = function (row) {
'</div>' +
'<div class="crm-select2-row-description">';
$.each(row.description || [], function(k, text) {
- markup += '<p>' + _.escape(text) + '</p>';
+ markup += '<p>' + _.escape(text) + '</p> ';
});
markup += '</div></div></div>';
return markup;
var
createLinks = $el.data('create-links'),
params = getEntityRefApiParams($el).params,
- markup = '<div class="crm-entityref-links">';
- if (!createLinks || $el.data('api-entity').toLowerCase() !== 'contact') {
+ entity = $el.data('api-entity'),
+ markup = '<div class="crm-entityref-links crm-entityref-links-create">';
+ if (!createLinks || (createLinks === true && !CRM.config.entityRef.links[entity])) {
return '';
}
if (createLinks === true) {
- createLinks = params.contact_type ? _.where(CRM.config.entityRef.contactCreate, {type: params.contact_type}) : CRM.config.entityRef.contactCreate;
+ createLinks = params.contact_type ? _.where(CRM.config.entityRef.links[entity], {type: params.contact_type}) : CRM.config.entityRef.links[entity];
}
_.each(createLinks, function(link) {
- var icon;
- switch (link.type) {
- case 'Individual':
- icon = 'fa-user';
- break;
-
- case 'Organization':
- icon = 'fa-building';
- break;
-
- case 'Household':
- icon = 'fa-home';
- break;
- }
- markup += ' <a class="crm-add-entity crm-hover-button" href="' + link.url + '">';
- if (icon) {
- markup += '<i class="crm-i ' + icon + '"></i> ';
- }
- markup += _.escape(link.label) + '</a>';
+ markup += ' <a class="crm-add-entity crm-hover-button" href="' + link.url + '">' +
+ '<i class="crm-i ' + (link.icon || 'fa-plus-circle') + '" aria-hidden="true"></i> ' +
+ _.escape(link.label) + '</a>';
});
markup += '</div>';
return markup;
function getEntityRefFilters($el) {
var
- entity = $el.data('api-entity').toLowerCase(),
- filters = $.extend([], CRM.config.entityRef.filters[entity] || []),
+ entity = $el.data('api-entity'),
+ filters = CRM.config.entityRef.filters[entity] || [],
params = $.extend({params: {}}, $el.data('api-params') || {}).params,
result = [];
- $.each(filters, function() {
- var filter = $.extend({type: 'select', 'attributes': {}, entity: entity}, this);
- if (typeof params[filter.key] === 'undefined') {
+ _.each(filters, function(filter) {
+ _.defaults(filter, {type: 'select', 'attributes': {}, entity: entity});
+ if (!params[filter.key]) {
+ // Filter out options if params don't match its condition
+ if (filter.condition && !_.isMatch(params, _.pick(filter.condition, _.keys(params)))) {
+ return;
+ }
result.push(filter);
}
else if (filter.key == 'contact_type' && typeof params.contact_sub_type === 'undefined') {
- filter.options = _.remove(filter.options, function(option) {
- return option.key.indexOf(params.contact_type + '__') === 0;
- });
result.push(filter);
}
});
'<select class="crm-entityref-filter-key' + (filter.key ? ' active' : '') + '">' +
'<option value="">' + _.escape(ts('Refine search...')) + '</option>' +
CRM.utils.renderOptions(filters, filter.key) +
- '</select>' + entityRefFilterValueMarkup(filter, filterSpec) + '</div>';
+ '</select>' + entityRefFilterValueMarkup($el, filter, filterSpec) + '</div>';
return markup;
}
/**
* Provide markup for entity ref filter value field
*/
- function entityRefFilterValueMarkup(filter, filterSpec) {
+ function entityRefFilterValueMarkup($el, filter, filterSpec) {
var markup = '';
if (filterSpec) {
var attrs = '',
attrs += ' ' + attr + '="' + val + '"';
});
if (filterSpec.type === 'select') {
- markup = '<select' + attrs + '><option value="">' + _.escape(ts('- select -')) + '</option>';
+ var fieldName = _.last(filter.key.split('.')),
+ options = [{key: '', value: ts('- select -')}];
if (filterSpec.options) {
- markup += CRM.utils.renderOptions(filterSpec.options, filter.value);
+ options = options.concat(getEntityRefFilterOptions(fieldName, $el, filterSpec));
}
- markup += '</select>';
+ markup = '<select' + attrs + '>' + CRM.utils.renderOptions(options, filter.value) + '</select>';
} else {
markup = '<input' + attrs + '/>';
}
$valField = null;
if (filterSpec) {
$('.crm-entityref-filter-value', '#select2-drop').remove();
- $valField = $(entityRefFilterValueMarkup(filter, filterSpec));
+ $valField = $(entityRefFilterValueMarkup($el, filter, filterSpec));
$keyField.after($valField);
- if (filterSpec.type === 'select' && !filterSpec.options) {
+ if (filterSpec.type === 'select') {
loadEntityRefFilterOptions(filter, filterSpec, $valField, $el);
}
} else {
}
/**
- * Fetch options for a filter via ajax api
+ * Fetch options for a filter from cache or ajax api
*/
function loadEntityRefFilterOptions(filter, filterSpec, $valField, $el) {
- $valField.prop('disabled', true);
// Fieldname may be prefixed with joins - strip those out
var fieldName = _.last(filter.key.split('.'));
+ if (filterSpec.options) {
+ CRM.utils.setOptions($valField, getEntityRefFilterOptions(fieldName, $el, filterSpec), false, filter.value);
+ return;
+ }
+ $('.crm-entityref-filters select', '#select2-drop').prop('disabled', true);
CRM.api3(filterSpec.entity, 'getoptions', {field: fieldName, context: 'search', sequential: 1})
.done(function(result) {
- var entity = $el.data('api-entity').toLowerCase(),
- globalFilterSpec = _.find(CRM.config.entityRef.filters[entity], {key: filter.key}) || {};
+ var entity = $el.data('api-entity').toLowerCase();
// Store options globally so we don't have to look them up again
- globalFilterSpec.options = result.values;
- $valField.prop('disabled', false);
- CRM.utils.setOptions($valField, result.values);
- $valField.val(filter.value || '');
+ filterSpec.options = result.values;
+ $('.crm-entityref-filters select', '#select2-drop').prop('disabled', false);
+ CRM.utils.setOptions($valField, getEntityRefFilterOptions(fieldName, $el, filterSpec), false, filter.value);
});
}
+ function getEntityRefFilterOptions(fieldName, $el, filterSpec) {
+ var values = _.cloneDeep(filterSpec.options),
+ params = $.extend({params: {}}, $el.data('api-params') || {}).params;
+ if (fieldName === 'contact_type' && params.contact_type) {
+ values = _.remove(values, function(option) {
+ return option.key.indexOf(params.contact_type + '__') === 0;
+ });
+ }
+ return values;
+ }
+
//CRM-15598 - Override url validator method to allow relative url's (e.g. /index.htm)
$.validator.addMethod("url", function(value, element) {
if (/^\//.test(value)) {
*/
$.fn.crmValidate = function(params) {
return $(this).each(function () {
- var that = this,
- settings = $.extend({}, CRM.validate._defaults, CRM.validate.params);
- $(this).validate(settings);
+ var validator = $(this).validate();
+ var that = this;
+ validator.settings = $.extend({}, validator.settings, CRM.validate._defaults, CRM.validate.params);
+ // Call our custom validation handler.
+ $(validator.currentForm).on("invalid-form.validate", validator.settings.invalidHandler);
// Call any post-initialization callbacks
if (CRM.validate.functions && CRM.validate.functions.length) {
$.each(CRM.validate.functions, function(i, func) {
});
};
+ // Submit-once
+ var submitted = [],
+ submitButton;
+ function submitOnceForm(e) {
+ if (e.isDefaultPrevented()) {
+ return;
+ }
+ if (_.contains(submitted, e.target)) {
+ return false;
+ }
+ submitted.push(e.target);
+ // Spin submit button icon
+ if (submitButton && $(submitButton, e.target).length) {
+ // Dialog button
+ if ($(e.target).closest('.ui-dialog .crm-ajax-container')) {
+ var identifier = $(submitButton).attr('name') || $(submitButton).attr('href');
+ if (identifier) {
+ submitButton = $(e.target).closest('.ui-dialog').find('button[data-identifier="' + identifier + '"]')[0] || submitButton;
+ }
+ }
+ var $icon = $(submitButton).siblings('.crm-i').add('.crm-i, .ui-icon', submitButton);
+ $icon.data('origClass', $icon.attr('class')).removeClass().addClass('crm-i crm-submit-icon fa-spinner fa-pulse');
+ }
+ }
+
+ // If form fails validation, restore button icon and reset the submitted array
+ function submitFormInvalid(form) {
+ submitted = [];
+ $('.crm-i.crm-submit-icon').each(function() {
+ if ($(this).data('origClass')) {
+ $(this).removeClass().addClass($(this).data('origClass'));
+ }
+ });
+ }
+
// Initialize widgets
$(document)
.on('crmLoad', function(e) {
}
})
.find('input.select-row:checked').parents('tr').addClass('crm-row-selected');
+ $('.crm-sortable-list', e.target).sortable();
$('table.crm-sortable', e.target).DataTable();
$('table.crm-ajax-table', e.target).each(function() {
var
});
}
});
- if ($("input:radio[name=radio_ts]").size() == 1) {
+ if ($("input:radio[name=radio_ts]").length == 1) {
$("input:radio[name=radio_ts]").prop("checked", true);
}
$('.crm-select2:not(.select2-offscreen, .select2-container)', e.target).crmSelect2();
CRM.wysiwyg.create(this);
}
});
+ // Submit once handlers
+ $('form[data-submit-once]', e.target)
+ .submit(submitOnceForm)
+ .on('invalid-form', submitFormInvalid);
+ $('form[data-submit-once] button[type=submit]', e.target).click(function(e) {
+ submitButton = e.target;
+ });
})
.on('dialogopen', function(e) {
var $el = $(e.target);
+ $('body').addClass('ui-dialog-open');
// Modal dialogs should disable scrollbars
if ($el.dialog('option', 'modal')) {
$el.addClass('modal-dialog');
$(this).button('option', 'icons', {primary: 'fa-expand'});
} else {
var menuHeight = $('#civicrm-menu').outerHeight();
+ if ($('body').hasClass('crm-menubar-below-cms-menu')) {
+ menuHeight += $('#civicrm-menu').offset().top;
+ }
$el.data('origSize', {
position: {my: 'center', at: 'center center+' + (menuHeight / 2), of: window},
width: $el.dialog('option', 'width'),
if ($('.ui-dialog .modal-dialog:visible').not(e.target).length < 1) {
$('body').css({overflow: ''});
}
+ if ($('.ui-dialog-content:visible').not(e.target).length < 1) {
+ $('body').removeClass('ui-dialog-open');
+ }
})
.on('submit', function(e) {
// CRM-14353 - disable changes warn when submitting a form
};
$.fn.crmtooltip = function () {
+ var TOOLTIP_HIDE_DELAY = 300;
+
$(document)
.on('mouseover', 'a.crm-summary-link:not(.crm-processed)', function (e) {
$(this).addClass('crm-processed crm-tooltip-active');
.load(this.href);
}
})
- .on('mouseout', 'a.crm-summary-link', function () {
- $(this).removeClass('crm-processed crm-tooltip-active crm-tooltip-down');
+ .on('mouseleave', 'a.crm-summary-link', function () {
+ var tooltipLink = $(this);
+ setTimeout(function () {
+ if (tooltipLink.filter(':hover').length === 0) {
+ tooltipLink.removeClass('crm-processed crm-tooltip-active crm-tooltip-down');
+ }
+ }, TOOLTIP_HIDE_DELAY);
})
.on('click', 'a.crm-summary-link', false);
};
}
};
/**
- * @see https://wiki.civicrm.org/confluence/display/CRMDOC/Notification+Reference
+ * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
*/
CRM.status = function(options, deferred) {
// 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.
};
/**
- * @see https://wiki.civicrm.org/confluence/display/CRMDOC/Notification+Reference
+ * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
*/
CRM.alert = function (text, title, type, options) {
type = type || 'alert';
};
/**
- * @see https://wiki.civicrm.org/confluence/display/CRMDOC/Notification+Reference
+ * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
*/
CRM.confirm = function (options) {
var dialog, url, msg, buttons = [], settings = {
};
/**
- * @see https://wiki.civicrm.org/confluence/display/CRMDOC/Notification+Reference
+ * @see https://docs.civicrm.org/dev/en/latest/framework/ui/#notifications-and-confirmations
*/
$.fn.crmError = function (text, title, options) {
title = title || '';
var extra = {
expires: 0
- };
+ }, label;
if ($(this).length) {
if (title === '') {
- var label = $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
+ label = $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
if (label.length) {
label.addClass('crm-error');
var $label = label.clone();
setTimeout(function () {
ele.one('change', function () {
if (msg && msg.close) msg.close();
- ele.removeClass('error');
- label.removeClass('crm-error');
+ ele.removeClass('crm-error');
+ if (label) {
+ label.removeClass('crm-error');
+ }
});
}, 1000);
}
*/
var currencyTemplate;
CRM.formatMoney = function(value, onlyNumber, format) {
- var decimal, separator, sign, i, j, result;
+ var precision, decimal, separator, sign, i, j, result;
if (value === 'init' && format) {
currencyTemplate = format;
return;
}
format = format || currencyTemplate;
- result = /1(.?)234(.?)56/.exec(format);
- if (result === null) {
+ if ((result = /1(.?)234(.?)56/.exec(format)) !== null) { // If value is formatted to 2 decimals
+ precision = 2;
+ }
+ else if ((result = /1(.?)234(.?)6/.exec(format)) !== null) { // If value is formatted to 1 decimal
+ precision = 1;
+ }
+ else if ((result = /1(.?)235/.exec(format)) !== null) { // If value is formatted to zero decimals
+ precision = false;
+ }
+ else {
return 'Invalid format passed to CRM.formatMoney';
}
separator = result[1];
- decimal = result[2];
+ decimal = precision ? result[2] : false;
sign = (value < 0) ? '-' : '';
//extracting the absolute value of the integer part of the number and converting to string
i = parseInt(value = Math.abs(value).toFixed(2)) + '';
j = ((j = i.length) > 3) ? j % 3 : 0;
- result = sign + (j ? i.substr(0, j) + separator : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + separator) + (2 ? decimal + Math.abs(value - i).toFixed(2).slice(2) : '');
- if ( onlyNumber ) {
+ 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) : '');
+ if (onlyNumber) {
return result;
}
- return format.replace(/1.*234.*56/, result);
+ switch (precision) {
+ case 2:
+ return format.replace(/1.*234.*56/, result);
+ case 1:
+ return format.replace(/1.*234.*6/, result);
+ case false:
+ return format.replace(/1.*235/, result);
+ }
};
CRM.angRequires = function(name) {
// Determine if a user has a given permission.
// @see CRM_Core_Resources::addPermissions
CRM.checkPerm = function(perm) {
- return CRM.permissions[perm];
+ return CRM.permissions && CRM.permissions[perm];
};
// Round while preserving sigfigs
return Math.round(n / scale) * scale;
};
- // Create a js Date object from a unix timestamp or a yyyy-mm-dd string
+ /**
+ * Create a js Date object from a unix timestamp or a yyyy-mm-dd string
+ * @param input
+ * @returns {Date}
+ */
CRM.utils.makeDate = function(input) {
switch (typeof input) {
case 'object':
case 'string':
// convert iso format with or without dashes
- if (input.indexOf('-') > 0) {
- return $.datepicker.parseDate('yy-mm-dd', input.substr(0, 10));
+ input = input.replace(/[- :]/g, '');
+ var output = $.datepicker.parseDate('yymmdd', input.substr(0, 8));
+ if (input.length === 14) {
+ output.setHours(
+ parseInt(input.substr(8, 2), 10),
+ parseInt(input.substr(10, 2), 10),
+ parseInt(input.substr(12, 2), 10)
+ );
}
- return $.datepicker.parseDate('yymmdd', input.substr(0, 8));
+ return output;
case 'number':
// convert unix timestamp
throw 'Invalid input passed to CRM.utils.makeDate';
};
- // Format a date for output to the user
- // Input may be a js Date object, a unix timestamp or a yyyy-mm-dd string
- CRM.utils.formatDate = function(input, outputFormat) {
- return input ? $.datepicker.formatDate(outputFormat || CRM.config.dateInputFormat, CRM.utils.makeDate(input)) : '';
+ /**
+ * Format a date (and optionally time) for output to the user
+ *
+ * @param {string|int|Date} input
+ * Input may be a js Date object, a unix timestamp or a 'yyyy-mm-dd' string
+ * @param {string|null} dateFormat
+ * A string like 'yy-mm-dd' or null to use the system default
+ * @param {int|bool} timeFormat
+ * Leave empty to omit time from the output (default)
+ * Or pass 12, 24, or true to use the system default for 12/24hr format
+ * @returns {string}
+ */
+ CRM.utils.formatDate = function(input, dateFormat, timeFormat) {
+ if (!input) {
+ return '';
+ }
+ var date = CRM.utils.makeDate(input),
+ output = $.datepicker.formatDate(dateFormat || CRM.config.dateInputFormat, date);
+ if (timeFormat) {
+ var hour = date.getHours(),
+ min = date.getMinutes(),
+ suf = '';
+ if (timeFormat === 12 || (timeFormat === true && !CRM.config.timeIs24Hr)) {
+ suf = ' ' + (hour < 12 ? ts('AM') : ts('PM'));
+ if (hour === 0 || hour > 12) {
+ hour = Math.abs(hour - 12);
+ }
+ } else if (hour < 10) {
+ hour = '0' + hour;
+ }
+ output += ' ' + hour + ':' + (min < 10 ? '0' : '') + min + suf;
+ }
+ return output;
};
// Used to set appropriate text color for a given background
return (yiq >= 128) ? 'black' : 'white';
};
+ // CVE-2015-9251 - Prevent auto-execution of scripts when no explicit dataType was provided
+ $.ajaxPrefilter(function(s) {
+ if (s.crossDomain) {
+ s.contents.script = false;
+ }
+ });
+
+ // 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.
+ $.htmlPrefilter = function(html) {
+ // Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
+ // their XML equivalent: e.g., "<div />" to "<div></div>". This is
+ // problematic for several reasons, including that it's vulnerable to XSS
+ // attacks. However, since this was jQuery's behavior for many years, many
+ // Drupal modules and jQuery plugins may be relying on it. Therefore, we
+ // preserve that behavior, but for a limited set of tags only, that we believe
+ // to not be vulnerable. This is the set of HTML tags that satisfy all of the
+ // following conditions:
+ // - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
+ // appear in that list, then we don't want to mess with it here either.
+ // @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
+ // - A normal element (not a void, template, text, or foreign element).
+ // @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
+ // - An element that is still defined by the current HTML specification
+ // (not a deprecated element), because we do not want to rely on how
+ // browsers parse deprecated elements.
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
+ // - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
+ // designed for fragments, not entire documents.
+ // - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
+ // regular expression, it didn't match on colgroup, and we don't want to
+ // introduce a behavior change for that.
+ var selfClosingTagsToReplace = [
+ 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
+ 'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
+ 'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
+ 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
+ 'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
+ 'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
+ 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
+ 'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
+ 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
+ 'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
+ ];
+
+ // Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
+ // two expressions makes it easier to target <a/> without also targeting
+ // every tag that starts with "a".
+ var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')';
+ var whitespace = '[\\x20\\t\\r\\n\\f]';
+ var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi');
+ var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi');
+
+ // jQuery 3.5 also fixed a vulnerability for when </select> appears within
+ // an <option> or <optgroup>, but it did that in local code that we can't
+ // backport directly. Instead, we filter such cases out. To do so, we need to
+ // determine when jQuery would otherwise invoke the vulnerable code, which it
+ // uses this regular expression to determine. The regular expression changed
+ // for version 3.0.0 and changed again for 3.4.0.
+ // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
+ // @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
+ // @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
+ var rtagName = /<([\w:]+)/;
+
+ // The regular expression that jQuery uses to determine which self-closing
+ // tags to expand to open and close tags. This is vulnerable, because it
+ // matches all tag names except the few excluded ones. We only use this
+ // expression for determining vulnerability. The expression changed for
+ // version 3, but we only need to check for vulnerability in versions 1 and 2,
+ // so we use the expression from those versions.
+ // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
+ var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
+
+ // This is how jQuery determines the first tag in the HTML.
+ // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
+ var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase();
+
+ // It is not valid HTML for <option> or <optgroup> to have <select> as
+ // either a descendant or sibling, and attempts to inject one can cause
+ // XSS on jQuery versions before 3.5. Since this is invalid HTML and a
+ // possible XSS attack, reject the entire string.
+ // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
+ if ((tag === 'option' || tag === 'optgroup') && html.match(/<\/?select/i)) {
+ html = '';
+ }
+
+ // Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
+ // the tags in the `selfClosingTagsToReplace` list defined above.
+ // @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
+ // @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
+ html = html.replace(rxhtmlTagWithoutSpaceOrAttributes, "<$1></$1>");
+ html = html.replace(rxhtmlTagWithSpaceAndMaybeAttributes, "<$1$2></$1>");
+
+ return html;
+ };
+
})(jQuery, _);