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 object xhr A reference to the ajax request that is currently running
20 * @var object lockedTargets, list of locked targets
24 * @var function Callback to execute after a successful request
25 * Used by PMA_commonFunctions from common.js
27 _callback: function () {},
29 * @var bool _debug Makes noise in your Firebug console
33 * @var object $msgbox A reference to a jQuery object that links to a message
34 * box that is generated by PMA_ajaxShowMessage()
38 * Given the filename of a script, returns a hash to be
39 * used to refer to all the events registered for the file
41 * @param key string key The filename for which to get the event name
45 hash: function (key) {
46 /* http://burtleburtle.net/bob/hash/doobs.html#one */
51 for (; i < len; ++i) {
52 hash += key.charCodeAt(i);
59 return Math.abs(hash);
62 * Registers an onload event for a file
64 * @param file string file The filename for which to register the event
65 * @param func function func The function to execute when the page is ready
67 * @return self For chaining
69 registerOnload: function (file, func) {
70 var eventName = 'onload_' + AJAX.hash(file);
71 $(document).on(eventName, func);
74 // no need to translate
75 'Registered event ' + eventName + ' for file ' + file
81 * Registers a teardown event for a file. This is useful to execute functions
82 * that unbind events for page elements that are about to be removed.
84 * @param string file The filename for which to register the event
85 * @param function func The function to execute when
86 * the page is about to be torn down
88 * @return self For chaining
90 registerTeardown: function (file, func) {
91 var eventName = 'teardown_' + AJAX.hash(file);
92 $(document).on(eventName, func);
95 // no need to translate
96 'Registered event ' + eventName + ' for file ' + file
102 * Called when a page has finished loading, once for every
103 * file that registered to the onload event of that file.
105 * @param string file The filename for which to fire the event
109 fireOnload: function (file) {
110 var eventName = 'onload_' + AJAX.hash(file);
111 $(document).trigger(eventName);
114 // no need to translate
115 'Fired event ' + eventName + ' for file ' + file
120 * Called just before a page is torn down, once for every
121 * file that registered to the teardown event of that file.
123 * @param string file The filename for which to fire the event
127 fireTeardown: function (file) {
128 var eventName = 'teardown_' + AJAX.hash(file);
129 $(document).triggerHandler(eventName);
132 // no need to translate
133 'Fired event ' + eventName + ' for file ' + file
138 * function to handle lock page mechanism
140 * @param event the event object
144 lockPageHandler: function (event) {
149 if (event.data.value === 3) {
150 newHash = event.data.content;
154 // Don't lock on enter.
155 if (0 === event.charCode) {
159 lockId = $(this).data('lock-id');
160 if (typeof lockId === 'undefined') {
164 * @todo Fix Code mirror does not give correct full value (query)
165 * in textarea, it returns only the change in content.
167 if (event.data.value === 1) {
168 newHash = AJAX.hash($(this).val());
170 newHash = AJAX.hash($(this).is(':checked'));
172 oldHash = $(this).data('val-hash');
174 // Set lock if old value !== new value
175 // otherwise release lock
176 if (oldHash !== newHash) {
177 AJAX.lockedTargets[lockId] = true;
179 delete AJAX.lockedTargets[lockId];
181 // Show lock icon if locked targets is not empty.
182 // otherwise remove lock icon
183 if (!jQuery.isEmptyObject(AJAX.lockedTargets)) {
184 $('#lock_page_icon').html(PMA_getImage('s_lock', PMA_messages.strLockToolTip).toString());
186 $('#lock_page_icon').html('');
194 resetLock: function () {
195 AJAX.lockedTargets = {};
196 $('#lock_page_icon').html('');
199 replace: function (content) {
200 $('#floating_menubar').html(content)
201 // Remove duplicate wrapper
202 // TODO: don't send it in the response
203 .children().first().remove();
204 $('#topmenu').menuResizer(PMA_mainMenuResizerCallback);
208 * Event handler for clicks on links and form submissions
210 * @param object e Event data
214 requestHandler: function (event) {
215 // In some cases we don't want to handle the request here and either
216 // leave the browser deal with it natively (e.g: file download)
217 // or leave an existing ajax event handler present elsewhere deal with it
218 var href = $(this).attr('href');
219 if (typeof event !== 'undefined' && (event.shiftKey || event.ctrlKey)) {
221 } else if ($(this).attr('target')) {
223 } else if ($(this).hasClass('ajax') || $(this).hasClass('disableAjax')) {
224 // reset the lockedTargets object, as specified AJAX operation has finished
227 } else if (href && href.match(/^#/)) {
229 } else if (href && href.match(/^mailto/)) {
231 } else if ($(this).hasClass('ui-datepicker-next') ||
232 $(this).hasClass('ui-datepicker-prev')
237 if (typeof event !== 'undefined') {
238 event.preventDefault();
239 event.stopImmediatePropagation();
242 // triggers a confirm dialog if:
243 // the user has performed some operations on loaded page
244 // the user clicks on some link, (won't trigger for buttons)
245 // the click event is not triggered by script
246 if (typeof event !== 'undefined' && event.type === 'click' &&
247 event.isTrigger !== true &&
248 !jQuery.isEmptyObject(AJAX.lockedTargets) &&
249 confirm(PMA_messages.strConfirmNavigation) === false
254 var isLink = !! href || false;
255 var previousLinkAborted = false;
257 if (AJAX.active === true) {
258 // Cancel the old request if abortable, when the user requests
259 // something else. Otherwise silently bail out, as there is already
260 // a request well in progress.
262 // In case of a link request, attempt aborting
264 if (AJAX.xhr.status === 0 && AJAX.xhr.statusText === 'abort') {
266 AJAX.$msgbox = PMA_ajaxShowMessage(PMA_messages.strAbortedRequest);
269 previousLinkAborted = true;
275 // In case submitting a form, don't attempt aborting
280 AJAX.source = $(this);
282 $('html, body').animate({ scrollTop: 0 }, 'fast');
284 var url = isLink ? href : $(this).attr('action');
285 var argsep = PMA_commonParams.get('arg_separator');
286 var params = 'ajax_request=true' + argsep + 'ajax_page_request=true';
287 var dataPost = AJAX.source.getPostData();
289 params += argsep + $(this).serialize();
290 } else if (dataPost) {
291 params += argsep + dataPost;
294 if (! (history && history.pushState)) {
295 // Add a list of menu hashes that we have in the cache to the request
296 params += PMA_MicroHistory.menus.getRequestParam();
300 console.log('Loading: ' + url); // no need to translate
305 AJAX.$msgbox = PMA_ajaxShowMessage();
306 // Save reference for the new link request
307 AJAX.xhr = $.get(url, params, AJAX.responseHandler);
308 if (history && history.pushState) {
312 if (previousLinkAborted) {
313 // hack: there is already an aborted entry on stack
314 // so just modify the aborted one
315 history.replaceState(state, null, href);
317 history.pushState(state, null, href);
322 * Manually fire the onsubmit event for the form, if any.
323 * The event was saved in the jQuery data object by an onload
324 * handler defined below. Workaround for bug #3583316
326 var onsubmit = $(this).data('onsubmit');
327 // Submit the request if there is no onsubmit handler
328 // or if it returns a value that evaluates to true
329 if (typeof onsubmit !== 'function' || onsubmit.apply(this, [event])) {
331 AJAX.$msgbox = PMA_ajaxShowMessage();
332 $.post(url, params, AJAX.responseHandler);
337 * Called after the request that was initiated by this.requestHandler()
338 * has completed successfully or with a caught error. For completely
339 * failed requests or requests with uncaught errors, see the .ajaxError
340 * handler at the bottom of this file.
342 * To refer to self use 'AJAX', instead of 'this' as this function
343 * is called in the jQuery context.
345 * @param object e Event data
349 responseHandler: function (data) {
350 if (typeof data === 'undefined' || data === null) {
353 if (typeof data.success !== 'undefined' && data.success) {
354 $('html, body').animate({ scrollTop: 0 }, 'fast');
355 PMA_ajaxRemoveMessage(AJAX.$msgbox);
357 if (data._redirect) {
358 PMA_ajaxShowMessage(data._redirect, false);
364 AJAX.scriptHandler.reset(function () {
365 if (data._reloadNavigation) {
366 PMA_reloadNavigation();
369 $('title').replaceWith(data._title);
372 if (history && history.pushState) {
374 url : data._selflink,
377 history.replaceState(state, null);
378 AJAX.handleMenu.replace(data._menu);
380 PMA_MicroHistory.menus.replace(data._menu);
381 PMA_MicroHistory.menus.add(data._menuHash, data._menu);
383 } else if (data._menuHash) {
384 if (! (history && history.pushState)) {
385 PMA_MicroHistory.menus.replace(PMA_MicroHistory.menus.get(data._menuHash));
388 if (data._disableNaviSettings) {
389 PMA_disableNaviSettings();
391 PMA_ensureNaviSettings(data._selflink);
394 // Remove all containers that may have
395 // been added outside of #page_content
397 .not('#pma_navigation')
398 .not('#floating_menubar')
399 .not('#page_nav_icons')
400 .not('#page_content')
405 .not('#pma_console_container')
406 .not('#prefs_autoload')
408 // Replace #page_content with new content
409 if (data.message && data.message.length > 0) {
410 $('#page_content').replaceWith(
411 '<div id=\'page_content\'>' + data.message + '</div>'
413 PMA_highlightSQL($('#page_content'));
414 checkNumberOfFields();
417 if (data._selflink) {
418 var source = data._selflink.split('?')[0];
419 // Check for faulty links
420 $selflink_replace = {
421 'import.php': 'tbl_sql.php',
422 'tbl_chart.php': 'sql.php',
423 'tbl_gis_visualization.php': 'sql.php'
425 if ($selflink_replace[source]) {
426 var replacement = $selflink_replace[source];
427 data._selflink = data._selflink.replace(source, replacement);
429 $('#selflink').find('> a').attr('href', data._selflink);
432 PMA_commonParams.setAll(data._params);
435 AJAX.scriptHandler.load(data._scripts);
437 if (data._selflink && data._scripts && data._menuHash && data._params) {
438 if (! (history && history.pushState)) {
439 PMA_MicroHistory.add(
444 AJAX.source.attr('rel')
448 if (data._displayMessage) {
449 $('#page_content').prepend(data._displayMessage);
450 PMA_highlightSQL($('#page_content'));
453 $('#pma_errors').remove();
456 if (data._errSubmitMsg) {
457 msg = data._errSubmitMsg;
460 $('<div/>', { id : 'pma_errors', class : 'clearfloat' })
461 .insertAfter('#selflink')
462 .append(data._errors);
463 // bind for php error reporting forms (bottom)
464 $('#pma_ignore_errors_bottom').on('click', function (e) {
466 PMA_ignorePhpErrors();
468 $('#pma_ignore_all_errors_bottom').on('click', function (e) {
470 PMA_ignorePhpErrors(false);
472 // In case of 'sendErrorReport'='always'
473 // submit the hidden error reporting form.
474 if (data._sendErrorAlways === '1' &&
475 data._stopErrorReportLoop !== '1'
477 $('#pma_report_errors_form').submit();
478 PMA_ajaxShowMessage(PMA_messages.phpErrorsBeingSubmitted, false);
479 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
480 } else if (data._promptPhpErrors) {
481 // otherwise just prompt user if it is set so.
482 msg = msg + PMA_messages.phpErrorsFound;
483 // scroll to bottom where all the errors are displayed.
484 $('html, body').animate({ scrollTop:$(document).height() }, 'slow');
487 PMA_ajaxShowMessage(msg, false);
488 // bind for php error reporting forms (popup)
489 $('#pma_ignore_errors_popup').on('click', function () {
490 PMA_ignorePhpErrors();
492 $('#pma_ignore_all_errors_popup').on('click', function () {
493 PMA_ignorePhpErrors(false);
496 if (typeof AJAX._callback === 'function') {
497 AJAX._callback.call();
499 AJAX._callback = function () {};
502 PMA_ajaxShowMessage(data.error, false);
505 PMA_handleRedirectAndReload(data);
506 if (data.fieldWithError) {
507 $(':input.error').removeClass('error');
508 $('#' + data.fieldWithError).addClass('error');
513 * This object is in charge of downloading scripts,
514 * keeping track of what's downloaded and firing
515 * the onload event for them when the page is ready.
519 * @var array _scripts The list of files already downloaded
523 * @var string _scriptsVersion version of phpMyAdmin from which the
524 * scripts have been loaded
526 _scriptsVersion: null,
528 * @var array _scriptsToBeLoaded The list of files that
529 * need to be downloaded
531 _scriptsToBeLoaded: [],
533 * @var array _scriptsToBeFired The list of files for which
534 * to fire the onload and unload events
536 _scriptsToBeFired: [],
537 _scriptsCompleted: false,
539 * Records that a file has been downloaded
541 * @param string file The filename
542 * @param string fire Whether this file will be registering
543 * onload/teardown events
545 * @return self For chaining
547 add: function (file, fire) {
548 this._scripts.push(file);
550 // Record whether to fire any events for the file
551 // This is necessary to correctly tear down the initial page
552 this._scriptsToBeFired.push(file);
557 * Download a list of js files in one request
559 * @param array files An array of filenames and flags
563 load: function (files, callback) {
566 // Clear loaded scripts if they are from another version of phpMyAdmin.
567 // Depends on common params being set before loading scripts in responseHandler
568 if (self._scriptsVersion === null) {
569 self._scriptsVersion = PMA_commonParams.get('PMA_VERSION');
570 } else if (self._scriptsVersion !== PMA_commonParams.get('PMA_VERSION')) {
572 self._scriptsVersion = PMA_commonParams.get('PMA_VERSION');
574 self._scriptsCompleted = false;
575 self._scriptsToBeFired = [];
576 // We need to first complete list of files to load
577 // as next loop will directly fire requests to load them
578 // and that triggers removal of them from
579 // self._scriptsToBeLoaded
581 self._scriptsToBeLoaded.push(files[i].name);
583 self._scriptsToBeFired.push(files[i].name);
587 var script = files[i].name;
588 // Only for scripts that we don't already have
589 if ($.inArray(script, self._scripts) === -1) {
591 this.appendScript(script, callback);
593 self.done(script, callback);
596 // Trigger callback if there is nothing else to load
597 self.done(null, callback);
600 * Called whenever all files are loaded
604 done: function (script, callback) {
605 if (typeof ErrorReport !== 'undefined') {
606 ErrorReport.wrap_global_functions();
608 if ($.inArray(script, this._scriptsToBeFired)) {
609 AJAX.fireOnload(script);
611 if ($.inArray(script, this._scriptsToBeLoaded)) {
612 this._scriptsToBeLoaded.splice($.inArray(script, this._scriptsToBeLoaded), 1);
614 if (script === null) {
615 this._scriptsCompleted = true;
617 /* We need to wait for last signal (with null) or last script load */
618 AJAX.active = (this._scriptsToBeLoaded.length > 0) || ! this._scriptsCompleted;
619 /* Run callback on last script */
620 if (! AJAX.active && $.isFunction(callback)) {
625 * Appends a script element to the head to load the scripts
629 appendScript: function (name, callback) {
630 var head = document.head || document.getElementsByTagName('head')[0];
631 var script = document.createElement('script');
634 script.type = 'text/javascript';
635 script.src = 'js/' + name + '?' + 'v=' + encodeURIComponent(PMA_commonParams.get('PMA_VERSION'));
636 script.async = false;
637 script.onload = function () {
638 self.done(name, callback);
640 head.appendChild(script);
643 * Fires all the teardown event handlers for the current page
644 * and rebinds all forms and links to the request handler
646 * @param function callback The callback to call after resetting
650 reset: function (callback) {
651 for (var i in this._scriptsToBeFired) {
652 AJAX.fireTeardown(this._scriptsToBeFired[i]);
654 this._scriptsToBeFired = [];
656 * Re-attach a generic event handler to clicks
657 * on pages and submissions of forms
659 $(document).off('click', 'a').on('click', 'a', AJAX.requestHandler);
660 $(document).off('submit', 'form').on('submit', 'form', AJAX.requestHandler);
661 if (! (history && history.pushState)) {
662 PMA_MicroHistory.update();
670 * Here we register a function that will remove the onsubmit event from all
671 * forms that will be handled by the generic page loader. We then save this
672 * event handler in the "jQuery data", so that we can fire it up later in
673 * AJAX.requestHandler().
677 AJAX.registerOnload('functions.js', function () {
678 // Registering the onload event for functions.js
679 // ensures that it will be fired for all pages
680 $('form').not('.ajax').not('.disableAjax').each(function () {
681 if ($(this).attr('onsubmit')) {
682 $(this).data('onsubmit', this.onsubmit).attr('onsubmit', '');
686 var $page_content = $('#page_content');
688 * Workaround for passing submit button name,value on ajax form submit
689 * by appending hidden element with submit button name and value.
691 $page_content.on('click', 'form input[type=submit]', function () {
692 var buttonName = $(this).attr('name');
693 if (typeof buttonName === 'undefined') {
696 $(this).closest('form').append($('<input/>', {
699 'value': $(this).val()
704 * Attach event listener to events when user modify visible
705 * Input,Textarea and select fields to make changes in forms
709 'form.lock-page textarea, ' +
710 'form.lock-page input[type="text"], ' +
711 'form.lock-page input[type="number"], ' +
712 'form.lock-page select',
718 'form.lock-page input[type="checkbox"], ' +
719 'form.lock-page input[type="radio"]',
724 * Reset lock when lock-page form reset event is fired
725 * Note: reset does not bubble in all browser so attach to
728 $('form.lock-page').on('reset', function (event) {
734 * Page load event handler
737 var menuContent = $('<div></div>')
738 .append($('#serverinfo').clone())
739 .append($('#topmenucontainer').clone())
741 if (history && history.pushState) {
742 // set initial state reload
743 var initState = ('state' in window.history && window.history.state !== null);
744 var initURL = $('#selflink').find('> a').attr('href') || location.href;
749 history.replaceState(state, null);
751 $(window).on('popstate', function (event) {
752 var initPop = (! initState && location.href === initURL);
754 // check if popstate fired on first page itself
758 var state = event.originalEvent.state;
759 if (state && state.menu) {
760 AJAX.$msgbox = PMA_ajaxShowMessage();
761 var params = 'ajax_request=true' + PMA_commonParams.get('arg_separator') + 'ajax_page_request=true';
762 var url = state.url || location.href;
763 $.get(url, params, AJAX.responseHandler);
764 // TODO: Check if sometimes menu is not retrieved from server,
765 // Not sure but it seems menu was missing only for printview which
766 // been removed lately, so if it's right some dead menu checks/fallbacks
767 // may need to be removed from this file and Header.php
768 // AJAX.handleMenu.replace(event.originalEvent.state.menu);
772 // Fallback to microhistory mechanism
774 .load([{ 'name' : 'microhistory.js', 'fire' : 1 }], function () {
775 // The cache primer is set by the footer class
776 if (PMA_MicroHistory.primer.url) {
777 PMA_MicroHistory.menus.add(
778 PMA_MicroHistory.primer.menuHash,
783 // Queue up this event twice to make sure that we get a copy
784 // of the page after all other onload events have been fired
785 if (PMA_MicroHistory.primer.url) {
786 PMA_MicroHistory.add(
787 PMA_MicroHistory.primer.url,
788 PMA_MicroHistory.primer.scripts,
789 PMA_MicroHistory.primer.menuHash
798 * Attach a generic event handler to clicks
799 * on pages and submissions of forms
801 $(document).on('click', 'a', AJAX.requestHandler);
802 $(document).on('submit', 'form', AJAX.requestHandler);
805 * Gracefully handle fatal server errors
806 * (e.g: 500 - Internal server error)
808 $(document).ajaxError(function (event, request, settings) {
810 console.log('AJAX error: status=' + request.status + ', text=' + request.statusText);
812 // Don't handle aborted requests
813 if (request.status !== 0 || request.statusText !== 'abort') {
815 var state = request.state();
817 if (request.status !== 0) {
818 details += '<div>' + escapeHtml(PMA_sprintf(PMA_messages.strErrorCode, request.status)) + '</div>';
820 details += '<div>' + escapeHtml(PMA_sprintf(PMA_messages.strErrorText, request.statusText + ' (' + state + ')')) + '</div>';
821 if (state === 'rejected' || state === 'timeout') {
822 details += '<div>' + escapeHtml(PMA_messages.strErrorConnection) + '</div>';
825 '<div class="error">' +
826 PMA_messages.strErrorProcessingRequest +