diff --git a/js/ajax.js b/js/ajax.js new file mode 100644 index 0000000000..f1cc9448e8 --- /dev/null +++ b/js/ajax.js @@ -0,0 +1,210 @@ +/** + * This object handles ajax requests for pages. It also + * handles the reloading of the main menu and scripts (TODO: navigation). + */ +var AJAX = { + /** + * @var bool active Whether we are busy + */ + active: false, + /** + * @var bool _debug Makes noise in your Firebug console + */ + _debug: false, + /** + * Given the filename of a script, returns a string to be + * used to refer to all the events registered for the file + * + * @param string key The filename for which to get the event name + * + * @return string + */ + getEventName: function (key){ + /* http://burtleburtle.net/bob/hash/doobs.html#one */ + key += ""; + var len = key.length, hash=0, i=0; + for (; i> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return "onload_" + Math.abs(hash); + }, + /** + * Registers an onload event for a file + * + * @param string file The filename for which to register the event + * @param function func The function to execute when the page is ready + * + * @return void + */ + registerOnload: function (file, func) { + eventName = AJAX.getEventName(file); + $(document).bind(eventName, func); + this._debug && console.log("Registered event " + eventName + " for file " + file); // no need to translate + }, + /** + * Called when a page has finished loading, once for every + * file that registered to the onload event of that file. + * + * @param string file The filename for which to fire the event + * + * @return void + */ + fireOnload: function (file) { + eventName = AJAX.getEventName(file); + $(document).trigger(eventName); + this._debug && console.log("Fired event " + eventName + " for file " + file); // no need to translate + }, + /** + * Event handler for clicks on links and form submissions + * + * @param eventData e + * + * @return void + */ + requestHandler: function (e) { + if ($(this).attr('target')) { + return true; + } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) { + return true; + } else if ($(this).attr('href') && $(this).attr('href').match(/^#/)) { + return true; + } else if ($(this).attr('href') && $(this).attr('href').match(/^mailto/)) { + return true; + } else { + e.preventDefault(); + e.stopImmediatePropagation(); + if (AJAX.active == true) { + return false; + } else { + AJAX.active = true; + } + } + + var url = $(this).attr('href') || $(this).attr('action') + '?' + $(this).serialize(); + this._debug && console.log("Loading: " + url); // no need to translate + var $msgbox = PMA_ajaxShowMessage(); + $('html, body').animate({scrollTop: 0}, 'fast'); + $.get(url, {'ajax_request': true, 'ajax_page_request': true}, function (data) { + if (data.success) { + PMA_ajaxRemoveMessage($msgbox); + + if (data._menu) { + $('#floating_menubar').html(data._menu) + .children().first().remove(); // Remove duplicate wrapper (TODO: don't send it in the response) + menuPrepare(); + menuResize(); + } + + $('*').unbind().die(); + + $('body').children().not('#floating_menubar').not('#page_content').not('#selflink').remove(); + + $('#page_content').replaceWith("
" + data.message + "
"); + + if (data._scripts) { + AJAX.scriptHandler.load(data._scripts); + } + } else { + PMA_ajaxShowMessage(data.error, false); + AJAX.active = false; + } + }); + }, + /** + * This object is in charge of downloading scripts, + * keeping track of what's downloaded and firing + * the onload event for them when the page is ready. + */ + scriptHandler: { + /** + * @var array _scripts The list of files already downloaded + */ + _scripts: [], + /** + * @var array _scriptsToBeLoaded The list of files that + * need to be downloaded + */ + _scriptsToBeLoaded: [], + /** + * @var array _scriptsToBeFired The list of files for which + * to fire the onload event + */ + _scriptsToBeFired: [], + /** + * Records that a file has been downloaded + * + * @param string file The filename + * + * @return self For chaining + */ + add: function (file) { + this._scripts.push(file); + return this; + }, + /** + * Queues up an array of files to be downloaded + * + * @param array files An array of filenames and flags + * + * @return void + */ + load: function (files) { + this._scriptsToBeLoaded = []; + this._scriptsToBeFired = []; + for (var i in files) { + this._scriptsToBeLoaded.push(files[i].name); + if (files[i].fire) { + this._scriptsToBeFired.push(files[i].name); + } + } + this.callback(); + }, + /** + * Called whenever a file is loaded. + * Will queue up another file, or call done(); + * + * @return void + */ + callback: function () { + var scripts = this._scriptsToBeLoaded; + if (scripts.length > 0) { + var script = scripts.shift(); + if (this._scripts.indexOf(script) === -1) { + this.add(script); + var self = this; + $.getScript('js/' + script, function () { + self.callback(); + }); + } else { + this.callback(); + } + } else { + this.done(); + } + }, + /** + * Called whenever all files are loaded + * + * @return void + */ + done: function () { + for (var i in this._scriptsToBeFired) { + AJAX.fireOnload(this._scriptsToBeFired[i]); + } + AJAX.active = false; + } + } +}; + +/** + * Attach a generic event handler to clicks + * on pages and submissions of forms + */ +$('a').live('click', AJAX.requestHandler); +$('form').live('submit', AJAX.requestHandler); + diff --git a/js/functions.js b/js/functions.js index 500db97259..5a13402ecd 100644 --- a/js/functions.js +++ b/js/functions.js @@ -2824,7 +2824,8 @@ function menuResize() } } -$(function() { +function menuPrepare() +{ var topmenu = $('#topmenu'); if (topmenu.length == 0) { return; @@ -2853,7 +2854,10 @@ $(function() { } }); topmenu.append(submenu); +} +$(function() { + menuPrepare(); // populate submenu and register resize event menuResize(); $(window).resize(menuResize); diff --git a/libraries/Header.class.php b/libraries/Header.class.php index 2651b919c2..8d14793f24 100644 --- a/libraries/Header.class.php +++ b/libraries/Header.class.php @@ -142,6 +142,7 @@ class PMA_Header private function _addDefaultScripts() { $this->_scripts->addFile('jquery/jquery-1.6.2.js'); + $this->_scripts->addFile('ajax.js'); $this->_scripts->addFile('jquery/jquery-ui-1.8.16.custom.js'); $this->_scripts->addFile('jquery/jquery.sprintf.js'); $this->_scripts->addFile('update-location.js'); @@ -206,6 +207,16 @@ class PMA_Header return $this->_scripts; } + /** + * Returns the PMA_Menu object + * + * @return PMA_Menu object + */ + public function getMenu() + { + return $this->_menu; + } + /** * Setter for the ID attribute in the BODY tag * diff --git a/libraries/Response.class.php b/libraries/Response.class.php index d10aecb0e0..eb8c14fbb0 100644 --- a/libraries/Response.class.php +++ b/libraries/Response.class.php @@ -66,6 +66,14 @@ class PMA_Response * @var bool */ private $_isAjax; + /** + * Whether we are servicing an ajax request for a page + * that was fired using the generic page handler in JS. + * + * @access private + * @var bool + */ + private $_isAjaxPage; /** * Whether there were any errors druing the processing of the request * Only used for ajax responses @@ -96,11 +104,15 @@ class PMA_Response $this->_JSON = array(); $this->_footer = new PMA_Footer(); - $this->_isSuccess = true; - $this->_isAjax = false; + $this->_isSuccess = true; + $this->_isAjax = false; + $this->_isAjaxPage = false; if (isset($_REQUEST['ajax_request']) && $_REQUEST['ajax_request'] == true) { $this->_isAjax = true; } + if (isset($_REQUEST['ajax_page_request']) && $_REQUEST['ajax_page_request'] == true) { + $this->_isAjaxPage = true; + } $this->_header->setAjax($this->_isAjax); $this->_footer->setAjax($this->_isAjax); $this->_CWD = getcwd(); @@ -278,6 +290,11 @@ class PMA_Response unset($this->_JSON['message']); } + if ($this->_isAjaxPage) { + $this->addJSON('_menu', $this->getHeader()->getMenu()->getDisplay()); + $this->addJSON('_scripts', $this->getHeader()->getScripts()->getFiles()); + } + // Set the Content-Type header to JSON so that jQuery parses the // response correctly. if (! defined('TESTSUITE')) { diff --git a/libraries/Scripts.class.php b/libraries/Scripts.class.php index 7fae5abf50..e2b5b32e7f 100644 --- a/libraries/Scripts.class.php +++ b/libraries/Scripts.class.php @@ -96,14 +96,15 @@ class PMA_Scripts */ public function addFile($filename, $conditional_ie = false) { - $filename = 'js/' . $filename; $hash = md5($filename); if (empty($this->_files[$hash])) { + $has_onload = $this->eventBlacklist($filename); $timestamp = null; if (strpos($filename, '?') === false) { - $timestamp = filemtime($filename); + $timestamp = filemtime('js/' . $filename); } $this->_files[$hash] = array( + 'has_onload' => $has_onload, 'filename' => $filename, 'timestamp' => $timestamp, 'conditional_ie' => $conditional_ie @@ -111,6 +112,29 @@ class PMA_Scripts } } + /** + * Determines whether to fire up an onload event for a file + * + * @param string $filename + * + * @return int 1 to fire up the event, 0 not to + */ + private function eventBlacklist($filename) + { + if ( strpos($filename, 'jquery') !== false + || strpos($filename, 'codemirror') !== false + || strpos($filename, 'messages.php') !== false + || strpos($filename, 'ajax.js') !== false + || strpos($filename, 'update-location.js') !== false + || strpos($filename, 'get_image.js.php') !== false + || strpos($filename, 'cross_framing_protection.js') !== false + ) { + return 0; + } else { + return 1; + } + } + /** * Adds a new code snippet to the code to be executed * @@ -141,6 +165,26 @@ class PMA_Scripts ); } + /** + * Returns a list with filenames and a flag to indicate + * whether to register onload events for this file + * + * @return array + */ + public function getFiles() + { + $retval = array(); + foreach ($this->_files as $file) { + if (! $file['conditional_ie'] || PMA_USR_BROWSER_AGENT == 'IE') { + $retval[] = array( + 'name' => $file['filename'], + 'fire' => $file['has_onload'] + ); + } + } + return $retval; + } + /** * Renders all the JavaScript file inclusions, code and events * @@ -152,11 +196,27 @@ class PMA_Scripts foreach ($this->_files as $file) { $retval .= $this->_includeFile( - $file['filename'], + 'js/' . $file['filename'], $file['timestamp'], $file['conditional_ie'] ); } + $code = 'AJAX.scriptHandler'; + foreach ($this->_files as $file) { + $code .= '.add("' . PMA_escapeJsString($file['filename']) . '")'; + } + $code .= ';'; + $this->addCode($code); + + $code = '$(function() {'; + foreach ($this->_files as $file) { + if ($file['has_onload']) { + $code .= 'AJAX.fireOnload("' . PMA_escapeJsString($file['filename']) . '");'; + } + } + $code .= '});'; + $this->addCode($code); + $retval .= '