commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-old / civicrm / packages / jquery / plugins / jquery.dashboard.js
1 /**
2 +--------------------------------------------------------------------+
3 | CiviCRM version 4.2 |
4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC (c) 2004-2012 |
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 */
149
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 };
185
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 });
199
200 //set empty Dashboard to false
201 emptyDashboard = false;
202 }
203 }
204
205 if ( emptyDashboard ) {
206 emptyDashboardCondition( );
207 }
208
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 }
217
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.)
233 placeholder: 'placeholder',
234 activate: function(event, ui) {
235 var h= cj(ui.item).height();
236 $('.placeholder').css('height', h +'px'); },
237
238 opacity: 0.2,
239
240 // Maks sure that only widgets are sortable, and not empty placeholders.
241 items: '> .widget',
242
243 forcePlaceholderSize: true,
244
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.
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
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 }
414
415 $('<div id="crm-dashlet-container"></div>')
416 .html('<div class="crm-container"><div id="crm-dashlet-fullscreen-content">Loading...</div></div>')
417 .dialog({
418 autoOpen: true,
419 title: widget.title,
420 modal: true,
421 height: 'auto',
422 width: 'auto',
423 position: [100,125],
424 close: function(event, ui) {
425 cj(this).dialog("destroy");
426 $('#crm-dashlet-container').remove();
427 $('#crm-dashlet-fullscreen-content').remove();
428 }
429 });
430
431 $.ajax({
432 url: widget.fullscreenUrl,
433 success: function ( content ) {
434 $('#crm-dashlet-fullscreen-content').html(content).crmAccordions();
435 }
436 });
437 };
438
439 // Exit fullscreen mode.
440 widget.exitFullscreen = function() {
441 // This is just a wrapper for dashboard.exitFullscreen() which does the heavy lifting.
442 dashboard.exitFullscreen();
443 };
444
445 // Adds controls to a widget. id is for internal use and image file name in images/dashboard/ (a .gif).
446 widget.addControl = function(id, control) {
447 var markup = '<a class="widget-icon ' + id + '-icon" alt="' + control.description + '" title="' + control.description + '"></a>';
448 control.element = $(markup).prependTo($('.widget-controls', widget.element)).click(control.callback);
449 };
450
451 // An external method used only by and from external scripts to reload content. Not invoked or used internally.
452 // The widget must provide the script that executes this, as well as the script that invokes it.
453 widget.reloadContent = function() {
454 getJavascript(widget.reloadContentScript);
455 invokeCallback(opts.widgetCallbacks.reloadContent, widget);
456 };
457
458 // Removes the widget from the dashboard, and saves columns.
459 widget.remove = function() {
460 if ( confirm( 'Are you sure you want to remove "' + widget.title + '"?') ) {
461 invokeCallback(opts.widgetCallbacks.remove, widget);
462 widget.element.fadeOut(opts.animationSpeed, function() {
463 $(this).remove();
464 dashboard.saveColumns();
465 });
466 }
467 };
468 // End public methods of widget.
469
470 /**
471 * Public properties of widget.
472 */
473
474 // Default controls. External script can add more with widget.addControls()
475 widget.controls = {
476 settings: {
477 description: 'Configure this dashlet',
478 callback: widget.toggleSettings
479 },
480 minimize: {
481 description: 'Collapse or expand this dashlet',
482 callback: widget.toggleMinimize
483 },
484 fullscreen: {
485 description: 'View this dashlet in full screen mode',
486 callback: widget.enterFullscreen
487 },
488 close: {
489 description: 'Remove this dashlet from your dashboard',
490 callback: widget.remove
491 }
492 };
493 // End public properties of widget.
494
495 /**
496 * Private properties of widget.
497 */
498
499 // We're gonna 'fork' execution again, so let's tell the user to hold with us till the AJAX callback gets invoked.
500 var throbber = $(opts.throbberMarkup).appendTo(widget.element);
501 var params = $.extend({}, opts.ajaxCallbacks.getWidget.data, {id: widget.id});
502 $.getJSON(opts.ajaxCallbacks.getWidget.url, params, init);
503
504 // Help dashboard track whether we've got any outstanding requests on which initialization is pending.
505 asynchronousRequestCounter++;
506 return widget;
507 // End of private properties of widget.
508
509 /**
510 * Private methods of widget.
511 */
512
513 // Ajax callback for widget initialization.
514 function init(data, status) {
515 asynchronousRequestCounter--;
516 $.extend(widget, data);
517
518 // Delete controls that don't apply to this widget.
519 if (!widget.settings) {
520 delete widget.controls.settings;
521 }
522 if (!widget.fullscreenUrl) {
523 delete widget.controls.fullscreen;
524 }
525
526 widget.element.attr('id', 'widget-' + widget.id).addClass(widget.classes);
527 throbber.remove();
528 // Build and add the widget's DOM element.
529 $(widgetHTML()).appendTo(widget.element);
530 // Save the content element so that external scripts can reload it easily.
531 widget.contentElement = $('.widget-content', widget.element);
532 $.each(widget.controls, widget.addControl);
533
534 // Switch the initial state so that it initializes to the correct state.
535 widget.minimized = !widget.minimized;
536 widget.toggleMinimize();
537 getJavascript(widget.initScript);
538 invokeCallback(opts.widgetCallbacks.get, widget);
539
540 // completeInit() is a private method of the dashboard. Let it complete initialization of the dashboard.
541 completeInit();
542 }
543
544 // Builds inner HTML for widgets.
545 function widgetHTML() {
546 var html = '';
547 html += '<div class="widget-wrapper">';
548 html += ' <div class="widget-controls"><h3 class="widget-header">' + widget.title + '</h3></div>';
549 html += ' <div class="widget-content">' + widget.content + '</div>';
550 html += '</div>';
551 return html;
552 }
553
554 // Initializes a widgets settings pane.
555 function initSettings() {
556 // Overwrite widget.settings (boolean).
557 initThrobber();
558 widget.settings = {
559 element: widget.throbber.show(),
560 ready: false
561 };
562
563 // Get the settings markup and script executables for this widget.
564 var params = $.extend({}, opts.ajaxCallbacks.widgetSettings.data, { id: widget.id });
565 $.getJSON(opts.ajaxCallbacks.widgetSettings.url, params, function(response, status) {
566 $.extend(widget.settings, response);
567 // Build and add the settings form to the DOM. Bind the form's submit event handler/callback.
568 widget.settings.element = $(widgetSettingsHTML()).appendTo($('.widget-wrapper', widget.element)).submit(widget.saveSettings);
569 // Bind the cancel button's event handler too.
570 widget.settings.cancelButton = $('.widget-settings-cancel', widget.settings.element).click(cancelEditSettings);
571 // Build and add the inner form elements from the HTML markup provided in the AJAX data.
572 widget.settings.innerElement = $('.widget-settings-inner', widget.settings.element).append(widget.settings.markup);
573 widget.settings.ready = true;
574
575 if (widget.settings.displayed) {
576 // If the user hasn't clicked away from the settings pane, then display the form.
577 widget.throbber.hide();
578 widget.showSettings();
579 }
580
581 getJavascript(widget.settings.initScript);
582 });
583 }
584
585 // Builds HTML for widget settings forms.
586 function widgetSettingsHTML() {
587 var html = '';
588 html += '<form class="widget-settings">';
589 html += ' <div class="widget-settings-inner"></div>';
590 html += ' <div class="widget-settings-buttons">';
591 html += ' <input id="' + widget.id + '-settings-save" class="widget-settings-save" value="Save" type="submit" />';
592 html += ' <input id="' + widget.id + '-settings-cancel" class="widget-settings-cancel" value="Cancel" type="submit" />';
593 html += ' </div>';
594 html += '</form>';
595 return html;
596 }
597
598 // Initializes a generic widget content throbber, for use by settings form and external scripts.
599 function initThrobber() {
600 if (!widget.throbber) {
601 widget.throbber = $(opts.throbberMarkup).appendTo($('.widget-wrapper', widget.element));
602 }
603 };
604
605 // Event handler/callback for cancel button clicks.
606 // @todo test this gets caught by all browsers when the cancel button is 'clicked' via the keyboard.
607 function cancelEditSettings() {
608 widget.toggleSettings();
609 return false;
610 };
611
612 // Helper function to execute external script on the server.
613 // @todo It would be nice to provide some context to the script. How?
614 function getJavascript(url) {
615 if (url) {
616 $.getScript(url);
617 }
618 }
619 };
620 };
621
622 // Public static properties of dashboard. Default settings.
623 $.fn.dashboard.defaults = {
624 columns: 2,
625 emptyPlaceholderInner: 'There are no dashlets in this column of your dashboard.',
626 fullscreenHeaderInner: 'Back to dashboard mode',
627 throbberMarkup: '<div class="crm-loading-element">Loading...</div>',
628 animationSpeed: 200,
629 callbacks: {},
630 widgetCallbacks: {}
631 };
632
633 // Default widget settings.
634 $.fn.dashboard.widget = {
635 defaults: {
636 minimized: false,
637 settings: false,
638 fullscreen: false
639 }
640 };
641 })(jQuery);