/**
+--------------------------------------------------------------------+
| CiviCRM version 4.2 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2012 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*
* Copyright (C) 2009 Bevan Rudge
* Licensed to CiviCRM under the Academic Free License version 3.0.
*
* @file Defines the jQuery.dashboard() plugin.
*
* Uses jQuery 1.3, jQuery UI 1.6 and several jQuery UI extensions, most of all Sortable
* http://visualjquery.com/
* http://docs.jquery.com/UI/Sortable
* http://ui.jquery.com/download
* Sortable
* Draggable
* UI Core
*
*/
(function($) { // Create closure.
// Constructor for dashboard object.
$.fn.dashboard = function(options) {
// Public properties of dashboard.
var dashboard = {};
dashboard.element = this.empty();
dashboard.ready = false;
dashboard.columns = Array();
dashboard.widgets = Array();
// End of public properties of dashboard.
/**
* Public methods of dashboard.
*/
// Saves the order of widgets for all columns including the widget.minimized status to options.ajaxCallbacks.saveColumns.
dashboard.saveColumns = function() {
// Update the display status of the empty placeholders.
for (var c in dashboard.columns) {
var col = dashboard.columns[c];
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...
for (var c in dashboard.columns) {
// IDs of the sortable elements in this column.
if( typeof dashboard.columns[c] == 'object' ) var ids = dashboard.columns[c].element.sortable('toArray');
// For each id...
for (var w in ids) {
// Chop 'widget-' off of the front so that we have the real widget id.
if( typeof ids[w] == 'string' ) var id = ids[w].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);
$.post(opts.ajaxCallbacks.saveColumns.url, params, function(response, status) {
invokeCallback(opts.callbacks.saveColumns, dashboard);
});
};
// Puts the dashboard into full screen mode, saving element for when the user exits full-screen mode.
// Does not add element to the DOM – this is the caller's responsibility.
// Does show and hide element though.
dashboard.enterFullscreen = function(element) {
// Hide the columns.
for (var c in dashboard.columns) {
if ( typeof dashboard.columns[c] == 'object' ) dashboard.columns[c].element.hide();
}
if (!dashboard.fullscreen) {
// Initialize.
var markup = '';
dashboard.fullscreen = {
headerElement: $(markup).prependTo(dashboard.element).click(dashboard.exitFullscreen).hide()
};
}
dashboard.fullscreen.headerElement.slideDown();
dashboard.fullscreen.currentElement = element.show();
dashboard.fullscreen.displayed = true;
invokeCallback(opts.callbacks.enterFullscreen, dashboard, dashboard.fullscreen.currentElement);
};
// Takes the dashboard out of full screen mode, hiding the active fullscreen element.
dashboard.exitFullscreen = function() {
if (!dashboard.fullscreen.displayed) {
return;
}
dashboard.fullscreen.headerElement.slideUp();
dashboard.fullscreen.currentElement.hide();
dashboard.fullscreen.displayed = false;
// Show the columns.
for (var c in dashboard.columns) {
if ( typeof dashboard.columns[c] == 'object' ) dashboard.columns[c].element.show();
}
invokeCallback(opts.callbacks.exitFullscreen, dashboard, dashboard.fullscreen.currentElement);
};
// End of public methods of dashboard.
/**
* Private properties of dashboard.
*/
// Used to determine whether there are any incomplete ajax requests pending initialization of the dashboard.
var asynchronousRequestCounter = 0;
// 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);
// Execution 'forks' here and restarts in init(). Tell the user we're busy with a throbber.
var throbber = $(opts.throbberMarkup).appendTo(dashboard.element);
$.getJSON(opts.ajaxCallbacks.getWidgetsByColumn.url, opts.ajaxCallbacks.getWidgetsByColumn.data, init);
asynchronousRequestCounter++;
return dashboard;
// End of constructor and private properties for dashboard object.
/**
* Private methods of dashboard.
*/
// Ajax callback for getWidgetsByColumn.
function init(widgets, status) {
asynchronousRequestCounter--;
throbber.remove();
var markup = '
' + opts.emptyPlaceholderInner + '';
// 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: Array(),
element: $('').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.
for (var id in widgets[c]) {
var widgetID = id.split('-');
// Build a new widget object and save it to various publicly accessible places.
col.initialWidgets[id] = dashboard.widgets[widgetID[1]] = widget({
id: widgetID[1],
element: $('').appendTo(col.element),
initialColumn: col,
minimized: ( widgets[c][widgetID[1]] > 0 ? true : false )
});
//set empty Dashboard to false
emptyDashboard = false;
}
}
if ( emptyDashboard ) {
emptyDashboardCondition( );
}
invokeCallback(opts.callbacks.init, dashboard);
}
// function that is called when dashboard is empty
function emptyDashboardCondition( ) {
cj(".show-refresh").hide( );
cj("#empty-message").show( );
}
// Contructors for each widget call this when initialization has finished so that dashboard can complete it's intitialization.
function completeInit() {
// Don't do anything if any widgets are waiting for ajax requests to complete in order to finish initialization.
if (asynchronousRequestCounter > 0) {
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= cj(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);
}
// 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, widget);
/**
* Public methods of widget.
*/
// Toggles the minimize() & maximize() methods.
widget.toggleMinimize = function() {
if (widget.minimized) {
widget.maximize();
}
else {
widget.minimize();
}
widget.hideSettings();
dashboard.saveColumns();
};
widget.minimize = function() {
$('.widget-content', widget.element).slideUp(opts.animationSpeed);
$(widget.controls.minimize.element).addClass( 'maximize-icon' );
$(widget.controls.minimize.element).removeClass( 'minimize-icon' );
widget.minimized = true;
};
widget.maximize = function() {
$('.widget-content', widget.element).slideDown(opts.animationSpeed);
$(widget.controls.minimize.element).removeClass( 'maximize-icon' );
$(widget.controls.minimize.element).addClass( 'minimize-icon' );
widget.minimized = false;
};
// 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();
for (var i in fields) {
var field = fields[i];
// 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) {
return;
}
$('')
.html('')
.dialog({
autoOpen: true,
title: widget.title,
modal: true,
height: 'auto',
width: 'auto',
position: [100,125],
close: function(event, ui) {
cj(this).dialog("destroy");
$('#crm-dashlet-container').remove();
$('#crm-dashlet-fullscreen-content').remove();
}
});
$.ajax({
url: widget.fullscreenUrl,
success: function ( content ) {
$('#crm-dashlet-fullscreen-content').html( content );
}
});
};
// Exit fullscreen mode.
widget.exitFullscreen = function() {
// This is just a wrapper for dashboard.exitFullscreen() which does the heavy lifting.
dashboard.exitFullscreen();
};
// 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 = '';
control.element = $(markup).prependTo($('.widget-controls', widget.element)).click(control.callback);
};
// An external method used only by and from external scripts to reload content. Not invoked or used internally.
// The widget must provide the script that executes this, as well as the script that invokes it.
widget.reloadContent = function() {
getJavascript(widget.reloadContentScript);
invokeCallback(opts.widgetCallbacks.reloadContent, widget);
};
// Removes the widget from the dashboard, and saves columns.
widget.remove = function() {
if ( confirm( 'Are you sure you want to remove "' + widget.title + '"?') ) {
invokeCallback(opts.widgetCallbacks.remove, widget);
widget.element.fadeOut(opts.animationSpeed, function() {
$(this).remove();
dashboard.saveColumns();
});
}
};
// End public methods of widget.
/**
* Public properties of widget.
*/
// Default controls. External script can add more with widget.addControls()
widget.controls = {
settings: {
description: 'Configure this dashlet',
callback: widget.toggleSettings
},
minimize: {
description: 'Collapse or expand this dashlet',
callback: widget.toggleMinimize
},
fullscreen: {
description: 'View this dashlet in full screen mode',
callback: widget.enterFullscreen
},
close: {
description: 'Remove this dashlet from your dashboard',
callback: widget.remove
}
};
// End public properties of widget.
/**
* Private properties of widget.
*/
// We're gonna 'fork' execution again, so let's tell the user to hold with us till the AJAX callback gets invoked.
var throbber = $(opts.throbberMarkup).appendTo(widget.element);
var params = $.extend({}, opts.ajaxCallbacks.getWidget.data, {id: widget.id});
$.getJSON(opts.ajaxCallbacks.getWidget.url, params, init);
// Help dashboard track whether we've got any outstanding requests on which initialization is pending.
asynchronousRequestCounter++;
return widget;
// End of private properties of widget.
/**
* Private methods of widget.
*/
// Ajax callback for widget initialization.
function init(data, status) {
asynchronousRequestCounter--;
$.extend(widget, data);
// Delete controls that don't apply to this widget.
if (!widget.settings) {
delete widget.controls.settings;
}
if (!widget.fullscreenUrl) {
delete widget.controls.fullscreen;
}
widget.element.attr('id', 'widget-' + widget.id).addClass(widget.classes);
throbber.remove();
// Build and add the widget's DOM element.
$(widgetHTML()).appendTo(widget.element);
// Save the content element so that external scripts can reload it easily.
widget.contentElement = $('.widget-content', widget.element);
$.each(widget.controls, widget.addControl);
// Switch the initial state so that it initializes to the correct state.
widget.minimized = !widget.minimized;
widget.toggleMinimize();
getJavascript(widget.initScript);
invokeCallback(opts.widgetCallbacks.get, widget);
// completeInit() is a private method of the dashboard. Let it complete initialization of the dashboard.
completeInit();
}
// Builds inner HTML for widgets.
function widgetHTML() {
var html = '';
html += '';
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 += '';
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: 'There are no dashlets in this column of your dashboard.',
fullscreenHeaderInner: 'Back to dashboard mode',
throbberMarkup: 'Loading...
',
animationSpeed: 200,
callbacks: {},
widgetCallbacks: {}
};
// Default widget settings.
$.fn.dashboard.widget = {
defaults: {
minimized: false,
settings: false,
fullscreen: false
}
};
})(jQuery); // end of closure