2 +--------------------------------------------------------------------+
3 | CiviCRM version 4.7 |
4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC (c) 2004-2015 |
6 +--------------------------------------------------------------------+
7 | This file is a part of CiviCRM. |
9 | CiviCRM is free software; you can copy, modify, and distribute it |
10 | under the terms of the GNU Affero General Public License |
11 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | CiviCRM is distributed in the hope that it will be useful, but |
14 | WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
16 | See the GNU Affero General Public License for more details. |
18 | You should have received a copy of the GNU Affero General Public |
19 | License and the CiviCRM Licensing Exception along |
20 | with this program; if not, contact CiviCRM LLC |
21 | at info[AT]civicrm[DOT]org. If you have questions about the |
22 | GNU Affero General Public License or the licensing of CiviCRM, |
23 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
24 +--------------------------------------------------------------------+
26 * Copyright (C) 2009 Bevan Rudge
27 * Licensed to CiviCRM under the Academic Free License version 3.0.
29 * @file Defines the jQuery.dashboard() plugin.
31 * Uses jQuery 1.3, jQuery UI 1.6 and several jQuery UI extensions, most of all Sortable
32 * http://visualjquery.com/
33 * http://docs.jquery.com/UI/Sortable
34 * http://ui.jquery.com/download
39 * NOTE: This file is viewed as "legacy" and shouldn't be used to
40 * develop new functionality. Its lint problems are grandfathered
41 * (although if someone wants to cleanup+test, please feel welcome).
43 /* jshint ignore:start */
44 (function($) { // Create closure.
45 // Constructor for dashboard object.
46 $.fn
.dashboard = function(options
) {
47 // Public properties of dashboard.
49 dashboard
.element
= this.empty();
50 dashboard
.ready
= false;
51 dashboard
.columns
= Array();
52 dashboard
.widgets
= Array();
53 // End of public properties of dashboard.
56 * Public methods of dashboard.
59 // Saves the order of widgets for all columns including the widget.minimized status to options.ajaxCallbacks.saveColumns.
60 dashboard
.saveColumns = function() {
61 // Update the display status of the empty placeholders.
62 for (var c
in dashboard
.columns
) {
63 var col
= dashboard
.columns
[c
];
64 if ( typeof col
== 'object' ) {
65 // Are there any visible children of the column (excluding the empty placeholder)?
66 if (col
.element
.children(':visible').not(col
.emptyPlaceholder
).length
> 0) {
67 col
.emptyPlaceholder
.hide();
70 col
.emptyPlaceholder
.show();
75 // Don't save any changes to the server unless the dashboard has finished initiating.
76 if (!dashboard
.ready
) {
80 // Build a list of params to post to the server.
84 for (var c2
in dashboard
.columns
) {
86 // IDs of the sortable elements in this column.
87 var ids
= (typeof dashboard
.columns
[c2
] == 'object') ? dashboard
.columns
[c2
].element
.sortable('toArray') : undefined;
91 // Chop 'widget-' off of the front so that we have the real widget id.
92 var id
= (typeof ids
[w
] == 'string') ? ids
[w
].substring('widget-'.length
) : undefined;
94 // Add one flat property to the params object that will look like an array element to the PHP server.
95 // Unfortunately jQuery doesn't do this for us.
96 if ( typeof dashboard
.widgets
[id
] == 'object' ) params
['columns[' + c2
+ '][' + id
+ ']'] = (dashboard
.widgets
[id
].minimized
? '1' : '0');
100 // The ajaxCallback settings overwrite any duplicate properties.
101 $.extend(params
, opts
.ajaxCallbacks
.saveColumns
.data
);
102 $.post(opts
.ajaxCallbacks
.saveColumns
.url
, params
, function(response
, status
) {
103 invokeCallback(opts
.callbacks
.saveColumns
, dashboard
);
107 // Puts the dashboard into full screen mode, saving element for when the user exits full-screen mode.
108 // Does not add element to the DOM – this is the caller's responsibility.
109 // Does show and hide element though.
110 dashboard
.enterFullscreen = function(element
) {
112 for (var c
in dashboard
.columns
) {
113 if ( typeof dashboard
.columns
[c
] == 'object' ) dashboard
.columns
[c
].element
.hide();
116 if (!dashboard
.fullscreen
) {
118 var markup
= '<a id="full-screen-header" class="full-screen-close-icon">' + opts
.fullscreenHeaderInner
+ '</a>';
119 dashboard
.fullscreen
= {
120 headerElement
: $(markup
).prependTo(dashboard
.element
).click(dashboard
.exitFullscreen
).hide()
124 dashboard
.fullscreen
.headerElement
.slideDown();
125 dashboard
.fullscreen
.currentElement
= element
.show();
126 dashboard
.fullscreen
.displayed
= true;
127 invokeCallback(opts
.callbacks
.enterFullscreen
, dashboard
, dashboard
.fullscreen
.currentElement
);
130 // Takes the dashboard out of full screen mode, hiding the active fullscreen element.
131 dashboard
.exitFullscreen = function() {
132 if (!dashboard
.fullscreen
.displayed
) {
136 dashboard
.fullscreen
.headerElement
.slideUp();
137 dashboard
.fullscreen
.currentElement
.hide();
138 dashboard
.fullscreen
.displayed
= false;
141 for (var c
in dashboard
.columns
) {
142 if ( typeof dashboard
.columns
[c
] == 'object' ) dashboard
.columns
[c
].element
.show();
145 invokeCallback(opts
.callbacks
.exitFullscreen
, dashboard
, dashboard
.fullscreen
.currentElement
);
147 // End of public methods of dashboard.
150 * Private properties of dashboard.
153 // Used to determine whether there are any incomplete ajax requests pending initialization of the dashboard.
154 var asynchronousRequestCounter
= 0;
156 // Used to determine whether two resort events are resulting from the same UI event.
157 var currentReSortEvent
= null;
159 // Merge in the caller's options with the defaults.
160 var opts
= $.extend({}, $.fn
.dashboard
.defaults
, options
);
162 // Execution 'forks' here and restarts in init(). Tell the user we're busy with a throbber.
163 var throbber
= $(opts
.throbberMarkup
).appendTo(dashboard
.element
);
164 $.getJSON(opts
.ajaxCallbacks
.getWidgetsByColumn
.url
, opts
.ajaxCallbacks
.getWidgetsByColumn
.data
, init
);
165 asynchronousRequestCounter
++;
167 // End of constructor and private properties for dashboard object.
170 * Private methods of dashboard.
173 // Ajax callback for getWidgetsByColumn.
174 function init(widgets
, status
) {
175 asynchronousRequestCounter
--;
177 var markup
= '<li class="empty-placeholder">' + opts
.emptyPlaceholderInner
+ '</li>';
179 // Build the dashboard in the DOM. For each column...
180 // (Don't iterate on widgets since this will break badly if the dataset has empty columns.)
181 var emptyDashboard
= true;
182 for (var c
= 0; c
< opts
.columns
; c
++) {
183 // Save the column to both the public scope for external accessibility and the local scope for readability.
184 var col
= dashboard
.columns
[c
] = {
185 initialWidgets
: Array(),
186 element
: $('<ul id="column-' + c
+ '" class="column column-' + c
+ '"></ul>').appendTo(dashboard
.element
)
189 // Add the empty placeholder now, hide it and save it.
190 col
.emptyPlaceholder
= $(markup
).appendTo(col
.element
).hide();
192 // For each widget in this column.
193 for (var id
in widgets
[c
]) {
194 var widgetID
= id
.split('-');
195 // Build a new widget object and save it to various publicly accessible places.
196 col
.initialWidgets
[id
] = dashboard
.widgets
[widgetID
[1]] = widget({
198 element
: $('<li class="widget"></li>').appendTo(col
.element
),
200 minimized
: ( widgets
[c
][widgetID
[1]] > 0 ? true : false )
203 //set empty Dashboard to false
204 emptyDashboard
= false;
208 if ( emptyDashboard
) {
209 emptyDashboardCondition( );
212 invokeCallback(opts
.callbacks
.init
, dashboard
);
215 // function that is called when dashboard is empty
216 function emptyDashboardCondition( ) {
217 cj(".show-refresh").hide( );
218 cj("#empty-message").show( );
221 // Contructors for each widget call this when initialization has finished so that dashboard can complete it's intitialization.
222 function completeInit() {
223 // Don't do anything if any widgets are waiting for ajax requests to complete in order to finish initialization.
224 if (asynchronousRequestCounter
> 0) {
228 // Make widgets sortable across columns.
229 dashboard
.sortableElement
= $('.column').sortable({
230 connectWith
: ['.column'],
232 // The class of the element by which widgets are draggable.
233 handle
: '.widget-header',
235 // The class of placeholder elements (the 'ghost' widget showing where the dragged item would land if released now.)
236 placeholder
: 'placeholder',
237 activate: function(event
, ui
) {
238 var h
= cj(ui
.item
).height();
239 $('.placeholder').css('height', h
+'px'); },
243 // Maks sure that only widgets are sortable, and not empty placeholders.
246 forcePlaceholderSize
: true,
248 // Callback functions.
250 start
: hideEmptyPlaceholders
253 // Update empty placeholders.
254 dashboard
.saveColumns();
255 dashboard
.ready
= true;
256 invokeCallback(opts
.callbacks
.ready
, dashboard
);
259 // Callback for when any list has changed (and the user has finished resorting).
260 function resorted(e
, ui
) {
261 // Only do anything if we haven't already handled resorts based on changes from this UI DOM event.
262 // (resorted() gets invoked once for each list when an item is moved from one to another.)
263 if (!currentReSortEvent
|| e
.originalEvent
!= currentReSortEvent
) {
264 currentReSortEvent
= e
.originalEvent
;
265 dashboard
.saveColumns();
269 // Callback for when a user starts resorting a list. Hides all the empty placeholders.
270 function hideEmptyPlaceholders(e
, ui
) {
271 for (var c
in dashboard
.columns
) {
272 if( (typeof dashboard
.columns
[c
]) == 'object' ) dashboard
.columns
[c
].emptyPlaceholder
.hide();
276 // @todo use an event library to register, bind to and invoke events.
277 // @param callback is a function.
278 // @param theThis is the context given to that function when it executes. It becomes 'this' inside of that function.
279 function invokeCallback(callback
, theThis
, parameterOne
) {
281 callback
.call(theThis
, parameterOne
);
287 * Private sub-class of dashboard
290 function widget(widget
) {
291 // Merge default options with the options defined for this widget.
292 widget
= $.extend({}, $.fn
.dashboard
.widget
.defaults
, widget
);
295 * Public methods of widget.
298 // Toggles the minimize() & maximize() methods.
299 widget
.toggleMinimize = function() {
300 if (widget
.minimized
) {
307 widget
.hideSettings();
308 dashboard
.saveColumns();
310 widget
.minimize = function() {
311 $('.widget-content', widget
.element
).slideUp(opts
.animationSpeed
);
312 $(widget
.controls
.minimize
.element
).addClass( 'fa-caret-right' );
313 $(widget
.controls
.minimize
.element
).removeClass( 'fa-caret-down' );
314 widget
.minimized
= true;
316 widget
.maximize = function() {
317 $('.widget-content', widget
.element
).slideDown(opts
.animationSpeed
);
318 $(widget
.controls
.minimize
.element
).removeClass( 'fa-caret-right' );
319 $(widget
.controls
.minimize
.element
).addClass( 'fa-caret-down' );
320 widget
.minimized
= false;
323 // Toggles whether the widget is in settings-display mode or not.
324 widget
.toggleSettings = function() {
325 if (widget
.settings
.displayed
) {
326 // Widgets always exit settings into maximized state.
328 widget
.hideSettings();
329 invokeCallback(opts
.widgetCallbacks
.hideSettings
, widget
);
333 widget
.showSettings();
334 invokeCallback(opts
.widgetCallbacks
.showSettings
, widget
);
337 widget
.showSettings = function() {
338 if (widget
.settings
.element
) {
339 widget
.settings
.element
.show();
341 // Settings are loaded via AJAX. Only execute the script if the settings have been loaded.
342 if (widget
.settings
.ready
) {
343 getJavascript(widget
.settings
.script
);
347 // Settings have not been initialized. Do so now.
350 widget
.settings
.displayed
= true;
352 widget
.hideSettings = function() {
353 if (widget
.settings
.element
) {
354 widget
.settings
.element
.hide();
356 widget
.settings
.displayed
= false;
358 widget
.saveSettings = function() {
359 // Build list of parameters to POST to server.
361 // serializeArray() returns an array of objects. Process it.
362 var fields
= widget
.settings
.element
.serializeArray();
363 for (var i
in fields
) {
364 var field
= fields
[i
];
365 // Put the values into flat object properties that PHP will parse into an array server-side.
366 // (Unfortunately jQuery doesn't do this)
367 params
['settings[' + field
.name
+ ']'] = field
.value
;
370 // Things get messy here.
371 // @todo Refactor to use currentState and targetedState properties to determine what needs
372 // to be done to get to any desired state on any UI or AJAX event – since these don't always
374 // E.g. When a user starts a new UI event before the Ajax event handler from a previous
375 // UI event gets invoked.
377 // Hide the settings first of all.
378 widget
.toggleSettings();
379 // Save the real settings element so that we can restore the reference later.
380 var settingsElement
= widget
.settings
.element
;
381 // Empty the settings form.
382 widget
.settings
.innerElement
.empty();
384 // So that showSettings() and hideSettings() can do SOMETHING, without showing the empty settings form.
385 widget
.settings
.element
= widget
.throbber
.hide();
386 widget
.settings
.ready
= false;
388 // Save the settings to the server.
389 $.extend(params
, opts
.ajaxCallbacks
.widgetSettings
.data
, { id
: widget
.id
});
390 $.post(opts
.ajaxCallbacks
.widgetSettings
.url
, params
, function(response
, status
) {
391 // Merge the response into widget.settings.
392 $.extend(widget
.settings
, response
);
393 // Restore the reference to the real settings element.
394 widget
.settings
.element
= settingsElement
;
395 // Make sure the settings form is empty and add the updated settings form.
396 widget
.settings
.innerElement
.empty().append(widget
.settings
.markup
);
397 widget
.settings
.ready
= true;
399 // Did the user already jump back into settings-display mode before we could finish reloading the settings form?
400 if (widget
.settings
.displayed
) {
401 // Ooops! We had better take care of hiding the throbber and showing the settings form then.
402 widget
.throbber
.hide();
403 widget
.showSettings();
404 invokeCallback(opts
.widgetCallbacks
.saveSettings
, dashboard
);
408 // Don't let form submittal bubble up.
412 widget
.enterFullscreen = function() {
413 // Make sure the widget actually supports full screen mode.
414 if (!widget
.fullscreenUrl
) {
417 CRM
.loadPage(widget
.fullscreenUrl
);
420 // Exit fullscreen mode.
421 widget
.exitFullscreen = function() {
422 // This is just a wrapper for dashboard.exitFullscreen() which does the heavy lifting.
423 dashboard
.exitFullscreen();
426 // Adds controls to a widget. id is for internal use and image file name in images/dashboard/ (a .gif).
427 widget
.addControl = function(id
, control
) {
428 var markup
= '<a class="crm-i ' + control
.icon
+ '" alt="' + control
.description
+ '" title="' + control
.description
+ '"></a>';
429 control
.element
= $(markup
).prependTo($('.widget-controls', widget
.element
)).click(control
.callback
);
432 // An external method used only by and from external scripts to reload content. Not invoked or used internally.
433 // The widget must provide the script that executes this, as well as the script that invokes it.
434 widget
.reloadContent = function() {
435 getJavascript(widget
.reloadContentScript
);
436 invokeCallback(opts
.widgetCallbacks
.reloadContent
, widget
);
439 // Removes the widget from the dashboard, and saves columns.
440 widget
.remove = function() {
441 if ( confirm( 'Are you sure you want to remove "' + widget
.title
+ '"?') ) {
442 invokeCallback(opts
.widgetCallbacks
.remove
, widget
);
443 widget
.element
.fadeOut(opts
.animationSpeed
, function() {
445 dashboard
.saveColumns();
449 // End public methods of widget.
452 * Public properties of widget.
455 // Default controls. External script can add more with widget.addControls()
458 description
: ts('Configure this dashlet'),
459 callback
: widget
.toggleSettings
,
463 description
: ts('Collapse or expand'),
464 callback
: widget
.toggleMinimize
,
465 icon
: 'fa-caret-down',
468 description
: ts('View fullscreen'),
469 callback
: widget
.enterFullscreen
,
473 description
: ts('Remove from dashboard'),
474 callback
: widget
.remove
,
478 // End public properties of widget.
481 * Private properties of widget.
484 // We're gonna 'fork' execution again, so let's tell the user to hold with us till the AJAX callback gets invoked.
485 var throbber
= $(opts
.throbberMarkup
).appendTo(widget
.element
);
486 var params
= $.extend({}, opts
.ajaxCallbacks
.getWidget
.data
, {id
: widget
.id
});
487 $.getJSON(opts
.ajaxCallbacks
.getWidget
.url
, params
, init
);
489 // Help dashboard track whether we've got any outstanding requests on which initialization is pending.
490 asynchronousRequestCounter
++;
492 // End of private properties of widget.
495 * Private methods of widget.
498 // Ajax callback for widget initialization.
499 function init(data
, status
) {
500 asynchronousRequestCounter
--;
501 $.extend(widget
, data
);
503 // Delete controls that don't apply to this widget.
504 if (!widget
.settings
) {
505 delete widget
.controls
.settings
;
507 if (!widget
.fullscreenUrl
) {
508 delete widget
.controls
.fullscreen
;
511 widget
.element
.attr('id', 'widget-' + widget
.id
).addClass(widget
.classes
);
513 // Build and add the widget's DOM element.
514 $(widget
.element
).append(widgetHTML()).trigger('crmLoad');
515 // Save the content element so that external scripts can reload it easily.
516 widget
.contentElement
= $('.widget-content', widget
.element
);
517 $.each(widget
.controls
, widget
.addControl
);
519 // Switch the initial state so that it initializes to the correct state.
520 widget
.minimized
= !widget
.minimized
;
521 widget
.toggleMinimize();
522 getJavascript(widget
.initScript
);
523 invokeCallback(opts
.widgetCallbacks
.get, widget
);
525 // completeInit() is a private method of the dashboard. Let it complete initialization of the dashboard.
529 // Builds inner HTML for widgets.
530 function widgetHTML() {
532 html
+= '<div class="widget-wrapper">';
533 html
+= ' <div class="widget-controls"><h3 class="widget-header">' + widget
.title
+ '</h3></div>';
534 html
+= ' <div class="widget-content crm-ajax-container">' + widget
.content
+ '</div>';
539 // Initializes a widgets settings pane.
540 function initSettings() {
541 // Overwrite widget.settings (boolean).
544 element
: widget
.throbber
.show(),
548 // Get the settings markup and script executables for this widget.
549 var params
= $.extend({}, opts
.ajaxCallbacks
.widgetSettings
.data
, { id
: widget
.id
});
550 $.getJSON(opts
.ajaxCallbacks
.widgetSettings
.url
, params
, function(response
, status
) {
551 $.extend(widget
.settings
, response
);
552 // Build and add the settings form to the DOM. Bind the form's submit event handler/callback.
553 widget
.settings
.element
= $(widgetSettingsHTML()).appendTo($('.widget-wrapper', widget
.element
)).submit(widget
.saveSettings
);
554 // Bind the cancel button's event handler too.
555 widget
.settings
.cancelButton
= $('.widget-settings-cancel', widget
.settings
.element
).click(cancelEditSettings
);
556 // Build and add the inner form elements from the HTML markup provided in the AJAX data.
557 widget
.settings
.innerElement
= $('.widget-settings-inner', widget
.settings
.element
).append(widget
.settings
.markup
);
558 widget
.settings
.ready
= true;
560 if (widget
.settings
.displayed
) {
561 // If the user hasn't clicked away from the settings pane, then display the form.
562 widget
.throbber
.hide();
563 widget
.showSettings();
566 getJavascript(widget
.settings
.initScript
);
570 // Builds HTML for widget settings forms.
571 function widgetSettingsHTML() {
573 html
+= '<form class="widget-settings">';
574 html
+= ' <div class="widget-settings-inner"></div>';
575 html
+= ' <div class="widget-settings-buttons">';
576 html
+= ' <input id="' + widget
.id
+ '-settings-save" class="widget-settings-save" value="Save" type="submit" />';
577 html
+= ' <input id="' + widget
.id
+ '-settings-cancel" class="widget-settings-cancel" value="Cancel" type="submit" />';
583 // Initializes a generic widget content throbber, for use by settings form and external scripts.
584 function initThrobber() {
585 if (!widget
.throbber
) {
586 widget
.throbber
= $(opts
.throbberMarkup
).appendTo($('.widget-wrapper', widget
.element
));
590 // Event handler/callback for cancel button clicks.
591 // @todo test this gets caught by all browsers when the cancel button is 'clicked' via the keyboard.
592 function cancelEditSettings() {
593 widget
.toggleSettings();
597 // Helper function to execute external script on the server.
598 // @todo It would be nice to provide some context to the script. How?
599 function getJavascript(url
) {
607 // Public static properties of dashboard. Default settings.
608 $.fn
.dashboard
.defaults
= {
610 emptyPlaceholderInner
: ts('There are no dashlets in this column of your dashboard.'),
611 fullscreenHeaderInner
: ts('Back to dashboard mode'),
612 throbberMarkup
: '<div class="crm-loading-element">' + ts('Loading') + '...</div>',
618 // Default widget settings.
619 $.fn
.dashboard
.widget
= {