more whitespace cleanup
[civicrm-core.git] / js / jquery / jquery.dashboard.js
CommitLineData
3d80d622
CW
1/**
2 +--------------------------------------------------------------------+
f5721b07 3 | CiviCRM version 4.5 |
3d80d622 4 +--------------------------------------------------------------------+
f5721b07 5 | Copyright CiviCRM LLC (c) 2004-2014 |
3d80d622
CW
6 +--------------------------------------------------------------------+
7 | This file is a part of CiviCRM. |
8 | |
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. |
12 | |
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. |
17 | |
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 +--------------------------------------------------------------------+
25 *
26 * Copyright (C) 2009 Bevan Rudge
27 * Licensed to CiviCRM under the Academic Free License version 3.0.
28 *
29 * @file Defines the jQuery.dashboard() plugin.
30 *
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
35 * Sortable
36 * Draggable
37 * UI Core
38 *
39 */
40
41(function($) { // Create closure.
42 // Constructor for dashboard object.
43 $.fn.dashboard = function(options) {
44 // Public properties of dashboard.
45 var dashboard = {};
46 dashboard.element = this.empty();
47 dashboard.ready = false;
48 dashboard.columns = Array();
49 dashboard.widgets = Array();
50 // End of public properties of dashboard.
51
52 /**
53 * Public methods of dashboard.
54 */
55
56 // Saves the order of widgets for all columns including the widget.minimized status to options.ajaxCallbacks.saveColumns.
57 dashboard.saveColumns = function() {
58 // Update the display status of the empty placeholders.
59 for (var c in dashboard.columns) {
60 var col = dashboard.columns[c];
61 if ( typeof col == 'object' ) {
62 // Are there any visible children of the column (excluding the empty placeholder)?
63 if (col.element.children(':visible').not(col.emptyPlaceholder).length > 0) {
64 col.emptyPlaceholder.hide();
65 }
66 else {
67 col.emptyPlaceholder.show();
68 }
69 }
70 }
71
72 // Don't save any changes to the server unless the dashboard has finished initiating.
73 if (!dashboard.ready) {
74 return;
75 }
76
77 // Build a list of params to post to the server.
78 var params = {};
79
80 // For each column...
81 for (var c in dashboard.columns) {
82
83 // IDs of the sortable elements in this column.
84 if( typeof dashboard.columns[c] == 'object' ) var ids = dashboard.columns[c].element.sortable('toArray');
85
86 // For each id...
87 for (var w in ids) {
88 // Chop 'widget-' off of the front so that we have the real widget id.
89 if( typeof ids[w] == 'string' ) var id = ids[w].substring('widget-'.length);
90
91 // Add one flat property to the params object that will look like an array element to the PHP server.
92 // Unfortunately jQuery doesn't do this for us.
93 if ( typeof dashboard.widgets[id] == 'object' ) params['columns[' + c + '][' + id + ']'] = (dashboard.widgets[id].minimized ? '1' : '0');
94 }
95 }
96
97 // The ajaxCallback settings overwrite any duplicate properties.
98 $.extend(params, opts.ajaxCallbacks.saveColumns.data);
99 $.post(opts.ajaxCallbacks.saveColumns.url, params, function(response, status) {
100 invokeCallback(opts.callbacks.saveColumns, dashboard);
101 });
102 };
103
104 // Puts the dashboard into full screen mode, saving element for when the user exits full-screen mode.
105 // Does not add element to the DOM – this is the caller's responsibility.
106 // Does show and hide element though.
107 dashboard.enterFullscreen = function(element) {
108 // Hide the columns.
109 for (var c in dashboard.columns) {
110 if ( typeof dashboard.columns[c] == 'object' ) dashboard.columns[c].element.hide();
111 }
112
113 if (!dashboard.fullscreen) {
114 // Initialize.
115 var markup = '<a id="full-screen-header" class="full-screen-close-icon">' + opts.fullscreenHeaderInner + '</a>';
116 dashboard.fullscreen = {
117 headerElement: $(markup).prependTo(dashboard.element).click(dashboard.exitFullscreen).hide()
118 };
119 }
120
121 dashboard.fullscreen.headerElement.slideDown();
122 dashboard.fullscreen.currentElement = element.show();
123 dashboard.fullscreen.displayed = true;
124 invokeCallback(opts.callbacks.enterFullscreen, dashboard, dashboard.fullscreen.currentElement);
125 };
126
127 // Takes the dashboard out of full screen mode, hiding the active fullscreen element.
128 dashboard.exitFullscreen = function() {
129 if (!dashboard.fullscreen.displayed) {
130 return;
131 }
132
133 dashboard.fullscreen.headerElement.slideUp();
134 dashboard.fullscreen.currentElement.hide();
135 dashboard.fullscreen.displayed = false;
136
137 // Show the columns.
138 for (var c in dashboard.columns) {
139 if ( typeof dashboard.columns[c] == 'object' ) dashboard.columns[c].element.show();
140 }
141
142 invokeCallback(opts.callbacks.exitFullscreen, dashboard, dashboard.fullscreen.currentElement);
143 };
144 // End of public methods of dashboard.
145
146 /**
147 * Private properties of dashboard.
148 */
31037a42 149
3d80d622
CW
150 // Used to determine whether there are any incomplete ajax requests pending initialization of the dashboard.
151 var asynchronousRequestCounter = 0;
152
153 // Used to determine whether two resort events are resulting from the same UI event.
154 var currentReSortEvent = null;
155
156 // Merge in the caller's options with the defaults.
157 var opts = $.extend({}, $.fn.dashboard.defaults, options);
158
159 // Execution 'forks' here and restarts in init(). Tell the user we're busy with a throbber.
160 var throbber = $(opts.throbberMarkup).appendTo(dashboard.element);
161 $.getJSON(opts.ajaxCallbacks.getWidgetsByColumn.url, opts.ajaxCallbacks.getWidgetsByColumn.data, init);
162 asynchronousRequestCounter++;
163 return dashboard;
164 // End of constructor and private properties for dashboard object.
165
166 /**
167 * Private methods of dashboard.
168 */
169
170 // Ajax callback for getWidgetsByColumn.
171 function init(widgets, status) {
172 asynchronousRequestCounter--;
173 throbber.remove();
174 var markup = '<li class="empty-placeholder">' + opts.emptyPlaceholderInner + '</li>';
175
176 // Build the dashboard in the DOM. For each column...
177 // (Don't iterate on widgets since this will break badly if the dataset has empty columns.)
178 var emptyDashboard = true;
179 for (var c = 0; c < opts.columns; c++) {
180 // Save the column to both the public scope for external accessibility and the local scope for readability.
181 var col = dashboard.columns[c] = {
182 initialWidgets: Array(),
183 element: $('<ul id="column-' + c + '" class="column column-' + c + '"></ul>').appendTo(dashboard.element)
184 };
31037a42 185
3d80d622
CW
186 // Add the empty placeholder now, hide it and save it.
187 col.emptyPlaceholder = $(markup).appendTo(col.element).hide();
188
189 // For each widget in this column.
190 for (var id in widgets[c]) {
191 var widgetID = id.split('-');
192 // Build a new widget object and save it to various publicly accessible places.
193 col.initialWidgets[id] = dashboard.widgets[widgetID[1]] = widget({
194 id: widgetID[1],
195 element: $('<li class="widget"></li>').appendTo(col.element),
196 initialColumn: col,
197 minimized: ( widgets[c][widgetID[1]] > 0 ? true : false )
198 });
31037a42 199
3d80d622
CW
200 //set empty Dashboard to false
201 emptyDashboard = false;
202 }
203 }
204
205 if ( emptyDashboard ) {
206 emptyDashboardCondition( );
207 }
31037a42 208
3d80d622
CW
209 invokeCallback(opts.callbacks.init, dashboard);
210 }
211
212 // function that is called when dashboard is empty
213 function emptyDashboardCondition( ) {
214 cj(".show-refresh").hide( );
215 cj("#empty-message").show( );
216 }
31037a42 217
3d80d622
CW
218 // Contructors for each widget call this when initialization has finished so that dashboard can complete it's intitialization.
219 function completeInit() {
220 // Don't do anything if any widgets are waiting for ajax requests to complete in order to finish initialization.
221 if (asynchronousRequestCounter > 0) {
222 return;
223 }
224
225 // Make widgets sortable across columns.
226 dashboard.sortableElement = $('.column').sortable({
227 connectWith: ['.column'],
228
229 // The class of the element by which widgets are draggable.
230 handle: '.widget-header',
231
232 // The class of placeholder elements (the 'ghost' widget showing where the dragged item would land if released now.)
31037a42
EM
233 placeholder: 'placeholder',
234 activate: function(event, ui) {
235 var h= cj(ui.item).height();
3d80d622 236 $('.placeholder').css('height', h +'px'); },
31037a42 237
3d80d622
CW
238 opacity: 0.2,
239
240 // Maks sure that only widgets are sortable, and not empty placeholders.
241 items: '> .widget',
31037a42 242
3d80d622 243 forcePlaceholderSize: true,
31037a42 244
3d80d622
CW
245 // Callback functions.
246 update: resorted,
247 start: hideEmptyPlaceholders
248 });
249
250 // Update empty placeholders.
251 dashboard.saveColumns();
252 dashboard.ready = true;
253 invokeCallback(opts.callbacks.ready, dashboard);
254 }
255
256 // Callback for when any list has changed (and the user has finished resorting).
257 function resorted(e, ui) {
258 // Only do anything if we haven't already handled resorts based on changes from this UI DOM event.
259 // (resorted() gets invoked once for each list when an item is moved from one to another.)
260 if (!currentReSortEvent || e.originalEvent != currentReSortEvent) {
261 currentReSortEvent = e.originalEvent;
262 dashboard.saveColumns();
263 }
264 }
265
266 // Callback for when a user starts resorting a list. Hides all the empty placeholders.
267 function hideEmptyPlaceholders(e, ui) {
268 for (var c in dashboard.columns) {
269 if( typeof dashboard.columns[c] == 'object ' ) dashboard.columns[c].emptyPlaceholder.hide();
270 }
271 }
272
273 // @todo use an event library to register, bind to and invoke events.
274 // @param callback is a function.
275 // @param theThis is the context given to that function when it executes. It becomes 'this' inside of that function.
276 function invokeCallback(callback, theThis, parameterOne) {
277 if (callback) {
278 callback.call(theThis, parameterOne);
279 }
280 }
281
282 /**
283 * widget object
284 * Private sub-class of dashboard
285 * Constructor starts
286 */
287 function widget(widget) {
288 // Merge default options with the options defined for this widget.
289 widget = $.extend({}, $.fn.dashboard.widget.defaults, widget);
290
291 /**
292 * Public methods of widget.
293 */
294
295 // Toggles the minimize() & maximize() methods.
296 widget.toggleMinimize = function() {
297 if (widget.minimized) {
298 widget.maximize();
299 }
300 else {
301 widget.minimize();
302 }
303
304 widget.hideSettings();
305 dashboard.saveColumns();
306 };
307 widget.minimize = function() {
308 $('.widget-content', widget.element).slideUp(opts.animationSpeed);
309 $(widget.controls.minimize.element).addClass( 'maximize-icon' );
310 $(widget.controls.minimize.element).removeClass( 'minimize-icon' );
311 widget.minimized = true;
312 };
313 widget.maximize = function() {
314 $('.widget-content', widget.element).slideDown(opts.animationSpeed);
315 $(widget.controls.minimize.element).removeClass( 'maximize-icon' );
316 $(widget.controls.minimize.element).addClass( 'minimize-icon' );
317 widget.minimized = false;
318 };
319
320 // Toggles whether the widget is in settings-display mode or not.
321 widget.toggleSettings = function() {
322 if (widget.settings.displayed) {
323 // Widgets always exit settings into maximized state.
324 widget.maximize();
325 widget.hideSettings();
326 invokeCallback(opts.widgetCallbacks.hideSettings, widget);
327 }
328 else {
329 widget.minimize();
330 widget.showSettings();
331 invokeCallback(opts.widgetCallbacks.showSettings, widget);
332 }
333 };
334 widget.showSettings = function() {
335 if (widget.settings.element) {
336 widget.settings.element.show();
337
338 // Settings are loaded via AJAX. Only execute the script if the settings have been loaded.
339 if (widget.settings.ready) {
340 getJavascript(widget.settings.script);
341 }
342 }
343 else {
344 // Settings have not been initialized. Do so now.
345 initSettings();
346 }
347 widget.settings.displayed = true;
348 };
349 widget.hideSettings = function() {
350 if (widget.settings.element) {
351 widget.settings.element.hide();
352 }
353 widget.settings.displayed = false;
354 };
355 widget.saveSettings = function() {
356 // Build list of parameters to POST to server.
357 var params = {};
358 // serializeArray() returns an array of objects. Process it.
359 var fields = widget.settings.element.serializeArray();
360 for (var i in fields) {
361 var field = fields[i];
362 // Put the values into flat object properties that PHP will parse into an array server-side.
363 // (Unfortunately jQuery doesn't do this)
364 params['settings[' + field.name + ']'] = field.value;
365 }
366
367 // Things get messy here.
31037a42
EM
368 // @todo Refactor to use currentState and targetedState properties to determine what needs
369 // to be done to get to any desired state on any UI or AJAX event – since these don't always
370 // match.
371 // E.g. When a user starts a new UI event before the Ajax event handler from a previous
3d80d622
CW
372 // UI event gets invoked.
373
374 // Hide the settings first of all.
375 widget.toggleSettings();
376 // Save the real settings element so that we can restore the reference later.
377 var settingsElement = widget.settings.element;
378 // Empty the settings form.
379 widget.settings.innerElement.empty();
380 initThrobber();
381 // So that showSettings() and hideSettings() can do SOMETHING, without showing the empty settings form.
382 widget.settings.element = widget.throbber.hide();
383 widget.settings.ready = false;
384
385 // Save the settings to the server.
386 $.extend(params, opts.ajaxCallbacks.widgetSettings.data, { id: widget.id });
387 $.post(opts.ajaxCallbacks.widgetSettings.url, params, function(response, status) {
388 // Merge the response into widget.settings.
389 $.extend(widget.settings, response);
390 // Restore the reference to the real settings element.
391 widget.settings.element = settingsElement;
392 // Make sure the settings form is empty and add the updated settings form.
393 widget.settings.innerElement.empty().append(widget.settings.markup);
394 widget.settings.ready = true;
395
396 // Did the user already jump back into settings-display mode before we could finish reloading the settings form?
397 if (widget.settings.displayed) {
398 // Ooops! We had better take care of hiding the throbber and showing the settings form then.
399 widget.throbber.hide();
400 widget.showSettings();
401 invokeCallback(opts.widgetCallbacks.saveSettings, dashboard);
402 }
403 }, 'json');
404
405 // Don't let form submittal bubble up.
406 return false;
407 };
408
409 widget.enterFullscreen = function() {
410 // Make sure the widget actually supports full screen mode.
411 if (!widget.fullscreenUrl) {
412 return;
413 }
68f3f169 414 CRM.loadPage(widget.fullscreenUrl);
3d80d622 415 };
31037a42 416
3d80d622
CW
417 // Exit fullscreen mode.
418 widget.exitFullscreen = function() {
419 // This is just a wrapper for dashboard.exitFullscreen() which does the heavy lifting.
420 dashboard.exitFullscreen();
421 };
422
423 // Adds controls to a widget. id is for internal use and image file name in images/dashboard/ (a .gif).
424 widget.addControl = function(id, control) {
31037a42 425 var markup = '<a class="widget-icon ' + id + '-icon" alt="' + control.description + '" title="' + control.description + '"></a>';
3d80d622
CW
426 control.element = $(markup).prependTo($('.widget-controls', widget.element)).click(control.callback);
427 };
428
429 // An external method used only by and from external scripts to reload content. Not invoked or used internally.
430 // The widget must provide the script that executes this, as well as the script that invokes it.
431 widget.reloadContent = function() {
432 getJavascript(widget.reloadContentScript);
433 invokeCallback(opts.widgetCallbacks.reloadContent, widget);
434 };
435
436 // Removes the widget from the dashboard, and saves columns.
437 widget.remove = function() {
31037a42 438 if ( confirm( 'Are you sure you want to remove "' + widget.title + '"?') ) {
3d80d622
CW
439 invokeCallback(opts.widgetCallbacks.remove, widget);
440 widget.element.fadeOut(opts.animationSpeed, function() {
441 $(this).remove();
442 dashboard.saveColumns();
443 });
444 }
445 };
446 // End public methods of widget.
447
448 /**
449 * Public properties of widget.
450 */
451
452 // Default controls. External script can add more with widget.addControls()
453 widget.controls = {
454 settings: {
77a8d7f9 455 description: ts('Configure this dashlet'),
3d80d622
CW
456 callback: widget.toggleSettings
457 },
458 minimize: {
77a8d7f9 459 description: ts('Collapse or expand'),
3d80d622
CW
460 callback: widget.toggleMinimize
461 },
462 fullscreen: {
77a8d7f9 463 description: ts('View fullscreen'),
3d80d622
CW
464 callback: widget.enterFullscreen
465 },
466 close: {
77a8d7f9 467 description: ts('Remove from dashboard'),
3d80d622
CW
468 callback: widget.remove
469 }
470 };
471 // End public properties of widget.
472
473 /**
474 * Private properties of widget.
475 */
476
477 // We're gonna 'fork' execution again, so let's tell the user to hold with us till the AJAX callback gets invoked.
478 var throbber = $(opts.throbberMarkup).appendTo(widget.element);
479 var params = $.extend({}, opts.ajaxCallbacks.getWidget.data, {id: widget.id});
480 $.getJSON(opts.ajaxCallbacks.getWidget.url, params, init);
481
482 // Help dashboard track whether we've got any outstanding requests on which initialization is pending.
483 asynchronousRequestCounter++;
484 return widget;
485 // End of private properties of widget.
486
487 /**
488 * Private methods of widget.
489 */
490
491 // Ajax callback for widget initialization.
492 function init(data, status) {
493 asynchronousRequestCounter--;
494 $.extend(widget, data);
495
496 // Delete controls that don't apply to this widget.
497 if (!widget.settings) {
498 delete widget.controls.settings;
499 }
500 if (!widget.fullscreenUrl) {
501 delete widget.controls.fullscreen;
502 }
503
504 widget.element.attr('id', 'widget-' + widget.id).addClass(widget.classes);
505 throbber.remove();
506 // Build and add the widget's DOM element.
507 $(widget.element).append(widgetHTML()).trigger('crmLoad');
508 // Save the content element so that external scripts can reload it easily.
509 widget.contentElement = $('.widget-content', widget.element);
510 $.each(widget.controls, widget.addControl);
511
512 // Switch the initial state so that it initializes to the correct state.
513 widget.minimized = !widget.minimized;
514 widget.toggleMinimize();
515 getJavascript(widget.initScript);
516 invokeCallback(opts.widgetCallbacks.get, widget);
517
518 // completeInit() is a private method of the dashboard. Let it complete initialization of the dashboard.
519 completeInit();
520 }
521
522 // Builds inner HTML for widgets.
523 function widgetHTML() {
524 var html = '';
525 html += '<div class="widget-wrapper">';
526 html += ' <div class="widget-controls"><h3 class="widget-header">' + widget.title + '</h3></div>';
527 html += ' <div class="widget-content crm-ajax-container">' + widget.content + '</div>';
528 html += '</div>';
529 return html;
530 }
531
532 // Initializes a widgets settings pane.
533 function initSettings() {
534 // Overwrite widget.settings (boolean).
535 initThrobber();
536 widget.settings = {
537 element: widget.throbber.show(),
538 ready: false
539 };
540
541 // Get the settings markup and script executables for this widget.
542 var params = $.extend({}, opts.ajaxCallbacks.widgetSettings.data, { id: widget.id });
543 $.getJSON(opts.ajaxCallbacks.widgetSettings.url, params, function(response, status) {
544 $.extend(widget.settings, response);
545 // Build and add the settings form to the DOM. Bind the form's submit event handler/callback.
546 widget.settings.element = $(widgetSettingsHTML()).appendTo($('.widget-wrapper', widget.element)).submit(widget.saveSettings);
547 // Bind the cancel button's event handler too.
548 widget.settings.cancelButton = $('.widget-settings-cancel', widget.settings.element).click(cancelEditSettings);
549 // Build and add the inner form elements from the HTML markup provided in the AJAX data.
550 widget.settings.innerElement = $('.widget-settings-inner', widget.settings.element).append(widget.settings.markup);
551 widget.settings.ready = true;
552
553 if (widget.settings.displayed) {
554 // If the user hasn't clicked away from the settings pane, then display the form.
555 widget.throbber.hide();
556 widget.showSettings();
557 }
558
559 getJavascript(widget.settings.initScript);
560 });
561 }
562
563 // Builds HTML for widget settings forms.
564 function widgetSettingsHTML() {
565 var html = '';
566 html += '<form class="widget-settings">';
567 html += ' <div class="widget-settings-inner"></div>';
568 html += ' <div class="widget-settings-buttons">';
569 html += ' <input id="' + widget.id + '-settings-save" class="widget-settings-save" value="Save" type="submit" />';
570 html += ' <input id="' + widget.id + '-settings-cancel" class="widget-settings-cancel" value="Cancel" type="submit" />';
571 html += ' </div>';
572 html += '</form>';
573 return html;
574 }
575
576 // Initializes a generic widget content throbber, for use by settings form and external scripts.
577 function initThrobber() {
578 if (!widget.throbber) {
579 widget.throbber = $(opts.throbberMarkup).appendTo($('.widget-wrapper', widget.element));
580 }
581 };
582
583 // Event handler/callback for cancel button clicks.
584 // @todo test this gets caught by all browsers when the cancel button is 'clicked' via the keyboard.
585 function cancelEditSettings() {
586 widget.toggleSettings();
587 return false;
588 };
589
590 // Helper function to execute external script on the server.
591 // @todo It would be nice to provide some context to the script. How?
592 function getJavascript(url) {
593 if (url) {
594 $.getScript(url);
595 }
596 }
597 };
598 };
599
600 // Public static properties of dashboard. Default settings.
601 $.fn.dashboard.defaults = {
602 columns: 2,
77a8d7f9
CW
603 emptyPlaceholderInner: ts('There are no dashlets in this column of your dashboard.'),
604 fullscreenHeaderInner: ts('Back to dashboard mode'),
605 throbberMarkup: '<div class="crm-loading-element">' + ts('Loading') + '...</div>',
3d80d622
CW
606 animationSpeed: 200,
607 callbacks: {},
608 widgetCallbacks: {}
609 };
610
611 // Default widget settings.
612 $.fn.dashboard.widget = {
613 defaults: {
614 minimized: false,
615 settings: false,
616 fullscreen: false
617 }
618 };
619})(jQuery);