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);
64 // no need to translate
65 "Registered event " + eventName + " for file " + file
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.
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
78 * @return self For chaining
80 registerTeardown: function (file, func) {
81 var eventName = 'teardown_' + AJAX.hash(file);
82 $(document).bind(eventName, func);
85 // no need to translate
86 "Registered event " + eventName + " for file " + file
92 * Called when a page has finished loading, once for every
93 * file that registered to the onload event of that file.
95 * @param string file The filename for which to fire the event
99 fireOnload: function (file) {
100 var eventName = 'onload_' + AJAX.hash(file);
101 $(document).trigger(eventName);
104 // no need to translate
105 "Fired event " + eventName + " for file " + file
110 * Called just before a page is torn down, once for every
111 * file that registered to the teardown event of that file.
113 * @param string file The filename for which to fire the event
117 fireTeardown: function (file) {
118 var eventName = 'teardown_' + AJAX.hash(file);
119 $(document).triggerHandler(eventName);
122 // no need to translate
123 "Fired event " + eventName + " for file " + file
128 * Event handler for clicks on links and form submissions
130 * @param object e Event data
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)) {
141 } else if ($(this).attr('target')) {
143 } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
145 } else if (href && href.match(/^#/)) {
147 } else if (href && href.match(/^mailto/)) {
149 } else if ($(this).hasClass('ui-datepicker-next') ||
150 $(this).hasClass('ui-datepicker-prev')
155 if (typeof event != 'undefined') {
156 event.preventDefault();
157 event.stopImmediatePropagation();
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
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';
175 params += '&' + $(this).serialize();
177 // Add a list of menu hashes that we have in the cache to the request
178 params += AJAX.cache.menus.getRequestParam();
181 console.log("Loading: " + url); // no need to translate
186 AJAX.$msgbox = PMA_ajaxShowMessage();
187 $.get(url, params, AJAX.responseHandler);
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
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])) {
199 AJAX.$msgbox = PMA_ajaxShowMessage();
200 $.post(url, params, AJAX.responseHandler);
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.
210 * To refer to self use 'AJAX', instead of 'this' as this function
211 * is called in the jQuery context.
213 * @param object e Event data
217 responseHandler: function (data) {
219 $table_clone = false;
220 PMA_ajaxRemoveMessage(AJAX.$msgbox);
222 if (data._redirect) {
223 PMA_ajaxShowMessage(data._redirect, false);
228 AJAX.scriptHandler.reset(function () {
229 if (data._reloadNavigation) {
230 PMA_reloadNavigation();
232 if (data._reloadQuerywindow) {
233 var params = data._reloadQuerywindow;
234 PMA_querywindow.reload(
240 if (data._focusQuerywindow) {
241 PMA_querywindow.focus(
242 data._focusQuerywindow
246 $('title').replaceWith(data._title);
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));
255 // Remove all containers that may have
256 // been added outside of #page_content
258 .not('#pma_navigation')
259 .not('#floating_menubar')
260 .not('#goto_pagetop')
261 .not('#page_content')
263 .not('#session_debug')
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>"
270 PMA_highlightSQL($('#page_content'));
273 if (data._selflink) {
274 $('#selflink > a').attr('href', data._selflink);
277 AJAX.scriptHandler.load(data._scripts, data._params.token);
279 if (data._selflink && data._scripts && data._menuHash && data._params) {
285 AJAX.source.attr('rel')
289 PMA_commonParams.setAll(data._params);
291 if (data._displayMessage) {
292 $('#page_content').prepend(data._displayMessage);
293 PMA_highlightSQL($('#page_content'));
296 $('#pma_errors').remove();
298 $('<div/>', {id : 'pma_errors'})
299 .insertAfter('#selflink')
300 .append(data._errors);
303 if (typeof AJAX._callback === 'function') {
304 AJAX._callback.call();
306 AJAX._callback = function () {};
309 PMA_ajaxShowMessage(data.error, false);
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.
320 * @var array _scripts The list of files already downloaded
324 * @var array _scriptsToBeLoaded The list of files that
325 * need to be downloaded
327 _scriptsToBeLoaded: [],
329 * @var array _scriptsToBeFired The list of files for which
330 * to fire the onload event
332 _scriptsToBeFired: [],
334 * Records that a file has been downloaded
336 * @param string file The filename
337 * @param string fire Whether this file will be registering
338 * onload/teardown events
340 * @return self For chaining
342 add: function (file, fire) {
343 this._scripts.push(file);
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);
352 * Download a list of js files in one request
354 * @param array files An array of filenames and flags
358 load: function (files, token) {
360 self._scriptsToBeLoaded = [];
361 self._scriptsToBeFired = [];
362 for (var i in files) {
363 self._scriptsToBeLoaded.push(files[i].name);
365 self._scriptsToBeFired.push(files[i].name);
368 // Generate a request string
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) {
377 request.push("scripts[]=" + script);
380 request.push("token=" + token);
381 request.push("call_done=1");
382 // Download the composite js file, if necessary
384 this.appendScript("js/get_scripts.js.php?" + request.join("&"));
390 * Called whenever all files are loaded
395 ErrorReport.wrap_global_functions();
396 for (var i in this._scriptsToBeFired) {
397 AJAX.fireOnload(this._scriptsToBeFired[i]);
402 * Appends a script element to the head to load the scripts
406 appendScript: function (url) {
407 var head = document.head || document.getElementsByTagName('head')[0];
408 var script = document.createElement('script');
409 script.type = 'text/javascript';
411 head.appendChild(script);
414 * Fires all the teardown event handlers for the current page
415 * and rebinds all forms and links to the request handler
417 * @param function callback The callback to call after resetting
421 reset: function (callback) {
422 for (var i in this._scriptsToBeFired) {
423 AJAX.fireTeardown(this._scriptsToBeFired[i]);
425 this._scriptsToBeFired = [];
427 * Re-attach a generic event handler to clicks
428 * on pages and submissions of forms
430 $('a').die('click').live('click', AJAX.requestHandler);
431 $('form').die('submit').live('submit', AJAX.requestHandler);
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().
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', '');
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
463 * @var int The maximum number of pages to keep in the cache
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.
473 * @var array Stores the content of the cached pages
477 * @var int The index of the currently loaded page
478 * This is used to know at which point in the history we are
482 * Saves a new page in the cache
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
494 * undefined: Default behaviour, 'samepage' if the
495 * selflinks of the two pages are the same.
496 * 'newpage' otherwise
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];
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
513 if (rel === 'newpage' ||
515 typeof rel === 'undefined' && (
516 typeof this.pages[this.current - 1] === 'undefined' ||
517 this.pages[this.current - 1].hash !== hash
523 content: $('#page_content').html(),
525 selflink: $('#selflink').html(),
529 AJAX.setUrlHash(this.current, hash);
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
537 * @param string index Which page from the history to load
541 navigate: function (index) {
542 if (typeof this.pages[index] === 'undefined') {
544 '<div class="error">' + PMA_messages.strInvalidPage + '</div>',
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;
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
566 update: function () {
567 var page = this.pages[this.current - 1];
569 page.content = $('#page_content').html();
573 * @var object Dedicated menu cache
577 * Returns the number of items in an associative array
581 size: function (obj) {
584 if (obj.hasOwnProperty(key)) {
591 * @var hash Stores the content of the cached menus
595 * Saves a new menu in the cache
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
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) {
608 if (! init || this.data[i].timestamp.getTime() < oldest.getTime()) {
609 oldest = this.data[i].timestamp;
615 delete this.data[key];
619 timestamp: new Date()
623 * Retrieves a menu given its hash
625 * @param string hash The hash of the menu to be retrieved
629 get: function (hash) {
630 if (this.data[hash]) {
631 return this.data[hash].content;
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
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
644 getRequestParam: function () {
647 for (var i in this.data) {
650 var menuHashesParam = menuHashes.join('-');
651 if (menuHashesParam) {
652 param = '&menuHashes=' + menuHashesParam;
657 * Replaces the menu with new content
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);
672 * URL hash management module.
673 * Allows direct bookmarking and microhistory.
675 AJAX.setUrlHash = (function (jQuery, window) {
678 * Indictaes whether we have already completed
679 * the initialisation of the hash
685 * Stores a hash that needed to be set when we were not ready
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
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');
709 * Sets the hash part of the URL
713 function setUrlHash(index, hash) {
716 * Setting hash leads to reload in webkit:
717 * http://www.quirksmode.org/bugreports/archives/2005/05/Safari_13_visual_anomaly_with_windowlocationhref.html
719 * so we expect that users are not running an ancient Safari version
724 window.location.hash = "PMAURL-" + index + ":" + hash;
727 savedHash = "PMAURL-" + index + ":" + hash;
731 * Start initialisation
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
740 // We don't have a valid hash, so we'll set it up
741 // when the page finishes loading
743 /* Check if we should set URL */
744 if (savedHash !== "") {
745 window.location.hash = savedHash;
749 // Indicate that we're done initialising
754 * Register an event handler for when the url hash changes
757 jQuery(window).hashchange(function () {
758 if (userChange === false) {
759 // Ignore internally triggered hash changes
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(':')
766 AJAX.cache.navigate(index);
771 * Publicly exposes a reference to the otherwise private setUrlHash function
777 * Page load event handler
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,
786 .append('<div></div>')
787 .append($('#serverinfo').clone())
788 .append($('#topmenucontainer').clone())
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) {
797 AJAX.cache.primer.url,
798 AJAX.cache.primer.scripts,
799 AJAX.cache.primer.menuHash
806 * Attach a generic event handler to clicks
807 * on pages and submissions of forms
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)
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);
821 '<div class="error">' +
822 PMA_messages.strErrorProcessingRequest +
823 '<div>' + errorCode + '</div>' +
824 '<div>' + errorText + '</div>' +