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