Merge pull request #431 from xmujay/0609_monitor
[phpmyadmin/aamir.git] / js / ajax.js
blobeab0e842ae7855bdf843a4948fa0eef30380f587
1 /* vim: set expandtab sw=4 ts=4 sts=4: */
2 /**
3  * This object handles ajax requests for pages. It also
4  * handles the reloading of the main menu and scripts.
5  */
6 var AJAX = {
7     /**
8      * @var bool active Whether we are busy
9      */
10     active: false,
11     /**
12      * @var object source The object whose event initialized the request
13      */
14     source: null,
15     /**
16      * @var function Callback to execute after a successful request
17      *               Used by PMA_commonFunctions from common.js
18      */
19     _callback: function () {},
20     /**
21      * @var bool _debug Makes noise in your Firebug console
22      */
23     _debug: false,
24     /**
25      * @var object $msgbox A reference to a jQuery object that links to a message
26      *                     box that is generated by PMA_ajaxShowMessage()
27      */
28     $msgbox: null,
29     /**
30      * Given the filename of a script, returns a hash to be
31      * used to refer to all the events registered for the file
32      *
33      * @param string key The filename for which to get the event name
34      *
35      * @return int
36      */
37     hash: function (key) {
38         /* http://burtleburtle.net/bob/hash/doobs.html#one */
39         key += "";
40         var len = key.length, hash = 0, i = 0;
41         for (; i < len; ++i) {
42             hash += key.charCodeAt(i);
43             hash += (hash << 10);
44             hash ^= (hash >> 6);
45         }
46         hash += (hash << 3);
47         hash ^= (hash >> 11);
48         hash += (hash << 15);
49         return Math.abs(hash);
50     },
51     /**
52      * Registers an onload event for a file
53      *
54      * @param string   file The filename for which to register the event
55      * @param function func The function to execute when the page is ready
56      *
57      * @return self For chaining
58      */
59     registerOnload: function (file, func) {
60         var eventName = 'onload_' + AJAX.hash(file);
61         $(document).bind(eventName, func);
62         this._debug && console.log(
63             // no need to translate
64             "Registered event " + eventName + " for file " + file
65         );
66         return this;
67     },
68     /**
69      * Registers a teardown event for a file. This is useful to execute functions
70      * that unbind events for page elements that are about to be removed.
71      *
72      * @param string   file The filename for which to register the event
73      * @param function func The function to execute when
74      *                      the page is about to be torn down
75      *
76      * @return self For chaining
77      */
78     registerTeardown: function (file, func) {
79         var eventName = 'teardown_' + AJAX.hash(file);
80         $(document).bind(eventName, func);
81         this._debug && console.log(
82             // no need to translate
83             "Registered event " + eventName + " for file " + file
84         );
85         return this;
86     },
87     /**
88      * Called when a page has finished loading, once for every
89      * file that registered to the onload event of that file.
90      *
91      * @param string file The filename for which to fire the event
92      *
93      * @return void
94      */
95     fireOnload: function (file) {
96         var eventName = 'onload_' + AJAX.hash(file);
97         $(document).trigger(eventName);
98         this._debug && console.log(
99             // no need to translate
100             "Fired event " + eventName + " for file " + file
101         );
102     },
103     /**
104      * Called just before a page is torn down, once for every
105      * file that registered to the teardown event of that file.
106      *
107      * @param string file The filename for which to fire the event
108      *
109      * @return void
110      */
111     fireTeardown: function (file) {
112         var eventName = 'teardown_' + AJAX.hash(file);
113         $(document).triggerHandler(eventName);
114         this._debug && console.log(
115             // no need to translate
116             "Fired event " + eventName + " for file " + file
117         );
118     },
119     /**
120      * Event handler for clicks on links and form submissions
121      *
122      * @param object e Event data
123      *
124      * @return void
125      */
126     requestHandler: function (event) {
127         // In some cases we don't want to handle the request here and either
128         // leave the browser deal with it natively (e.g: file download)
129         // or leave an existing ajax event handler present elsewhere deal with it
130         var href = $(this).attr('href');
131         if (event.shiftKey || event.ctrlKey) {
132             return true;
133         } else if ($(this).attr('target')) {
134             return true;
135         } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
136             return true;
137         } else if (href && href.match(/^#/)) {
138             return true;
139         } else if (href && href.match(/^mailto/)) {
140             return true;
141         } else if ($(this).hasClass('ui-datepicker-next') ||
142                    $(this).hasClass('ui-datepicker-prev')
143                   ) {
144             return true;
145         }
147         if (typeof event != 'undefined') {
148             event.preventDefault();
149             event.stopImmediatePropagation();
150         }
151         if (AJAX.active === true) {
152             // Silently bail out, there is already a request in progress.
153             // TODO: save a reference to the request and cancel the old request
154             // when the user requests something else. Something like this is
155             // already implemented in the PMA_fastFilter object in navigation.js
156             return false;
157         }
159         AJAX.source = $(this);
161         $('html, body').animate({scrollTop: 0}, 'fast');
163         var isLink = !! href || false;
164         var url = isLink ? href : $(this).attr('action');
165         var params = 'ajax_request=true&ajax_page_request=true';
166         if (! isLink) {
167             params += '&' + $(this).serialize();
168         }
169         // Add a list of menu hashes that we have in the cache to the request
170         params += AJAX.cache.menus.getRequestParam();
172         AJAX._debug && console.log("Loading: " + url); // no need to translate
174         if (isLink) {
175             AJAX.active = true;
176             AJAX.$msgbox = PMA_ajaxShowMessage();
177             $.get(url, params, AJAX.responseHandler);
178         } else {
179             /**
180              * Manually fire the onsubmit event for the form, if any.
181              * The event was saved in the jQuery data object by an onload
182              * handler defined below. Workaround for bug #3583316
183              */
184             var onsubmit = $(this).data('onsubmit');
185             // Submit the request if there is no onsubmit handler
186             // or if it returns a value that evaluates to true
187             if (typeof onsubmit !== 'function' || onsubmit.apply(this, [event])) {
188                 AJAX.active = true;
189                 AJAX.$msgbox = PMA_ajaxShowMessage();
190                 $.post(url, params, AJAX.responseHandler);
191             }
192         }
193     },
194     /**
195      * Called after the request that was initiated by this.requestHandler()
196      * has completed successfully or with a caught error. For completely
197      * failed requests or requests with uncaught errors, see the .ajaxError
198      * handler at the bottom of this file.
199      *
200      * To refer to self use 'AJAX', instead of 'this' as this function
201      * is called in the jQuery context.
202      *
203      * @param object e Event data
204      *
205      * @return void
206      */
207     responseHandler: function (data) {
208         if (data.success) {
209             $table_clone = false;
210             PMA_ajaxRemoveMessage(AJAX.$msgbox);
212             if (data._redirect) {
213                 PMA_ajaxShowMessage(data._redirect, false);
214                 AJAX.active = false;
215                 return;
216             }
218             AJAX.scriptHandler.reset(function () {
219                 if (data._reloadNavigation) {
220                     PMA_reloadNavigation();
221                 }
222                 if (data._reloadQuerywindow) {
223                     var params = data._reloadQuerywindow;
224                     PMA_querywindow.reload(
225                         params.db,
226                         params.table,
227                         params.sql_query
228                     );
229                 }
230                 if (data._focusQuerywindow) {
231                     PMA_querywindow.focus(
232                         data._focusQuerywindow
233                     );
234                 }
235                 if (data._title) {
236                     $('title').replaceWith(data._title);
237                 }
238                 if (data._menu) {
239                     AJAX.cache.menus.replace(data._menu);
240                     AJAX.cache.menus.add(data._menuHash, data._menu);
241                 } else if (data._menuHash) {
242                     AJAX.cache.menus.replace(AJAX.cache.menus.get(data._menuHash));
243                 }
245                 // Remove all containers that may have
246                 // been added outside of #page_content
247                 $('body').children()
248                     .not('#pma_navigation')
249                     .not('#floating_menubar')
250                     .not('#goto_pagetop')
251                     .not('#page_content')
252                     .not('#selflink')
253                     .not('#session_debug')
254                     .remove();
255                 // Replace #page_content with new content
256                 if (data.message && data.message.length > 0) {
257                     $('#page_content').replaceWith(
258                         "<div id='page_content'>" + data.message + "</div>"
259                     );
260                     PMA_highlightSQL($('#page_content'));
261                 }
263                 if (data._selflink) {
264                     $('#selflink > a').attr('href', data._selflink);
265                 }
266                 if (data._scripts) {
267                     AJAX.scriptHandler.load(data._scripts);
268                 }
269                 if (data._selflink && data._scripts && data._menuHash && data._params) {
270                     AJAX.cache.add(
271                         data._selflink,
272                         data._scripts,
273                         data._menuHash,
274                         data._params,
275                         AJAX.source.attr('rel')
276                     );
277                 }
278                 if (data._params) {
279                     PMA_commonParams.setAll(data._params);
280                 }
281                 if (data._displayMessage) {
282                     $('#page_content').prepend(data._displayMessage);
283                     PMA_highlightSQL($('#page_content'));
284                 }
286                 $('#pma_errors').remove();
287                 if (data._errors) {
288                     $('<div/>', {id : 'pma_errors'})
289                         .insertAfter('#selflink')
290                         .append(data._errors);
291                 }
293                 if (typeof AJAX._callback === 'function') {
294                     AJAX._callback.call();
295                 }
296                 AJAX._callback = function () {};
297             });
298         } else {
299             PMA_ajaxShowMessage(data.error, false);
300             AJAX.active = false;
301         }
302     },
303     /**
304      * This object is in charge of downloading scripts,
305      * keeping track of what's downloaded and firing
306      * the onload event for them when the page is ready.
307      */
308     scriptHandler: {
309         /**
310          * @var array _scripts The list of files already downloaded
311          */
312         _scripts: [],
313         /**
314          * @var array _scriptsToBeLoaded The list of files that
315          *                               need to be downloaded
316          */
317         _scriptsToBeLoaded: [],
318         /**
319          * @var array _scriptsToBeFired The list of files for which
320          *                              to fire the onload event
321          */
322         _scriptsToBeFired: [],
323         /**
324          * Records that a file has been downloaded
325          *
326          * @param string file The filename
327          * @param string fire Whether this file will be registering
328          *                    onload/teardown events
329          *
330          * @return self For chaining
331          */
332         add: function (file, fire) {
333             this._scripts.push(file);
334             if (fire) {
335                 // Record whether to fire any events for the file
336                 // This is necessary to correctly tear down the initial page
337                 this._scriptsToBeFired.push(file);
338             }
339             return this;
340         },
341         /**
342          * Download a list of js files in one request
343          *
344          * @param array files An array of filenames and flags
345          *
346          * @return void
347          */
348         load: function (files) {
349             var self = this;
350             self._scriptsToBeLoaded = [];
351             self._scriptsToBeFired = [];
352             for (var i in files) {
353                 self._scriptsToBeLoaded.push(files[i].name);
354                 if (files[i].fire) {
355                     self._scriptsToBeFired.push(files[i].name);
356                 }
357             }
358             // Generate a request string
359             var request = [];
360             var needRequest = false;
361             for (var index in self._scriptsToBeLoaded) {
362                 var script = self._scriptsToBeLoaded[index];
363                 // Only for scripts that we don't already have
364                 if ($.inArray(script, self._scripts) == -1) {
365                     needRequest = true;
366                     this.add(script);
367                     request.push("scripts[]=" + script);
368                 }
369             }
370             // Download the composite js file, if necessary
371             if (needRequest) {
372                 $.ajax({
373                     url: "js/get_scripts.js.php?" + request.join("&"),
374                     cache: true,
375                     success: function () {
376                         self.done();
377                     },
378                     dataType: "script"
379                 });
380             } else {
381                 self.done();
382             }
383         },
384         /**
385          * Called whenever all files are loaded
386          *
387          * @return void
388          */
389         done: function () {
390             for (var i in this._scriptsToBeFired) {
391                 AJAX.fireOnload(this._scriptsToBeFired[i]);
392             }
393             AJAX.active = false;
394         },
395         /**
396          * Fires all the teardown event handlers for the current page
397          * and rebinds all forms and links to the request handler
398          *
399          * @param function callback The callback to call after resetting
400          *
401          * @return void
402          */
403         reset: function (callback) {
404             for (var i in this._scriptsToBeFired) {
405                 AJAX.fireTeardown(this._scriptsToBeFired[i]);
406             }
407             this._scriptsToBeFired = [];
408             /**
409              * Re-attach a generic event handler to clicks
410              * on pages and submissions of forms
411              */
412             $('a').die('click').live('click', AJAX.requestHandler);
413             $('form').die('submit').live('submit', AJAX.requestHandler);
414             AJAX.cache.update();
415             callback();
416         }
417     }
421  * Here we register a function that will remove the onsubmit event from all
422  * forms that will be handled by the generic page loader. We then save this
423  * event handler in the "jQuery data", so that we can fire it up later in
424  * AJAX.requestHandler().
426  * See bug #3583316
427  */
428 AJAX.registerOnload('functions.js', function () {
429     // Registering the onload event for functions.js
430     // ensures that it will be fired for all pages
431     $('form').not('.ajax').not('.disableAjax').each(function () {
432         if ($(this).attr('onsubmit')) {
433             $(this).data('onsubmit', this.onsubmit).attr('onsubmit', '');
434         }
435     });
439  * An implementation of a client-side page cache.
440  * This object also uses the cache to provide a simple microhistory,
441  * that is the ability to use the back and forward buttons in the browser
442  */
443 AJAX.cache = {
444     /**
445      * @var int The maximum number of pages to keep in the cache
446      */
447     MAX: 6,
448     /**
449      * @var object A hash used to prime the cache with data about the initially
450      *             loaded page. This is set in the footer, and then loaded
451      *             by a double-queued event further down this file.
452      */
453     primer: {},
454     /**
455      * @var array Stores the content of the cached pages
456      */
457     pages: [],
458     /**
459      * @var int The index of the currently loaded page
460      *          This is used to know at which point in the history we are
461      */
462     current: 0,
463     /**
464      * Saves a new page in the cache
465      *
466      * @param string hash    The hash part of the url that is being loaded
467      * @param array  scripts A list of scripts that is requured for the page
468      * @param string menu    A hash that links to a menu stored
469      *                       in a dedicated menu cache
470      * @param array  params  A list of parameters used by PMA_commonParams()
471      * @param string rel     A relationship to the current page:
472      *                       'samepage': Forces the response to be treated as
473      *                                   the same page as the current one
474      *                       'newpage':  Forces the response to be treated as
475      *                                   a new page
476      *                       undefined:  Default behaviour, 'samepage' if the
477      *                                   selflinks of the two pages are the same.
478      *                                   'newpage' otherwise
479      *
480      * @return void
481      */
482     add: function (hash, scripts, menu, params, rel) {
483         if (this.pages.length > AJAX.cache.MAX) {
484             // Trim the cache, to the maximum number of allowed entries
485             // This way we will have a cached menu for every page
486             for (var i = 0; i < this.pages.length - this.MAX; i++) {
487                 delete this.pages[i];
488             }
489         }
490         while (this.current < this.pages.length) {
491             // trim the cache if we went back in the history
492             // and are now going forward again
493             this.pages.pop();
494         }
495         if (rel === 'newpage' ||
496             (
497                 typeof rel === 'undefined' && (
498                     typeof this.pages[this.current - 1] === 'undefined'
499                     ||
500                     this.pages[this.current - 1].hash !== hash
501                 )
502             )
503         ) {
504             this.pages.push({
505                 hash: hash,
506                 content: $('#page_content').html(),
507                 scripts: scripts,
508                 selflink: $('#selflink').html(),
509                 menu: menu,
510                 params: params
511             });
512             AJAX.setUrlHash(this.current, hash);
513             this.current++;
514         }
515     },
516     /**
517      * Restores a page from the cache. This is called when the hash
518      * part of the url changes and it's structure appears to be valid
519      *
520      * @param string index Which page from the history to load
521      *
522      * @return void
523      */
524     navigate: function (index) {
525         if (typeof this.pages[index] === 'undefined') {
526             PMA_ajaxShowMessage(
527                 '<div class="error">' + PMA_messages.strInvalidPage + '</div>',
528                 false
529             );
530         } else {
531             AJAX.active = true;
532             var record = this.pages[index];
533             AJAX.scriptHandler.reset(function () {
534                 $('#page_content').html(record.content);
535                 $('#selflink').html(record.selflink);
536                 AJAX.cache.menus.replace(AJAX.cache.menus.get(record.menu));
537                 PMA_commonParams.setAll(record.params);
538                 AJAX.scriptHandler.load(record.scripts);
539                 AJAX.cache.current = ++index;
540             });
541         }
542     },
543     /**
544      * Resaves the content of the current page in the cache.
545      * Necessary in order not to show the user some outdated version of the page
546      *
547      * @return void
548      */
549     update: function () {
550         var page = this.pages[this.current - 1];
551         if (page) {
552             page.content = $('#page_content').html();
553         }
554     },
555     /**
556      * @var object Dedicated menu cache
557      */
558     menus: {
559         /**
560          * Returns the number of items in an associative array
561          *
562          * @return int
563          */
564         size: function (obj) {
565             var size = 0, key;
566             for (key in obj) {
567                 if (obj.hasOwnProperty(key)) {
568                     size++;
569                 }
570             }
571             return size;
572         },
573         /**
574          * @var hash Stores the content of the cached menus
575          */
576         data: {},
577         /**
578          * Saves a new menu in the cache
579          *
580          * @param string hash    The hash (trimmed md5) of the menu to be saved
581          * @param string content The HTML code of the menu to be saved
582          *
583          * @return void
584          */
585         add: function (hash, content) {
586             if (this.size(this.data) > AJAX.cache.MAX) {
587                 // when the cache grows, we remove the oldest entry
588                 var oldest, key, init = 0;
589                 for (var i in this.data) {
590                     if (this.data[i]) {
591                         if (! init || this.data[i].timestamp.getTime() < oldest.getTime()) {
592                             oldest = this.data[i].timestamp;
593                             key = i;
594                             init = 1;
595                         }
596                     }
597                 }
598                 delete this.data[key];
599             }
600             this.data[hash] = {
601                 content: content,
602                 timestamp: new Date()
603             };
604         },
605         /**
606          * Retrieves a menu given its hash
607          *
608          * @param string hash The hash of the menu to be retrieved
609          *
610          * @return string
611          */
612         get: function (hash) {
613             if (this.data[hash]) {
614                 return this.data[hash].content;
615             } else {
616                 // This should never happen as long as the number of stored menus
617                 // is larger or equal to the number of pages in the page cache
618                 return '';
619             }
620         },
621         /**
622          * Prepares part of the parameter string used during page requests,
623          * this is necessary to tell the server which menus we have in the cache
624          *
625          * @return string
626          */
627         getRequestParam: function () {
628             var param = '';
629             var menuHashes = [];
630             for (var i in this.data) {
631                 menuHashes.push(i);
632             }
633             var menuHashesParam = menuHashes.join('-');
634             if (menuHashesParam) {
635                 param = '&menuHashes=' + menuHashesParam;
636             }
637             return param;
638         },
639         /**
640          * Replaces the menu with new content
641          *
642          * @return void
643          */
644         replace: function (content) {
645             $('#floating_menubar').html(content)
646                 // Remove duplicate wrapper
647                 // TODO: don't send it in the response
648                 .children().first().remove();
649             $('#topmenu').menuResizer(PMA_mainMenuResizerCallback);
650         }
651     }
655  * URL hash management module.
656  * Allows direct bookmarking and microhistory.
657  */
658 AJAX.setUrlHash = (function (jQuery, window) {
659     "use strict";
660     /**
661      * Indictaes whether we have already completed
662      * the initialisation of the hash
663      *
664      * @access private
665      */
666     var ready = false;
667     /**
668      * Stores a hash that needed to be set when we were not ready
669      *
670      * @access private
671      */
672     var savedHash = "";
673     /**
674      * Flag to indicate if the change of hash was triggered
675      * by a user pressing the back/forward button or if
676      * the change was triggered internally
677      *
678      * @access private
679      */
680     var userChange = true;
682     // Fix favicon disappearing in Firefox when setting location.hash
683     function resetFavicon() {
684         if (jQuery.browser.mozilla) {
685             // Move the link tags for the favicon to the bottom
686             // of the head element to force a reload of the favicon
687             $('head > link[href=favicon\\.ico]').appendTo('head');
688         }
689     }
691     /**
692      * Sets the hash part of the URL
693      *
694      * @access public
695      */
696     function setUrlHash(index, hash) {
697         /*
698          * Known problem:
699          * Setting hash leads to reload in webkit:
700          * http://www.quirksmode.org/bugreports/archives/2005/05/Safari_13_visual_anomaly_with_windowlocationhref.html
701          *
702          * so we expect that users are not running an ancient Safari version
703          */
705         userChange = false;
706         if (ready) {
707             window.location.hash = "PMAURL-" + index + ":" + hash;
708             resetFavicon();
709         } else {
710             savedHash = "PMAURL-" + index + ":" + hash;
711         }
712     }
713     /**
714      * Start initialisation
715      */
716     if (window.location.hash.substring(0, 8) == '#PMAURL-') {
717         // We have a valid hash, let's redirect the user
718         // to the page that it's pointing to
719         window.location = window.location.hash.substring(
720             window.location.hash.indexOf(':') + 1
721         );
722     } else {
723         // We don't have a valid hash, so we'll set it up
724         // when the page finishes loading
725         jQuery(function () {
726             /* Check if we should set URL */
727             if (savedHash !== "") {
728                 window.location.hash = savedHash;
729                 savedHash = "";
730                 resetFavicon();
731             }
732             // Indicate that we're done initialising
733             ready = true;
734         });
735     }
736     /**
737      * Register an event handler for when the url hash changes
738      */
739     jQuery(function () {
740         jQuery(window).hashchange(function () {
741             if (userChange === false) {
742                 // Ignore internally triggered hash changes
743                 userChange = true;
744             } else if (/^#PMAURL-\d+:/.test(window.location.hash)) {
745                 // Change page if the hash changed was triggered by a user action
746                 var index = window.location.hash.substring(
747                     8, window.location.hash.indexOf(':')
748                 );
749                 AJAX.cache.navigate(index);
750             }
751         });
752     });
753     /**
754      * Publicly exposes a reference to the otherwise private setUrlHash function
755      */
756     return setUrlHash;
757 })(jQuery, window);
760  * Page load event handler
761  */
762 $(function () {
763     // Add the menu from the initial page into the cache
764     // The cache primer is set by the footer class
765     if (AJAX.cache.primer.url) {
766         AJAX.cache.menus.add(
767             AJAX.cache.primer.menuHash,
768             $('<div></div>')
769                 .append('<div></div>')
770                 .append($('#serverinfo').clone())
771                 .append($('#topmenucontainer').clone())
772                 .html()
773         );
774     }
775     $(function () {
776         // Queue up this event twice to make sure that we get a copy
777         // of the page after all other onload events have been fired
778         if (AJAX.cache.primer.url) {
779             AJAX.cache.add(
780                 AJAX.cache.primer.url,
781                 AJAX.cache.primer.scripts,
782                 AJAX.cache.primer.menuHash
783             );
784         }
785     });
789  * Attach a generic event handler to clicks
790  * on pages and submissions of forms
791  */
792 $('a').live('click', AJAX.requestHandler);
793 $('form').live('submit', AJAX.requestHandler);
796  * Gracefully handle fatal server errors
797  * (e.g: 500 - Internal server error)
798  */
799 $(document).ajaxError(function (event, request, settings) {
800     if (request.status !== 0) { // Don't handle aborted requests
801         var errorCode = $.sprintf(PMA_messages.strErrorCode, request.status);
802         var errorText = $.sprintf(PMA_messages.strErrorText, request.statusText);
803         PMA_ajaxShowMessage(
804             '<div class="error">'
805             + PMA_messages.strErrorProcessingRequest
806             + '<div>' + errorCode + '</div>'
807             + '<div>' + errorText + '</div>'
808             + '</div>',
809             false
810         );
811         AJAX.active = false;
812     }