1 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 * This object handles ajax requests for pages. It also
4 * handles the reloading of the main menu and scripts.
8 * @var bool active Whether we are busy
12 * @var object source The object whose event initialized the request
16 * @var function Callback to execute after a successful request
17 * Used by PMA_commonFunctions from common.js
19 _callback: function () {},
21 * @var bool _debug Makes noise in your Firebug console
25 * @var object $msgbox A reference to a jQuery object that links to a message
26 * box that is generated by PMA_ajaxShowMessage()
30 * Given the filename of a script, returns a hash to be
31 * used to refer to all the events registered for the file
33 * @param string key The filename for which to get the event name
37 hash: function (key) {
38 /* http://burtleburtle.net/bob/hash/doobs.html#one */
40 var len = key.length, hash = 0, i = 0;
41 for (; i < len; ++i) {
42 hash += key.charCodeAt(i);
49 return Math.abs(hash);
52 * Registers an onload event for a file
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
57 * @return self For chaining
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
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.
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
76 * @return self For chaining
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
88 * Called when a page has finished loading, once for every
89 * file that registered to the onload event of that file.
91 * @param string file The filename for which to fire the event
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
104 * Called just before a page is torn down, once for every
105 * file that registered to the teardown event of that file.
107 * @param string file The filename for which to fire the event
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
120 * Event handler for clicks on links and form submissions
122 * @param object e Event data
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) {
133 } else if ($(this).attr('target')) {
135 } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
137 } else if (href && href.match(/^#/)) {
139 } else if (href && href.match(/^mailto/)) {
141 } else if ($(this).hasClass('ui-datepicker-next') ||
142 $(this).hasClass('ui-datepicker-prev')
147 if (typeof event != 'undefined') {
148 event.preventDefault();
149 event.stopImmediatePropagation();
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
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';
167 params += '&' + $(this).serialize();
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
176 AJAX.$msgbox = PMA_ajaxShowMessage();
177 $.get(url, params, AJAX.responseHandler);
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
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])) {
189 AJAX.$msgbox = PMA_ajaxShowMessage();
190 $.post(url, params, AJAX.responseHandler);
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.
200 * To refer to self use 'AJAX', instead of 'this' as this function
201 * is called in the jQuery context.
203 * @param object e Event data
207 responseHandler: function (data) {
209 $table_clone = false;
210 PMA_ajaxRemoveMessage(AJAX.$msgbox);
212 if (data._redirect) {
213 PMA_ajaxShowMessage(data._redirect, false);
218 AJAX.scriptHandler.reset(function () {
219 if (data._reloadNavigation) {
220 PMA_reloadNavigation();
222 if (data._reloadQuerywindow) {
223 var params = data._reloadQuerywindow;
224 PMA_querywindow.reload(
230 if (data._focusQuerywindow) {
231 PMA_querywindow.focus(
232 data._focusQuerywindow
236 $('title').replaceWith(data._title);
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));
245 // Remove all containers that may have
246 // been added outside of #page_content
248 .not('#pma_navigation')
249 .not('#floating_menubar')
250 .not('#goto_pagetop')
251 .not('#page_content')
253 .not('#session_debug')
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>"
260 PMA_highlightSQL($('#page_content'));
263 if (data._selflink) {
264 $('#selflink > a').attr('href', data._selflink);
267 AJAX.scriptHandler.load(data._scripts);
269 if (data._selflink && data._scripts && data._menuHash && data._params) {
275 AJAX.source.attr('rel')
279 PMA_commonParams.setAll(data._params);
281 if (data._displayMessage) {
282 $('#page_content').prepend(data._displayMessage);
283 PMA_highlightSQL($('#page_content'));
286 $('#pma_errors').remove();
288 $('<div/>', {id : 'pma_errors'})
289 .insertAfter('#selflink')
290 .append(data._errors);
293 if (typeof AJAX._callback === 'function') {
294 AJAX._callback.call();
296 AJAX._callback = function () {};
299 PMA_ajaxShowMessage(data.error, false);
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.
310 * @var array _scripts The list of files already downloaded
314 * @var array _scriptsToBeLoaded The list of files that
315 * need to be downloaded
317 _scriptsToBeLoaded: [],
319 * @var array _scriptsToBeFired The list of files for which
320 * to fire the onload event
322 _scriptsToBeFired: [],
324 * Records that a file has been downloaded
326 * @param string file The filename
327 * @param string fire Whether this file will be registering
328 * onload/teardown events
330 * @return self For chaining
332 add: function (file, fire) {
333 this._scripts.push(file);
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);
342 * Download a list of js files in one request
344 * @param array files An array of filenames and flags
348 load: function (files) {
350 self._scriptsToBeLoaded = [];
351 self._scriptsToBeFired = [];
352 for (var i in files) {
353 self._scriptsToBeLoaded.push(files[i].name);
355 self._scriptsToBeFired.push(files[i].name);
358 // Generate a request string
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) {
367 request.push("scripts[]=" + script);
370 // Download the composite js file, if necessary
373 url: "js/get_scripts.js.php?" + request.join("&"),
375 success: function () {
385 * Called whenever all files are loaded
390 for (var i in this._scriptsToBeFired) {
391 AJAX.fireOnload(this._scriptsToBeFired[i]);
396 * Fires all the teardown event handlers for the current page
397 * and rebinds all forms and links to the request handler
399 * @param function callback The callback to call after resetting
403 reset: function (callback) {
404 for (var i in this._scriptsToBeFired) {
405 AJAX.fireTeardown(this._scriptsToBeFired[i]);
407 this._scriptsToBeFired = [];
409 * Re-attach a generic event handler to clicks
410 * on pages and submissions of forms
412 $('a').die('click').live('click', AJAX.requestHandler);
413 $('form').die('submit').live('submit', AJAX.requestHandler);
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().
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', '');
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
445 * @var int The maximum number of pages to keep in the cache
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.
455 * @var array Stores the content of the cached pages
459 * @var int The index of the currently loaded page
460 * This is used to know at which point in the history we are
464 * Saves a new page in the cache
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
476 * undefined: Default behaviour, 'samepage' if the
477 * selflinks of the two pages are the same.
478 * 'newpage' otherwise
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];
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
495 if (rel === 'newpage' ||
497 typeof rel === 'undefined' && (
498 typeof this.pages[this.current - 1] === 'undefined'
500 this.pages[this.current - 1].hash !== hash
506 content: $('#page_content').html(),
508 selflink: $('#selflink').html(),
512 AJAX.setUrlHash(this.current, hash);
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
520 * @param string index Which page from the history to load
524 navigate: function (index) {
525 if (typeof this.pages[index] === 'undefined') {
527 '<div class="error">' + PMA_messages.strInvalidPage + '</div>',
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;
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
549 update: function () {
550 var page = this.pages[this.current - 1];
552 page.content = $('#page_content').html();
556 * @var object Dedicated menu cache
560 * Returns the number of items in an associative array
564 size: function (obj) {
567 if (obj.hasOwnProperty(key)) {
574 * @var hash Stores the content of the cached menus
578 * Saves a new menu in the cache
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
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) {
591 if (! init || this.data[i].timestamp.getTime() < oldest.getTime()) {
592 oldest = this.data[i].timestamp;
598 delete this.data[key];
602 timestamp: new Date()
606 * Retrieves a menu given its hash
608 * @param string hash The hash of the menu to be retrieved
612 get: function (hash) {
613 if (this.data[hash]) {
614 return this.data[hash].content;
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
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
627 getRequestParam: function () {
630 for (var i in this.data) {
633 var menuHashesParam = menuHashes.join('-');
634 if (menuHashesParam) {
635 param = '&menuHashes=' + menuHashesParam;
640 * Replaces the menu with new content
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);
655 * URL hash management module.
656 * Allows direct bookmarking and microhistory.
658 AJAX.setUrlHash = (function (jQuery, window) {
661 * Indictaes whether we have already completed
662 * the initialisation of the hash
668 * Stores a hash that needed to be set when we were not ready
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
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');
692 * Sets the hash part of the URL
696 function setUrlHash(index, hash) {
699 * Setting hash leads to reload in webkit:
700 * http://www.quirksmode.org/bugreports/archives/2005/05/Safari_13_visual_anomaly_with_windowlocationhref.html
702 * so we expect that users are not running an ancient Safari version
707 window.location.hash = "PMAURL-" + index + ":" + hash;
710 savedHash = "PMAURL-" + index + ":" + hash;
714 * Start initialisation
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
723 // We don't have a valid hash, so we'll set it up
724 // when the page finishes loading
726 /* Check if we should set URL */
727 if (savedHash !== "") {
728 window.location.hash = savedHash;
732 // Indicate that we're done initialising
737 * Register an event handler for when the url hash changes
740 jQuery(window).hashchange(function () {
741 if (userChange === false) {
742 // Ignore internally triggered hash changes
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(':')
749 AJAX.cache.navigate(index);
754 * Publicly exposes a reference to the otherwise private setUrlHash function
760 * Page load event handler
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,
769 .append('<div></div>')
770 .append($('#serverinfo').clone())
771 .append($('#topmenucontainer').clone())
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) {
780 AJAX.cache.primer.url,
781 AJAX.cache.primer.scripts,
782 AJAX.cache.primer.menuHash
789 * Attach a generic event handler to clicks
790 * on pages and submissions of forms
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)
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);
804 '<div class="error">'
805 + PMA_messages.strErrorProcessingRequest
806 + '<div>' + errorCode + '</div>'
807 + '<div>' + errorText + '</div>'