* @see https://wiki.civicrm.org/confluence/display/CRMDOC/AJAX+Interface
* @see https://wiki.civicrm.org/confluence/display/CRMDOC/Ajax+Pages+and+Forms
*/
-(function($, CRM) {
+(function($, CRM, undefined) {
/**
- * Almost like {crmURL} but on the client side
- * eg: var url = CRM.url('civicrm/contact/view', {reset:1,cid:42});
- * or: $('a.my-link').crmURL();
+ * @param string path
+ * @param string|object query
+ * @param string mode - optionally specify "front" or "back"
*/
- var tplURL = '/civicrm/example?placeholder';
- var urlInitted = false;
- CRM.url = function (p, params) {
- if (p == "init") {
- tplURL = params;
- urlInitted = true;
- return;
+ var tplURL;
+ CRM.url = function (path, query, mode) {
+ if (typeof path === 'object') {
+ return tplURL = path;
+ }
+ if (!tplURL) {
+ CRM.console('error', 'Error: CRM.url called before initialization');
}
- if (!urlInitted) {
- console && console.log && console.log('Warning: CRM.url called before initialization');
+ if (!mode) {
+ mode = CRM.config && CRM.config.isFrontend ? 'front' : 'back';
}
- params = params || '';
- var frag = p.split ('?');
- var url = tplURL.replace("civicrm/example", frag[0]);
+ query = query || '';
+ var frag = path.split('?');
+ var url = tplURL[mode].replace("*path*", frag[0]);
- if (typeof(params) == 'string') {
- url = url.replace("placeholder", params);
+ if (!query) {
+ url = url.replace(/[?&]\*query\*/, '');
}
else {
- url = url.replace("placeholder", $.param(params));
+ url = url.replace("*query*", typeof query === 'string' ? query : $.param(query));
}
if (frag[1]) {
- url += (url.indexOf('?') === (url.length - 1) ? '' : '&') + frag[1];
- }
- // remove trailing "?"
- if (url.indexOf('?') === (url.length - 1)) {
- url = url.slice(0, (url.length - 1));
+ url += (url.indexOf('?') < 0 ? '?' : '&') + frag[1];
}
return url;
};
- // Backwards compatible with jQuery fn
+ // @deprecated
$.extend ({'crmURL':
function (p, params) {
- console && console.log && console.log('Calling crmURL from jQuery is deprecated. Please use CRM.url() instead.');
+ CRM.console('warn', 'Calling crmURL from jQuery is deprecated. Please use CRM.url() instead.');
return CRM.url(p, params);
}
});
* @deprecated
*/
$.fn.crmAPI = function(entity, action, params, options) {
- console && console.log && console.log('Calling crmAPI from jQuery is deprecated. Please use CRM.api() instead.');
+ CRM.console('warn', 'Calling crmAPI from jQuery is deprecated. Please use CRM.api3() instead.');
return CRM.api.call(this, entity, action, params, options);
};
this.element.trigger('crmAjaxFail', data);
CRM.alert(ts('Unable to reach the server. Please refresh this page in your browser and try again.'), ts('Network Error'), 'error');
},
+ _onError: function(data) {
+ this.element.attr('data-unsaved-changes', 'false').trigger('crmAjaxError', data);
+ if (this.options.crmForm && this.options.crmForm.autoClose && this.element.data('uiDialog')) {
+ this.element.dialog('close');
+ }
+ },
_formatUrl: function(url) {
// Strip hash
url = url.split('#')[0];
if (this._originalContent === null) {
this._originalContent = this.element.contents().detach();
}
- this.options.block && $('.blockOverlay', this.element).length < 1 && this.element.block();
+ this.options.block && this.element.block();
$.getJSON(url, function(data) {
- if (typeof(data) != 'object' || typeof(data.content) != 'string') {
+ that.options.block && that.element.unblock();
+ if (!$.isPlainObject(data)) {
that._onFailure(data);
return;
}
+ if (data.status === 'error') {
+ that._onError(data);
+ return;
+ }
data.url = url;
- that.element.trigger('crmBeforeLoad', data).html(data.content);
+ that.element.trigger('crmUnload').trigger('crmBeforeLoad', data);
+ that._beforeRemovingContent();
+ that.element.html(data.content);
that._handleOrderLinks();
that.element.trigger('crmLoad', data);
that.options.crmForm && that.element.trigger('crmFormLoad', data);
that._onFailure();
});
},
- _destroy: function() {
- this.element.removeClass('crm-ajax-container');
+ // Perform any cleanup needed before removing/replacing content
+ _beforeRemovingContent: function() {
+ var that = this;
+ if (window.tinyMCE && tinyMCE.editors) {
+ $.each(tinyMCE.editors, function(k) {
+ if ($.contains(that.element[0], this.getElement())) {
+ this.remove();
+ }
+ });
+ }
this.options.crmForm && $('form', this.element).ajaxFormUnbind();
+ },
+ _destroy: function() {
+ this.element.removeClass('crm-ajax-container').trigger('crmUnload');
+ this._beforeRemovingContent();
if (this._originalContent !== null) {
this.element.empty().append(this._originalContent);
}
if (typeof settings.dialog.height === 'string' && settings.dialog.height.indexOf('%') > 0) {
settings.dialog.height = parseInt($(window).height() * (parseFloat(settings.dialog.height)/100), 10);
}
- $('<div id="'+ settings.target.substring(1) +'"><div class="crm-loading-element">' + ts('Loading') + '...</div></div>').dialog(settings.dialog);
- $(settings.target).on('dialogclose', function() {
- if (!$(this).data('hasUnsavedChanges')) {
- $(this).crmSnippet('destroy').dialog('destroy').remove();
+ // Increase percent width on small screens
+ if (typeof settings.dialog.width === 'string' && settings.dialog.width.indexOf('%') > 0) {
+ var screenWidth = $(window).width(),
+ percentage = parseInt(settings.dialog.width.replace('%', ''), 10),
+ gap = 100-percentage;
+ if (screenWidth < 701) {
+ settings.dialog.width = '100%';
}
- });
- }
- if (settings.dialog && !settings.dialog.title) {
- $(settings.target).on('crmLoad', function(e, data) {
- if (e.target === $(settings.target)[0] && data && data.title) {
- $(this).dialog('option', 'title', data.title);
+ else if (screenWidth < 1400) {
+ settings.dialog.width = '' + parseInt(percentage+gap-((screenWidth - 700)/7*(gap)/100), 10) + '%';
}
- });
+ }
+ $('<div id="'+ settings.target.substring(1) +'"><div class="crm-loading-element">' + ts('Loading') + '...</div></div>').dialog(settings.dialog);
+ $(settings.target)
+ .on('dialogclose', function() {
+ if ($(this).attr('data-unsaved-changes') !== 'true') {
+ $(this).crmSnippet('destroy').dialog('destroy').remove();
+ }
+ })
+ .on('crmLoad', function(e, data) {
+ // Set title
+ if (e.target === $(settings.target)[0] && data && !settings.dialog.title && data.title) {
+ $(this).dialog('option', 'title', data.title);
+ }
+ // Adjust height to fit content (small delay to allow elements to render)
+ window.setTimeout(function() {
+ var currentHeight = $(settings.target).parent().outerHeight(),
+ padding = currentHeight - $(settings.target).height(),
+ newHeight = $(settings.target).prop('scrollHeight') + padding,
+ menuHeight = $('#civicrm-menu').outerHeight(),
+ maxHeight = $(window).height() - menuHeight;
+ newHeight = newHeight > maxHeight ? maxHeight : newHeight;
+ if (newHeight > (currentHeight + 15)) {
+ $(settings.target).dialog('option', {
+ position: {my: 'center', at: 'center center+' + (menuHeight / 2), of: window},
+ height: newHeight
+ });
+ }
+ }, 500);
+ });
}
$(settings.target).crmSnippet(settings).crmSnippet('refresh');
return $(settings.target);
};
CRM.loadForm = function(url, options) {
- var settings = {
+ var formErrors = [], settings = {
crmForm: {
ajaxForm: {},
autoClose: true,
refreshAction: ['next_new', 'submit_savenext', 'upload_new'],
cancelButton: '.cancel',
openInline: 'a.open-inline, a.button, a.action-item',
- onCancel: function(event) {},
- onError: function(data) {
- var $el = $(this);
- $el.html(data.content).trigger('crmLoad', data).trigger('crmFormLoad', data).trigger('crmFormError', data);
- if (typeof(data.errors) == 'object') {
- $.each(data.errors, function(formElement, msg) {
- $('[name="'+formElement+'"]', $el).crmError(msg);
- });
- }
- }
+ onCancel: function(event) {}
}
};
// Move options that belong to crmForm. Others will be passed through to crmSnippet
var widget = CRM.loadPage(url, settings).off('.crmForm');
+ // CRM-14353 - Warn of unsaved changes for all forms except those which have opted out
function cancelAction() {
- var dirty = CRM.utils.initialValueChanged(widget),
- title = widget.dialog('option', 'title');
- widget.data('hasUnsavedChanges', dirty).dialog('close');
+ var dirty = CRM.utils.initialValueChanged($('form:not([data-warn-changes=false])', widget));
+ widget.attr('data-unsaved-changes', dirty ? 'true' : 'false');
if (dirty) {
var id = widget.attr('id') + '-unsaved-alert',
- alert = CRM.alert('<p>' + ts('%1 has not been saved.', {1: title}) + '</p><p><a href="#" id="' + id + '">' + ts('Restore') + '</a></p>', ts('Unsaved Changes'), alert, {expires: 60000});
+ title = widget.dialog('option', 'title'),
+ alert = CRM.alert('<p>' + ts('%1 has not been saved.', {1: title}) + '</p><p><a href="#" id="' + id + '">' + ts('Restore') + '</a></p>', ts('Unsaved Changes'), 'alert unsaved-dialog', {expires: 60000});
$('#' + id).button({icons: {primary: 'ui-icon-arrowreturnthick-1-w'}}).click(function(e) {
- widget.dialog('open');
- alert.close();
+ widget.attr('data-unsaved-changes', 'false').dialog('open');
e.preventDefault();
});
}
}
- if (widget.data('uiDialog')) {
- // This is a bit harsh but we are removing jQuery UI's event handler from the close button and adding our own
- $('.ui-dialog-titlebar-close').first().off().click(cancelAction);
- }
+
+ widget.data('uiDialog') && widget.on('dialogbeforeclose', function(e) {
+ // CRM-14353 - Warn unsaved changes if user clicks close button or presses "esc"
+ if (e.originalEvent) {
+ cancelAction();
+ }
+ });
widget.on('crmFormLoad.crmForm', function(event, data) {
- var $el = $(this)
- .data('hasUnsavedChanges', false);
- var settings = $el.crmSnippet('option', 'crmForm');
+ var $el = $(this).attr('data-unsaved-changes', 'false'),
+ settings = $el.crmSnippet('option', 'crmForm');
settings.cancelButton && $(settings.cancelButton, this).click(function(e) {
e.preventDefault();
var returnVal = settings.onCancel.call($el, e);
$el.trigger('crmFormCancel', e);
if ($el.data('uiDialog') && settings.autoClose) {
cancelAction();
+ $el.dialog('close');
}
else if (!settings.autoClose) {
$el.crmSnippet('resetUrl').crmSnippet('refresh');
}
});
if (settings.validate) {
- $("form", this).validate(typeof(settings.validate) == 'object' ? settings.validate : CRM.validate.params);
+ $("form", this).crmValidate();
}
$("form:not('[data-no-ajax-submit=true]')", this).ajaxForm($.extend({
url: data.url.replace(/reset=1[&]?/, ''),
dataType: 'json',
success: function(response) {
- if (response.status !== 'form_error') {
- $el.crmSnippet('option', 'block') && $el.unblock();
+ if (response.content === undefined) {
$el.trigger('crmFormSuccess', response);
// Reset form for e.g. "save and new"
if (response.userContext && (response.status === 'redirect' || (settings.refreshAction && $.inArray(response.buttonName, settings.refreshAction) >= 0))) {
$el.data('civiCrmSnippet')._originalUrl = response.userContext;
$el.crmSnippet('resetUrl').crmSnippet('refresh');
}
- else if ($el.data('uiDialog') && settings.autoClose) {
+ // Close if we are on the original url or the action was "delete" (in which case returning to view may be inappropriate)
+ else if ($el.data('uiDialog') && (settings.autoClose || response.action === 8)) {
$el.dialog('close');
}
else if (settings.autoClose === false) {
}
}
else {
+ $el.crmSnippet('option', 'block') && $el.unblock();
response.url = data.url;
- settings.onError.call($el, response);
+ $el.html(response.content).trigger('crmLoad', response).trigger('crmFormLoad', response);
+ if (response.status === 'form_error') {
+ formErrors = [];
+ $el.trigger('crmFormError', response);
+ $.each(response.errors || [], function(formElement, msg) {
+ formErrors.push($('[name="'+formElement+'"]', $el).crmError(msg));
+ });
+ }
}
},
beforeSerialize: function(form, options) {
this.updateElement && this.updateElement();
});
}
+ if (window.tinyMCE && tinyMCE.editors) {
+ $.each(tinyMCE.editors, function() {
+ this.save();
+ });
+ }
},
beforeSubmit: function(submission) {
+ $.each(formErrors, function() {
+ this && this.close && this.close();
+ });
$el.crmSnippet('option', 'block') && $el.block();
$el.trigger('crmFormSubmit', submission);
}
return false;
});
}
+ // Show form buttons as part of the dialog
+ if ($el.data('uiDialog')) {
+ var buttonContainers = '.crm-submit-buttons, .action-link',
+ buttons = [],
+ added = [];
+ $(buttonContainers, $el).find('input.crm-form-submit, a.button').each(function() {
+ var $el = $(this),
+ label = $el.is('input') ? $el.attr('value') : $el.text(),
+ identifier = $el.attr('name') || $el.attr('href');
+ if (!identifier || identifier === '#' || $.inArray(identifier, added) < 0) {
+ var $icon = $el.find('.icon'),
+ button = {'data-identifier': identifier, text: label, click: function() {
+ $el.click();
+ }};
+ if ($icon.length) {
+ button.icons = {primary: $icon.attr('class')};
+ } else {
+ var action = $el.hasClass('cancel') ? 'close' : (identifier.substr(identifier.length-4) === '_new' ? 'plus' : 'check');
+ button.icons = {primary: 'ui-icon-' + action};
+ }
+ buttons.push(button);
+ added.push(identifier);
+ }
+ // display:none causes the form to not submit when pressing "enter"
+ $el.parents(buttonContainers).css({height: 0, padding: 0, margin: 0, overflow: 'hidden'});
+ });
+ $el.dialog('option', 'buttons', buttons);
+ }
+ // Allow a button to prevent ajax submit
+ $('input[data-no-ajax-submit=true]').click(function() {
+ $(this).closest('form').ajaxFormUnbind();
+ });
// For convenience, focus the first field
$('input[type=text], textarea, select', this).filter(':visible').first().not('.dateplugin').focus();
});
return widget;
};
/**
- * Handler for jQuery click event e.g. $('a').click(CRM.popup)
- * @returns {boolean}
+ * Handler for jQuery click event e.g. $('a').click(CRM.popup);
*/
CRM.popup = function(e) {
var $el = $(this).first(),
else if ($el.hasClass('medium-popup')) {
settings.dialog.width = settings.dialog.height = '50%';
}
- else if ($el.hasClass('huge-popup')) {
- settings.dialog.height = '90%';
- }
var dialog = popup(url, settings);
// Trigger events from the dialog on the original link element
$el.trigger('crmPopupOpen', [dialog]);
e.preventDefault();
};
/**
- * An event callback for CRM.popup or a standalone function to refresh the content around a popup link
- * @param e event|selector
+ * An event callback for CRM.popup or a standalone function to refresh the content around a given element
+ * @param e {event|selector}
*/
CRM.refreshParent = function(e) {
// Use e.target if input smells like an event, otherwise assume it's a jQuery selector
var $el = (e.stopPropagation && e.target) ? $(e.target) : $(e),
$table = $el.closest('.dataTable');
// Call native refresh method on ajax datatables
- if ($table && $.fn.DataTable.fnIsDataTable($table[0]) && $table.dataTable().fnSettings().sAjaxSource) {
+ if ($table.length && $.fn.DataTable.fnIsDataTable($table[0]) && $table.dataTable().fnSettings().sAjaxSource) {
// Refresh ALL datatables - needed for contact relationship tab
$.each($.fn.dataTable.fnTables(), function() {
$(this).dataTable().fnSettings().sAjaxSource && $(this).unblock().dataTable().fnDraw();
};
$(function($) {
- $('body').on('click', 'a.crm-popup', CRM.popup);
+ $('body')
+ .on('click', 'a.crm-popup', CRM.popup)
+ // Close unsaved dialog messages
+ .on('dialogopen', function(e) {
+ $('.alert.unsaved-dialog .ui-notify-cross', '#crm-notification-container').click();
+ })
+ // Destroy old unsaved dialog
+ .on('dialogcreate', function(e) {
+ $('.ui-dialog-content.crm-ajax-container:hidden[data-unsaved-changes=true]').crmSnippet('destroy').dialog('destroy').remove();
+ });
});
}(jQuery, CRM));