First commit
This commit is contained in:
commit
c6e2478c40
13918 changed files with 2303184 additions and 0 deletions
59
sites/all/modules/civicrm/js/jquery/jquery.crmAjaxTable.js
Normal file
59
sites/all/modules/civicrm/js/jquery/jquery.crmAjaxTable.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
// https://civicrm.org/licensing
|
||||
(function($, _) {
|
||||
"use strict";
|
||||
/* jshint validthis: true */
|
||||
|
||||
$.fn.crmAjaxTable = function() {
|
||||
|
||||
// Strip the ids from ajax urls to make pageLength storage more generic
|
||||
function simplifyUrl(ajax) {
|
||||
// Datatables ajax prop could be a url string or an object containing the url
|
||||
var url = typeof ajax === 'object' ? ajax.url : ajax;
|
||||
return typeof url === 'string' ? url.replace(/[&?]\w*id=\d+/g, '') : null;
|
||||
}
|
||||
|
||||
return $(this).each(function() {
|
||||
// Recall pageLength for this table
|
||||
var url = simplifyUrl($(this).data('ajax'));
|
||||
if (url && window.localStorage && localStorage['dataTablePageLength:' + url]) {
|
||||
$(this).data('pageLength', localStorage['dataTablePageLength:' + url]);
|
||||
}
|
||||
// Declare the defaults for DataTables
|
||||
var defaults = {
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"order": [],
|
||||
"dom": '<"crm-datatable-pager-top"lfp>rt<"crm-datatable-pager-bottom"ip>',
|
||||
"pageLength": 25,
|
||||
"pagingType": "full_numbers",
|
||||
"drawCallback": function(settings) {
|
||||
//Add data attributes to cells
|
||||
$('thead th', settings.nTable).each( function( index ) {
|
||||
$.each(this.attributes, function() {
|
||||
if(this.name.match("^cell-")) {
|
||||
var cellAttr = this.name.substring(5);
|
||||
var cellValue = this.value;
|
||||
$('tbody tr', settings.nTable).each( function() {
|
||||
$('td:eq('+ index +')', this).attr( cellAttr, cellValue );
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
//Reload table after draw
|
||||
$(settings.nTable).trigger('crmLoad');
|
||||
}
|
||||
};
|
||||
//Include any table specific data
|
||||
var settings = $.extend(true, defaults, $(this).data('table'));
|
||||
// Remember pageLength
|
||||
$(this).on('length.dt', function(e, settings, len) {
|
||||
if (settings.ajax && window.localStorage) {
|
||||
localStorage['dataTablePageLength:' + simplifyUrl(settings.ajax)] = len;
|
||||
}
|
||||
});
|
||||
//Make the DataTables call
|
||||
$(this).DataTable(settings);
|
||||
});
|
||||
};
|
||||
|
||||
})(CRM.$, CRM._);
|
244
sites/all/modules/civicrm/js/jquery/jquery.crmEditable.js
Normal file
244
sites/all/modules/civicrm/js/jquery/jquery.crmEditable.js
Normal file
|
@ -0,0 +1,244 @@
|
|||
// https://civicrm.org/licensing
|
||||
(function($, _) {
|
||||
"use strict";
|
||||
/* jshint validthis: true */
|
||||
|
||||
// TODO: We'll need a way to clear this cache if options are edited.
|
||||
// Maybe it should be stored in the CRM object so other parts of the app can use it.
|
||||
// Note that if we do move it, we should also change the format of option lists to our standard sequential arrays
|
||||
var optionsCache = {};
|
||||
|
||||
/**
|
||||
* Helper fn to retrieve semantic data from markup
|
||||
*/
|
||||
$.fn.crmEditableEntity = function() {
|
||||
var
|
||||
el = this[0],
|
||||
ret = {},
|
||||
$row = this.first().closest('.crm-entity');
|
||||
ret.entity = $row.data('entity') || $row[0].id.split('-')[0];
|
||||
ret.id = $row.data('id') || $row[0].id.split('-')[1];
|
||||
ret.action = $row.data('action') || 'create';
|
||||
|
||||
if (!ret.entity || !ret.id) {
|
||||
return false;
|
||||
}
|
||||
$('.crm-editable, [data-field]', $row).each(function() {
|
||||
var fieldName = $(this).data('field') || this.className.match(/crmf-(\S*)/)[1];
|
||||
if (fieldName) {
|
||||
ret[fieldName] = $(this).text();
|
||||
if (this === el) {
|
||||
ret.field = fieldName;
|
||||
}
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* @see http://wiki.civicrm.org/confluence/display/CRMDOC/Structure+convention+for+automagic+edit+in+place
|
||||
*/
|
||||
$.fn.crmEditable = function(options) {
|
||||
function checkable() {
|
||||
$(this).off('.crmEditable').on('change.crmEditable', function() {
|
||||
var $el = $(this),
|
||||
info = $el.crmEditableEntity();
|
||||
if (!info.field) {
|
||||
return false;
|
||||
}
|
||||
var params = {
|
||||
sequential: 1,
|
||||
id: info.id,
|
||||
field: info.field,
|
||||
value: $el.is(':checked') ? 1 : 0
|
||||
};
|
||||
CRM.api3(info.entity, info.action, params, true);
|
||||
});
|
||||
}
|
||||
|
||||
return this.each(function() {
|
||||
var $i,
|
||||
fieldName = "",
|
||||
defaults = {
|
||||
error: function(entity, field, value, data) {
|
||||
restoreContainer();
|
||||
$(this).html(originalValue || settings.placeholder).click();
|
||||
var msg = $.isPlainObject(data) && data.error_message;
|
||||
errorMsg = $(':input', this).first().crmError(msg || ts('Sorry an error occurred and your information was not saved'), ts('Error'));
|
||||
},
|
||||
success: function(entity, field, value, data, settings) {
|
||||
restoreContainer();
|
||||
if ($i.data('refresh')) {
|
||||
CRM.refreshParent($i);
|
||||
} else {
|
||||
value = value === '' ? settings.placeholder : _.escape(value);
|
||||
$i.html(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
originalValue = '',
|
||||
errorMsg,
|
||||
editableSettings = $.extend({}, defaults, options);
|
||||
|
||||
if ($(this).hasClass('crm-editable-enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.nodeName == "INPUT" && this.type == "checkbox") {
|
||||
checkable.call(this, this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Table cell needs something inside it to look right
|
||||
if ($(this).is('td')) {
|
||||
$(this)
|
||||
.removeClass('crm-editable')
|
||||
.wrapInner('<div class="crm-editable" />');
|
||||
$i = $('div.crm-editable', this)
|
||||
.data($(this).data());
|
||||
var field = this.className.match(/crmf-(\S*)/);
|
||||
if (field) {
|
||||
$i.data('field', field[1]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$i = $(this);
|
||||
}
|
||||
|
||||
var settings = {
|
||||
tooltip: $i.data('tooltip') || ts('Click to edit'),
|
||||
placeholder: $i.data('placeholder') || '<i class="crm-i fa-pencil crm-editable-placeholder"></i>',
|
||||
onblur: 'cancel',
|
||||
cancel: '<button type="cancel"><i class="crm-i fa-times"></i></button>',
|
||||
submit: '<button type="submit"><i class="crm-i fa-check"></i></button>',
|
||||
cssclass: 'crm-editable-form',
|
||||
data: getData,
|
||||
onreset: restoreContainer
|
||||
};
|
||||
if ($i.data('type')) {
|
||||
settings.type = $i.data('type');
|
||||
if (settings.type == 'boolean') {
|
||||
settings.type = 'select';
|
||||
$i.data('options', {'0': ts('No'), '1': ts('Yes')});
|
||||
}
|
||||
}
|
||||
if (settings.type == 'textarea') {
|
||||
$i.addClass('crm-editable-textarea-enabled');
|
||||
}
|
||||
$i.addClass('crm-editable-enabled');
|
||||
|
||||
function callback(value, settings) {
|
||||
$i.addClass('crm-editable-saving');
|
||||
var
|
||||
info = $i.crmEditableEntity(),
|
||||
$el = $($i),
|
||||
params = {},
|
||||
action = $i.data('action') || info.action;
|
||||
if (!info.field) {
|
||||
return false;
|
||||
}
|
||||
if (info.id && info.id !== 'new') {
|
||||
params.id = info.id;
|
||||
}
|
||||
if (action === 'setvalue') {
|
||||
params.field = info.field;
|
||||
params.value = value;
|
||||
}
|
||||
else {
|
||||
params[info.field] = value;
|
||||
}
|
||||
CRM.api3(info.entity, action, params, {error: null})
|
||||
.done(function(data) {
|
||||
if (data.is_error) {
|
||||
return editableSettings.error.call($el[0], info.entity, info.field, value, data);
|
||||
}
|
||||
if ($el.data('options')) {
|
||||
value = $el.data('options')[value] || '';
|
||||
}
|
||||
else if ($el.data('optionsHashKey')) {
|
||||
var options = optionsCache[$el.data('optionsHashKey')];
|
||||
value = options && options[value] ? options[value] : '';
|
||||
}
|
||||
$el.trigger('crmFormSuccess', [value]);
|
||||
editableSettings.success.call($el[0], info.entity, info.field, value, data, settings);
|
||||
})
|
||||
.fail(function(data) {
|
||||
editableSettings.error.call($el[0], info.entity, info.field, value, data);
|
||||
});
|
||||
}
|
||||
|
||||
CRM.loadScript(CRM.config.resourceBase + 'packages/jquery/plugins/jquery.jeditable.min.js').done(function() {
|
||||
$i.editable(callback, settings);
|
||||
});
|
||||
|
||||
// CRM-15759 - Workaround broken textarea handling in jeditable 1.7.1
|
||||
$i.click(function() {
|
||||
$('textarea', this).off()
|
||||
// Fix cancel-on-blur
|
||||
.on('blur', function(e) {
|
||||
if (!e.relatedTarget || !$(e.relatedTarget).is('.crm-editable-form button')) {
|
||||
$i.find('button[type=cancel]').click();
|
||||
}
|
||||
})
|
||||
// Add support for ctrl-enter shortcut key
|
||||
.on('keydown', function (e) {
|
||||
if (e.ctrlKey && e.keyCode == 13) {
|
||||
$i.find('button[type=submit]').click();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getData(value, settings) {
|
||||
// Add css class to wrapper
|
||||
// FIXME: This should be a response to an event instead of coupled with this function but jeditable 1.7.1 doesn't trigger any events :(
|
||||
$i.addClass('crm-editable-editing');
|
||||
|
||||
originalValue = value;
|
||||
|
||||
if ($i.data('type') == 'select' || $i.data('type') == 'boolean') {
|
||||
if ($i.data('options')) {
|
||||
return formatOptions($i.data('options'));
|
||||
}
|
||||
var result,
|
||||
info = $i.crmEditableEntity(),
|
||||
// Strip extra id from multivalued custom fields
|
||||
custom = info.field.match(/(custom_\d+)_\d+/),
|
||||
field = custom ? custom[1] : info.field,
|
||||
hash = info.entity + '.' + field,
|
||||
params = {
|
||||
field: field,
|
||||
context: 'create'
|
||||
};
|
||||
$i.data('optionsHashKey', hash);
|
||||
if (!optionsCache[hash]) {
|
||||
$.ajax({
|
||||
url: CRM.url('civicrm/ajax/rest'),
|
||||
data: {entity: info.entity, action: 'getoptions', json: JSON.stringify(params)},
|
||||
async: false, // jeditable lacks support for async options lookup
|
||||
success: function(data) {optionsCache[hash] = data.values;}
|
||||
});
|
||||
}
|
||||
return formatOptions(optionsCache[hash]);
|
||||
}
|
||||
// Unwrap contents then replace html special characters with plain text
|
||||
return _.unescape(value.replace(/<(?:.|\n)*?>/gm, ''));
|
||||
}
|
||||
|
||||
function formatOptions(options) {
|
||||
if (typeof $i.data('emptyOption') === 'string') {
|
||||
// Using 'null' because '' is broken in jeditable 1.7.1
|
||||
return $.extend({'null': $i.data('emptyOption')}, options);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function restoreContainer() {
|
||||
if (errorMsg && errorMsg.close) errorMsg.close();
|
||||
$i.removeClass('crm-editable-saving crm-editable-editing');
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
})(jQuery, CRM._);
|
120
sites/all/modules/civicrm/js/jquery/jquery.crmIconPicker.js
Normal file
120
sites/all/modules/civicrm/js/jquery/jquery.crmIconPicker.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
// https://civicrm.org/licensing
|
||||
(function($, _) {
|
||||
"use strict";
|
||||
/* jshint validthis: true */
|
||||
var icons = [], loaded;
|
||||
|
||||
$.fn.crmIconPicker = function() {
|
||||
|
||||
function loadIcons() {
|
||||
if (!loaded) {
|
||||
loaded = $.Deferred();
|
||||
CRM.$.get(CRM.config.resourceBase + 'bower_components/font-awesome/css/font-awesome.css').done(function(data) {
|
||||
var match,
|
||||
regex = /\.(fa-[-a-zA-Z0-9]+):before {/g;
|
||||
while((match = regex.exec(data)) !== null) {
|
||||
icons.push(match[1]);
|
||||
}
|
||||
loaded.resolve();
|
||||
});
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
return this.each(function() {
|
||||
if ($(this).hasClass('iconpicker-widget')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $input = $(this),
|
||||
$button = $('<a class="crm-icon-picker-button" href="#" />').button().removeClass('ui-corner-all').attr('title', $input.attr('title')),
|
||||
$style = $('<select class="crm-form-select"></select>'),
|
||||
options = [
|
||||
{key: 'fa-rotate-90', value: ts('Rotate right')},
|
||||
{key: 'fa-rotate-270', value: ts('Rotate left')},
|
||||
{key: 'fa-rotate-180', value: ts('Rotate 180')},
|
||||
{key: 'fa-flip-horizontal', value: ts('Flip horizontal')},
|
||||
{key: 'fa-flip-vertical', value: ts('Flip vertical')}
|
||||
];
|
||||
|
||||
function formatButton() {
|
||||
var val = $input.val().replace('fa ', '');
|
||||
val = val.replace('crm-i ', '');
|
||||
var split = val.split(' ');
|
||||
$button.button('option', {
|
||||
label: split[0] || ts('None'),
|
||||
icons: {primary: val ? val : 'fa-'}
|
||||
});
|
||||
$style.toggle(!!split[0]).val(split[1] || '');
|
||||
}
|
||||
|
||||
$input.hide().addClass('iconpicker-widget').after($style).after(' ').after($button).change(formatButton);
|
||||
|
||||
CRM.utils.setOptions($style, options, ts('Normal'));
|
||||
|
||||
formatButton();
|
||||
|
||||
$style.change(function() {
|
||||
if ($input.val()) {
|
||||
var split = $input.val().split(' '),
|
||||
style = $style.val();
|
||||
$input.val(split[0] + (style ? ' ' + style : '')).change();
|
||||
}
|
||||
});
|
||||
|
||||
$button.click(function(e) {
|
||||
var dialog;
|
||||
|
||||
function displayIcons() {
|
||||
var term = $('input[name=search]', dialog).val().replace(/-/g, '').toLowerCase(),
|
||||
$place = $('div.icons', dialog).html('');
|
||||
$.each(icons, function(i, icon) {
|
||||
if (!term.length || icon.replace(/-/g, '').indexOf(term) > -1) {
|
||||
var item = $('<a href="#" title="' + icon + '"/>').button({
|
||||
icons: {primary: icon}
|
||||
});
|
||||
$place.append(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function displayDialog() {
|
||||
dialog.append('<style type="text/css">' +
|
||||
'#crmIconPicker {font-size: 2em;}' +
|
||||
'#crmIconPicker .icon-search input {font-family: FontAwesome; padding-left: .5em; margin-bottom: 1em;}' +
|
||||
'#crmIconPicker a.ui-button {width: 2em; height: 2em; color: #222;}' +
|
||||
'#crmIconPicker a.ui-button .ui-icon {margin-top: -0.5em; width: auto; height: auto;}' +
|
||||
'</style>' +
|
||||
'<div class="icon-search"><input class="crm-form-text" name="search" placeholder=""/></div>' +
|
||||
'<div class="icons"></div>'
|
||||
);
|
||||
displayIcons();
|
||||
dialog.unblock();
|
||||
}
|
||||
|
||||
function pickIcon(e) {
|
||||
var newIcon = $(this).attr('title'),
|
||||
style = $style.val();
|
||||
$input.val(newIcon + (style ? ' ' + style : '')).change();
|
||||
dialog.dialog('close');
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
dialog = $('<div id="crmIconPicker"/>').dialog({
|
||||
title: $input.attr('title'),
|
||||
width: '80%',
|
||||
height: 400,
|
||||
modal: true
|
||||
}).block()
|
||||
.on('click', 'a', pickIcon)
|
||||
.on('keyup', 'input[name=search]', displayIcons)
|
||||
.on('dialogclose', function() {
|
||||
$(this).dialog('destroy').remove();
|
||||
});
|
||||
loadIcons().done(displayDialog);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
}(CRM.$, CRM._));
|
|
@ -0,0 +1,99 @@
|
|||
(function($, _) {
|
||||
var ufGroupCollection = new CRM.UF.UFGroupCollection(_.sortBy(CRM.initialProfileList.values, 'title'));
|
||||
//var ufGroupCollection = new CRM.UF.UFGroupCollection(CRM.initialProfileList.values, {
|
||||
// comparator: 'title' // no point, this doesn't work with subcollections
|
||||
//});
|
||||
ufGroupCollection.unshift(new CRM.UF.UFGroupModel({
|
||||
id: '',
|
||||
title: ts('- select -')
|
||||
}));
|
||||
|
||||
/**
|
||||
* Example:
|
||||
* <input type="text" value="{$profileId}" class="crm-profile-selector" />
|
||||
* ...
|
||||
* cj('.crm-profile-selector').crmProfileSelector({
|
||||
* groupTypeFilter: "Contact,Individual,Activity;;ActivityType:7",
|
||||
* entities: "contact_1:IndividualModel,activity_1:ActivityModel"
|
||||
* });
|
||||
*
|
||||
* Note: The system does not currently support dynamic entities -- it only supports
|
||||
* a couple of entities named "contact_1" and "activity_1". See also
|
||||
* CRM.UF.guessEntityName().
|
||||
*/
|
||||
$.fn.crmProfileSelector = function(options) {
|
||||
return this.each(function() {
|
||||
// Hide the existing <SELECT> and instead construct a ProfileSelector view.
|
||||
// Keep them synchronized.
|
||||
var matchingUfGroups,
|
||||
$select = $(this).hide().addClass('rendered');
|
||||
|
||||
var validTypesId = [];
|
||||
var usedByFilter = null;
|
||||
if (options.groupTypeFilter) {
|
||||
matchingUfGroups = ufGroupCollection.subcollection({
|
||||
filter: function(ufGroupModel) {
|
||||
//CRM-16915 - filter with module used by the profile
|
||||
if (options.usedByFilter && options.usedByFilter.length) {
|
||||
usedByFilter = options.usedByFilter;
|
||||
}
|
||||
return ufGroupModel.checkGroupType(options.groupTypeFilter, options.allowAllSubtypes, usedByFilter);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
matchingUfGroups = ufGroupCollection;
|
||||
}
|
||||
|
||||
//CRM-15427 check for valid subtypes raise a warning if not valid
|
||||
if (options.allowAllSubtypes && !validTypesId.length) {
|
||||
validTypes = ufGroupCollection.subcollection({
|
||||
filter: function(ufGroupModel) {
|
||||
return ufGroupModel.checkGroupType(options.groupTypeFilter);
|
||||
}
|
||||
});
|
||||
_.each(validTypes.models, function(validTypesattr) {
|
||||
validTypesId.push(validTypesattr.id);
|
||||
});
|
||||
}
|
||||
if (validTypesId.length && $.inArray($select.val(), validTypesId) == -1) {
|
||||
var civiComponent;
|
||||
if (options.groupTypeFilter.indexOf('Membership') !== -1) {
|
||||
civiComponent = 'Membership';
|
||||
}
|
||||
else if (options.groupTypeFilter.indexOf('Participant') !== -1) {
|
||||
civiComponent = 'Event';
|
||||
}
|
||||
else {
|
||||
civiComponent = 'Contribution';
|
||||
}
|
||||
CRM.alert(ts('The selected profile is using a custom field which is not assigned to the "%1" being configured.', {1: civiComponent}), ts('Warning'));
|
||||
}
|
||||
var view = new CRM.ProfileSelector.View({
|
||||
ufGroupId: $select.val(),
|
||||
ufGroupCollection: matchingUfGroups,
|
||||
ufEntities: options.entities
|
||||
});
|
||||
view.on('change:ufGroupId', function() {
|
||||
$select.val(view.getUfGroupId()).change();
|
||||
});
|
||||
view.render();
|
||||
$select.after(view.el);
|
||||
setTimeout(function() {
|
||||
view.doPreview();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
$('#crm-container').on('crmLoad', function() {
|
||||
$('.crm-profile-selector:not(.rendered)', this).each(function() {
|
||||
$(this).crmProfileSelector({
|
||||
groupTypeFilter: $(this).data('groupType'),
|
||||
entities: $(this).data('entities'),
|
||||
//CRM-15427
|
||||
allowAllSubtypes: $(this).data('default'),
|
||||
usedByFilter: $(this).data('usedfor')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
})(CRM.$, CRM._);
|
|
@ -0,0 +1,50 @@
|
|||
(function($, CRM) {
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
*
|
||||
* cj('.my-link').crmRevisionLink({
|
||||
* 'reportId': 123, // CRM_Report_Utils_Report::getInstanceIDForValue('logging/contact/summary'),
|
||||
* 'tableName': 'my_table',
|
||||
* 'contactId': 123
|
||||
* ));
|
||||
*
|
||||
* Note: This file is used by CivHR
|
||||
*/
|
||||
$.fn.crmRevisionLink = function(options) {
|
||||
return this.each(function(){
|
||||
var $dialog = $('<div><div class="revision-content"></div></div>');
|
||||
$('body').append($dialog);
|
||||
$(this).on("click", function() {
|
||||
$dialog.show();
|
||||
$dialog.dialog({
|
||||
title: ts("Revisions"),
|
||||
modal: true,
|
||||
width: "680px",
|
||||
bgiframe: true,
|
||||
overlay: { opacity: 0.5, background: "black" },
|
||||
open:function() {
|
||||
var ajaxurl = CRM.url("civicrm/report/instance/" + options.reportId);
|
||||
cj.ajax({
|
||||
data: "reset=1&snippet=4§ion=2&altered_contact_id_op=eq&altered_contact_id_value="+options.contactId+"&log_type_table_op=has&log_type_table_value=" + options.tableName,
|
||||
url: ajaxurl,
|
||||
success: function (data) {
|
||||
$dialog.find(".revision-content").html(data);
|
||||
if (!$dialog.find(".revision-content .report-layout").length) {
|
||||
$dialog.find(".revision-content").html("Sorry, couldn't find any revisions.");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
buttons: {
|
||||
"Done": function() {
|
||||
$(this).dialog("destroy");
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}); // this.each
|
||||
}; // fn.crmRevisionLink
|
||||
|
||||
})(jQuery, CRM);
|
580
sites/all/modules/civicrm/js/jquery/jquery.dashboard.js
Normal file
580
sites/all/modules/civicrm/js/jquery/jquery.dashboard.js
Normal file
|
@ -0,0 +1,580 @@
|
|||
// https://civicrm.org/licensing
|
||||
/* global CRM, ts */
|
||||
/*jshint loopfunc: true */
|
||||
(function($) {
|
||||
'use strict';
|
||||
// Constructor for dashboard object.
|
||||
$.fn.dashboard = function(options) {
|
||||
// Public properties of dashboard.
|
||||
var dashboard = {};
|
||||
dashboard.element = this.empty();
|
||||
dashboard.ready = false;
|
||||
dashboard.columns = [];
|
||||
dashboard.widgets = {};
|
||||
|
||||
/**
|
||||
* Public methods of dashboard.
|
||||
*/
|
||||
|
||||
// Saves the order of widgets for all columns including the widget.minimized status to options.ajaxCallbacks.saveColumns.
|
||||
dashboard.saveColumns = function(showStatus) {
|
||||
// Update the display status of the empty placeholders.
|
||||
$.each(dashboard.columns, function(c, col) {
|
||||
if ( typeof col == 'object' ) {
|
||||
// Are there any visible children of the column (excluding the empty placeholder)?
|
||||
if (col.element.children(':visible').not(col.emptyPlaceholder).length > 0) {
|
||||
col.emptyPlaceholder.hide();
|
||||
}
|
||||
else {
|
||||
col.emptyPlaceholder.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Don't save any changes to the server unless the dashboard has finished initiating.
|
||||
if (!dashboard.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a list of params to post to the server.
|
||||
var params = {};
|
||||
|
||||
// For each column...
|
||||
$.each(dashboard.columns, function(c, col) {
|
||||
|
||||
// IDs of the sortable elements in this column.
|
||||
var ids = (typeof col == 'object') ? col.element.sortable('toArray') : [];
|
||||
|
||||
// For each id...
|
||||
$.each(ids, function(w, id) {
|
||||
if (typeof id == 'string') {
|
||||
// Chop 'widget-' off of the front so that we have the real widget id.
|
||||
id = id.substring('widget-'.length);
|
||||
// Add one flat property to the params object that will look like an array element to the PHP server.
|
||||
// Unfortunately jQuery doesn't do this for us.
|
||||
if (typeof dashboard.widgets[id] == 'object') params['columns[' + c + '][' + id + ']'] = (dashboard.widgets[id].minimized ? '1' : '0');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// The ajaxCallback settings overwrite any duplicate properties.
|
||||
$.extend(params, opts.ajaxCallbacks.saveColumns.data);
|
||||
var post = $.post(opts.ajaxCallbacks.saveColumns.url, params, function() {
|
||||
invokeCallback(opts.callbacks.saveColumns, dashboard);
|
||||
});
|
||||
if (showStatus !== false) {
|
||||
CRM.status({}, post);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Private properties of dashboard.
|
||||
*/
|
||||
|
||||
// Used to determine whether two resort events are resulting from the same UI event.
|
||||
var currentReSortEvent = null;
|
||||
|
||||
// Merge in the caller's options with the defaults.
|
||||
var opts = $.extend({}, $.fn.dashboard.defaults, options);
|
||||
|
||||
var localCache = window.localStorage && localStorage.dashboard ? JSON.parse(localStorage.dashboard) : {};
|
||||
|
||||
init(opts.widgetsByColumn);
|
||||
|
||||
return dashboard;
|
||||
|
||||
/**
|
||||
* Private methods of dashboard.
|
||||
*/
|
||||
|
||||
// Initialize widget columns.
|
||||
function init(widgets) {
|
||||
var markup = '<li class="empty-placeholder">' + opts.emptyPlaceholderInner + '</li>';
|
||||
|
||||
// Build the dashboard in the DOM. For each column...
|
||||
// (Don't iterate on widgets since this will break badly if the dataset has empty columns.)
|
||||
var emptyDashboard = true;
|
||||
for (var c = 0; c < opts.columns; c++) {
|
||||
// Save the column to both the public scope for external accessibility and the local scope for readability.
|
||||
var col = dashboard.columns[c] = {
|
||||
initialWidgets: [],
|
||||
element: $('<ul id="column-' + c + '" class="column column-' + c + '"></ul>').appendTo(dashboard.element)
|
||||
};
|
||||
|
||||
// Add the empty placeholder now, hide it and save it.
|
||||
col.emptyPlaceholder = $(markup).appendTo(col.element).hide();
|
||||
|
||||
// For each widget in this column.
|
||||
$.each(widgets[c], function(num, item) {
|
||||
var id = (num+1) + '-' + item.id;
|
||||
col.initialWidgets[id] = dashboard.widgets[item.id] = widget($.extend({
|
||||
element: $('<li class="widget"></li>').appendTo(col.element),
|
||||
initialColumn: col
|
||||
}, item));
|
||||
emptyDashboard = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (emptyDashboard) {
|
||||
emptyDashboardCondition();
|
||||
} else {
|
||||
completeInit();
|
||||
}
|
||||
|
||||
invokeCallback(opts.callbacks.init, dashboard);
|
||||
}
|
||||
|
||||
// function that is called when dashboard is empty
|
||||
function emptyDashboardCondition( ) {
|
||||
$(".show-refresh").hide( );
|
||||
$("#empty-message").show( );
|
||||
}
|
||||
|
||||
// Cache dashlet info in localStorage
|
||||
function saveLocalCache() {
|
||||
localCache = {};
|
||||
$.each(dashboard.widgets, function(id, widget) {
|
||||
localCache[id] = {
|
||||
content: widget.content,
|
||||
lastLoaded: widget.lastLoaded,
|
||||
minimized: widget.minimized
|
||||
};
|
||||
});
|
||||
if (window.localStorage) {
|
||||
localStorage.dashboard = JSON.stringify(localCache);
|
||||
}
|
||||
}
|
||||
|
||||
// Contructors for each widget call this when initialization has finished so that dashboard can complete it's intitialization.
|
||||
function completeInit() {
|
||||
// Only do this once.
|
||||
if (dashboard.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make widgets sortable across columns.
|
||||
dashboard.sortableElement = $('.column').sortable({
|
||||
connectWith: ['.column'],
|
||||
|
||||
// The class of the element by which widgets are draggable.
|
||||
handle: '.widget-header',
|
||||
|
||||
// The class of placeholder elements (the 'ghost' widget showing where the dragged item would land if released now.)
|
||||
placeholder: 'placeholder',
|
||||
activate: function(event, ui) {
|
||||
var h= $(ui.item).height();
|
||||
$('.placeholder').css('height', h +'px');
|
||||
},
|
||||
|
||||
opacity: 0.2,
|
||||
|
||||
// Maks sure that only widgets are sortable, and not empty placeholders.
|
||||
items: '> .widget',
|
||||
|
||||
forcePlaceholderSize: true,
|
||||
|
||||
// Callback functions.
|
||||
update: resorted,
|
||||
start: hideEmptyPlaceholders
|
||||
});
|
||||
|
||||
// Update empty placeholders.
|
||||
dashboard.saveColumns();
|
||||
dashboard.ready = true;
|
||||
invokeCallback(opts.callbacks.ready, dashboard);
|
||||
|
||||
// Auto-refresh widgets when content is stale
|
||||
window.setInterval(function() {
|
||||
if (!document.hasFocus || document.hasFocus()) {
|
||||
$.each(dashboard.widgets, function (i, widget) {
|
||||
if (!widget.cacheIsFresh()) {
|
||||
widget.reloadContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Callback for when any list has changed (and the user has finished resorting).
|
||||
function resorted(e, ui) {
|
||||
// Only do anything if we haven't already handled resorts based on changes from this UI DOM event.
|
||||
// (resorted() gets invoked once for each list when an item is moved from one to another.)
|
||||
if (!currentReSortEvent || e.originalEvent != currentReSortEvent) {
|
||||
currentReSortEvent = e.originalEvent;
|
||||
dashboard.saveColumns();
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for when a user starts resorting a list. Hides all the empty placeholders.
|
||||
function hideEmptyPlaceholders(e, ui) {
|
||||
for (var c in dashboard.columns) {
|
||||
if( (typeof dashboard.columns[c]) == 'object' ) dashboard.columns[c].emptyPlaceholder.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// @todo use an event library to register, bind to and invoke events.
|
||||
// @param callback is a function.
|
||||
// @param theThis is the context given to that function when it executes. It becomes 'this' inside of that function.
|
||||
function invokeCallback(callback, theThis, parameterOne) {
|
||||
if (callback) {
|
||||
callback.call(theThis, parameterOne);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* widget object
|
||||
* Private sub-class of dashboard
|
||||
* Constructor starts
|
||||
*/
|
||||
function widget(widget) {
|
||||
// Merge default options with the options defined for this widget.
|
||||
widget = $.extend({}, $.fn.dashboard.widget.defaults, localCache[widget.id] || {}, widget);
|
||||
|
||||
/**
|
||||
* Public methods of widget.
|
||||
*/
|
||||
|
||||
// Toggles the minimize() & maximize() methods.
|
||||
widget.toggleMinimize = function() {
|
||||
if (widget.minimized) {
|
||||
widget.maximize();
|
||||
}
|
||||
else {
|
||||
widget.minimize();
|
||||
}
|
||||
|
||||
widget.hideSettings();
|
||||
};
|
||||
widget.minimize = function() {
|
||||
$('.widget-content', widget.element).slideUp(opts.animationSpeed);
|
||||
$(widget.controls.minimize.element)
|
||||
.addClass('fa-caret-right')
|
||||
.removeClass('fa-caret-down')
|
||||
.attr('title', ts('Expand'));
|
||||
widget.minimized = true;
|
||||
saveLocalCache();
|
||||
};
|
||||
widget.maximize = function() {
|
||||
$(widget.controls.minimize.element)
|
||||
.removeClass( 'fa-caret-right' )
|
||||
.addClass( 'fa-caret-down' )
|
||||
.attr('title', ts('Collapse'));
|
||||
widget.minimized = false;
|
||||
saveLocalCache();
|
||||
if (!widget.contentLoaded) {
|
||||
loadContent();
|
||||
}
|
||||
$('.widget-content', widget.element).slideDown(opts.animationSpeed);
|
||||
};
|
||||
|
||||
// Toggles whether the widget is in settings-display mode or not.
|
||||
widget.toggleSettings = function() {
|
||||
if (widget.settings.displayed) {
|
||||
// Widgets always exit settings into maximized state.
|
||||
widget.maximize();
|
||||
widget.hideSettings();
|
||||
invokeCallback(opts.widgetCallbacks.hideSettings, widget);
|
||||
}
|
||||
else {
|
||||
widget.minimize();
|
||||
widget.showSettings();
|
||||
invokeCallback(opts.widgetCallbacks.showSettings, widget);
|
||||
}
|
||||
};
|
||||
widget.showSettings = function() {
|
||||
if (widget.settings.element) {
|
||||
widget.settings.element.show();
|
||||
|
||||
// Settings are loaded via AJAX. Only execute the script if the settings have been loaded.
|
||||
if (widget.settings.ready) {
|
||||
getJavascript(widget.settings.script);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Settings have not been initialized. Do so now.
|
||||
initSettings();
|
||||
}
|
||||
widget.settings.displayed = true;
|
||||
};
|
||||
widget.hideSettings = function() {
|
||||
if (widget.settings.element) {
|
||||
widget.settings.element.hide();
|
||||
}
|
||||
widget.settings.displayed = false;
|
||||
};
|
||||
widget.saveSettings = function() {
|
||||
// Build list of parameters to POST to server.
|
||||
var params = {};
|
||||
// serializeArray() returns an array of objects. Process it.
|
||||
var fields = widget.settings.element.serializeArray();
|
||||
$.each(fields, function(i, field) {
|
||||
// Put the values into flat object properties that PHP will parse into an array server-side.
|
||||
// (Unfortunately jQuery doesn't do this)
|
||||
params['settings[' + field.name + ']'] = field.value;
|
||||
});
|
||||
|
||||
// Things get messy here.
|
||||
// @todo Refactor to use currentState and targetedState properties to determine what needs
|
||||
// to be done to get to any desired state on any UI or AJAX event – since these don't always
|
||||
// match.
|
||||
// E.g. When a user starts a new UI event before the Ajax event handler from a previous
|
||||
// UI event gets invoked.
|
||||
|
||||
// Hide the settings first of all.
|
||||
widget.toggleSettings();
|
||||
// Save the real settings element so that we can restore the reference later.
|
||||
var settingsElement = widget.settings.element;
|
||||
// Empty the settings form.
|
||||
widget.settings.innerElement.empty();
|
||||
initThrobber();
|
||||
// So that showSettings() and hideSettings() can do SOMETHING, without showing the empty settings form.
|
||||
widget.settings.element = widget.throbber.hide();
|
||||
widget.settings.ready = false;
|
||||
|
||||
// Save the settings to the server.
|
||||
$.extend(params, opts.ajaxCallbacks.widgetSettings.data, { id: widget.id });
|
||||
$.post(opts.ajaxCallbacks.widgetSettings.url, params, function(response, status) {
|
||||
// Merge the response into widget.settings.
|
||||
$.extend(widget.settings, response);
|
||||
// Restore the reference to the real settings element.
|
||||
widget.settings.element = settingsElement;
|
||||
// Make sure the settings form is empty and add the updated settings form.
|
||||
widget.settings.innerElement.empty().append(widget.settings.markup);
|
||||
widget.settings.ready = true;
|
||||
|
||||
// Did the user already jump back into settings-display mode before we could finish reloading the settings form?
|
||||
if (widget.settings.displayed) {
|
||||
// Ooops! We had better take care of hiding the throbber and showing the settings form then.
|
||||
widget.throbber.hide();
|
||||
widget.showSettings();
|
||||
invokeCallback(opts.widgetCallbacks.saveSettings, dashboard);
|
||||
}
|
||||
}, 'json');
|
||||
|
||||
// Don't let form submittal bubble up.
|
||||
return false;
|
||||
};
|
||||
|
||||
widget.enterFullscreen = function() {
|
||||
// Make sure the widget actually supports full screen mode.
|
||||
if (widget.fullscreenUrl) {
|
||||
CRM.loadPage(widget.fullscreenUrl);
|
||||
}
|
||||
};
|
||||
|
||||
// Adds controls to a widget. id is for internal use and image file name in images/dashboard/ (a .gif).
|
||||
widget.addControl = function(id, control) {
|
||||
var markup = '<a class="crm-i ' + control.icon + '" alt="' + control.description + '" title="' + control.description + '"></a>';
|
||||
control.element = $(markup).prependTo($('.widget-controls', widget.element)).click(control.callback);
|
||||
};
|
||||
|
||||
// Fetch remote content.
|
||||
widget.reloadContent = function() {
|
||||
// If minimized, we'll reload later
|
||||
if (widget.minimized) {
|
||||
widget.contentLoaded = false;
|
||||
widget.lastLoaded = 0;
|
||||
} else {
|
||||
CRM.loadPage(widget.url, {target: widget.contentElement});
|
||||
}
|
||||
};
|
||||
|
||||
// Removes the widget from the dashboard, and saves columns.
|
||||
widget.remove = function() {
|
||||
invokeCallback(opts.widgetCallbacks.remove, widget);
|
||||
widget.element.fadeOut(opts.animationSpeed, function() {
|
||||
$(this).remove();
|
||||
delete(dashboard.widgets[widget.id]);
|
||||
dashboard.saveColumns(false);
|
||||
});
|
||||
CRM.alert(
|
||||
ts('You can re-add it by clicking the "Configure Your Dashboard" button.'),
|
||||
ts('"%1" Removed', {1: widget.title}),
|
||||
'success'
|
||||
);
|
||||
};
|
||||
|
||||
widget.cacheIsFresh = function() {
|
||||
return (((widget.cacheMinutes * 60000 + widget.lastLoaded) > $.now()) && widget.content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Public properties of widget.
|
||||
*/
|
||||
|
||||
// Default controls. External script can add more with widget.addControls()
|
||||
widget.controls = {
|
||||
settings: {
|
||||
description: ts('Configure this dashlet'),
|
||||
callback: widget.toggleSettings,
|
||||
icon: 'fa-wrench'
|
||||
},
|
||||
minimize: {
|
||||
description: widget.minimized ? ts('Expand') : ts('Collapse'),
|
||||
callback: widget.toggleMinimize,
|
||||
icon: widget.minimized ? 'fa-caret-right' : 'fa-caret-down'
|
||||
},
|
||||
fullscreen: {
|
||||
description: ts('View fullscreen'),
|
||||
callback: widget.enterFullscreen,
|
||||
icon: 'fa-expand'
|
||||
},
|
||||
close: {
|
||||
description: ts('Remove from dashboard'),
|
||||
callback: widget.remove,
|
||||
icon: 'fa-times'
|
||||
}
|
||||
};
|
||||
widget.contentLoaded = false;
|
||||
|
||||
init();
|
||||
return widget;
|
||||
|
||||
/**
|
||||
* Private methods of widget.
|
||||
*/
|
||||
|
||||
function loadContent() {
|
||||
var loadFromCache = widget.cacheIsFresh();
|
||||
if (loadFromCache) {
|
||||
widget.contentElement.html(widget.content).trigger('crmLoad', widget);
|
||||
}
|
||||
widget.contentElement.off('crmLoad').on('crmLoad', function(event, data) {
|
||||
if ($(event.target).is(widget.contentElement)) {
|
||||
widget.content = data.content;
|
||||
// Cache for one day
|
||||
widget.lastLoaded = $.now();
|
||||
saveLocalCache();
|
||||
invokeCallback(opts.widgetCallbacks.get, widget);
|
||||
}
|
||||
});
|
||||
if (!loadFromCache) {
|
||||
widget.reloadContent();
|
||||
}
|
||||
widget.contentLoaded = true;
|
||||
}
|
||||
|
||||
// Build widget & load content.
|
||||
function init() {
|
||||
// Delete controls that don't apply to this widget.
|
||||
if (!widget.settings) {
|
||||
delete widget.controls.settings;
|
||||
widget.settings = {};
|
||||
}
|
||||
if (!widget.fullscreenUrl) {
|
||||
delete widget.controls.fullscreen;
|
||||
}
|
||||
var cssClass = 'widget-' + widget.name.replace('/', '-');
|
||||
widget.element.attr('id', 'widget-' + widget.id).addClass(cssClass);
|
||||
// Build and add the widget's DOM element.
|
||||
$(widget.element).append(widgetHTML());
|
||||
// Save the content element so that external scripts can reload it easily.
|
||||
widget.contentElement = $('.widget-content', widget.element);
|
||||
$.each(widget.controls, widget.addControl);
|
||||
|
||||
if (widget.minimized) {
|
||||
widget.contentElement.hide();
|
||||
} else {
|
||||
loadContent();
|
||||
}
|
||||
}
|
||||
|
||||
// Builds inner HTML for widgets.
|
||||
function widgetHTML() {
|
||||
var html = '';
|
||||
html += '<div class="widget-wrapper">';
|
||||
html += ' <div class="widget-controls"><h3 class="widget-header">' + widget.title + '</h3></div>';
|
||||
html += ' <div class="widget-content"></div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// Initializes a widgets settings pane.
|
||||
function initSettings() {
|
||||
// Overwrite widget.settings (boolean).
|
||||
initThrobber();
|
||||
widget.settings = {
|
||||
element: widget.throbber.show(),
|
||||
ready: false
|
||||
};
|
||||
|
||||
// Get the settings markup and script executables for this widget.
|
||||
var params = $.extend({}, opts.ajaxCallbacks.widgetSettings.data, { id: widget.id });
|
||||
$.getJSON(opts.ajaxCallbacks.widgetSettings.url, params, function(response, status) {
|
||||
$.extend(widget.settings, response);
|
||||
// Build and add the settings form to the DOM. Bind the form's submit event handler/callback.
|
||||
widget.settings.element = $(widgetSettingsHTML()).appendTo($('.widget-wrapper', widget.element)).submit(widget.saveSettings);
|
||||
// Bind the cancel button's event handler too.
|
||||
widget.settings.cancelButton = $('.widget-settings-cancel', widget.settings.element).click(cancelEditSettings);
|
||||
// Build and add the inner form elements from the HTML markup provided in the AJAX data.
|
||||
widget.settings.innerElement = $('.widget-settings-inner', widget.settings.element).append(widget.settings.markup);
|
||||
widget.settings.ready = true;
|
||||
|
||||
if (widget.settings.displayed) {
|
||||
// If the user hasn't clicked away from the settings pane, then display the form.
|
||||
widget.throbber.hide();
|
||||
widget.showSettings();
|
||||
}
|
||||
|
||||
getJavascript(widget.settings.initScript);
|
||||
});
|
||||
}
|
||||
|
||||
// Builds HTML for widget settings forms.
|
||||
function widgetSettingsHTML() {
|
||||
var html = '';
|
||||
html += '<form class="widget-settings">';
|
||||
html += ' <div class="widget-settings-inner"></div>';
|
||||
html += ' <div class="widget-settings-buttons">';
|
||||
html += ' <input id="' + widget.id + '-settings-save" class="widget-settings-save" value="Save" type="submit" />';
|
||||
html += ' <input id="' + widget.id + '-settings-cancel" class="widget-settings-cancel" value="Cancel" type="submit" />';
|
||||
html += ' </div>';
|
||||
html += '</form>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// Initializes a generic widget content throbber, for use by settings form and external scripts.
|
||||
function initThrobber() {
|
||||
if (!widget.throbber) {
|
||||
widget.throbber = $(opts.throbberMarkup).appendTo($('.widget-wrapper', widget.element));
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler/callback for cancel button clicks.
|
||||
// @todo test this gets caught by all browsers when the cancel button is 'clicked' via the keyboard.
|
||||
function cancelEditSettings() {
|
||||
widget.toggleSettings();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper function to execute external script on the server.
|
||||
// @todo It would be nice to provide some context to the script. How?
|
||||
function getJavascript(url) {
|
||||
if (url) {
|
||||
$.getScript(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Public static properties of dashboard. Default settings.
|
||||
$.fn.dashboard.defaults = {
|
||||
columns: 2,
|
||||
emptyPlaceholderInner: '',
|
||||
throbberMarkup: '',
|
||||
animationSpeed: 200,
|
||||
callbacks: {},
|
||||
widgetCallbacks: {}
|
||||
};
|
||||
|
||||
// Default widget settings.
|
||||
$.fn.dashboard.widget = {
|
||||
defaults: {
|
||||
minimized: false,
|
||||
content: null,
|
||||
lastLoaded: 0,
|
||||
settings: false
|
||||
// id, url, fullscreenUrl, title, name, cacheMinutes
|
||||
}
|
||||
};
|
||||
})(jQuery);
|
Loading…
Add table
Add a link
Reference in a new issue