diff --git a/.eslintignore b/.eslintignore index c383636a10..125f7df65e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ tmp/ vendor/ js/dist/ js/lib/ +js/src/plugins/ diff --git a/.jsdoc.json b/.jsdoc.json new file mode 100644 index 0000000000..239c150893 --- /dev/null +++ b/.jsdoc.json @@ -0,0 +1,30 @@ +{ + "tags": { + "allowUnknownTags": true, + "dictionaries": ["jsdoc", "closure"] + }, + "recurseDepth": 10, + "source": { + "include": ["js/src/", "package.json"], + "includePattern": ".js$", + "exclude": ["js/src/plugins"], + "excludePattern": "(node_modules/|docs)" + }, + "sourceType": "module", + "plugins": [ + "plugins/markdown" + ], + "templates": { + "cleverLinks": true, + "monospaceLinks": true, + "useLongnameInNav": false, + "showInheritedInNav": true + }, + "opts": { + "destination": "./doc/js/", + "encoding": "utf8", + "private": true, + "recurse": true, + "template": "./node_modules/minami" + } +} diff --git a/db_search.php b/db_search.php index 56777e7a6b..a4c9a963a5 100644 --- a/db_search.php +++ b/db_search.php @@ -21,9 +21,8 @@ require_once 'libraries/common.inc.php'; $response = Response::getInstance(); $header = $response->getHeader(); $scripts = $header->getScripts(); -$scripts->addFile('db_search.js'); -$scripts->addFile('sql.js'); -$scripts->addFile('makegrid.js'); +$scripts->addFile('db_search'); +$scripts->addFile('sql'); require 'libraries/db_common.inc.php'; diff --git a/db_sql.php b/db_sql.php index fb403d484d..7a2ff9134c 100644 --- a/db_sql.php +++ b/db_sql.php @@ -24,9 +24,7 @@ PageSettings::showGroup('Sql'); $response = Response::getInstance(); $header = $response->getHeader(); $scripts = $header->getScripts(); -$scripts->addFile('makegrid.js'); -$scripts->addFile('vendor/jquery/jquery.uitablefilter.js'); -$scripts->addFile('sql.js'); +$scripts->addFile('sql'); require 'libraries/db_common.inc.php'; diff --git a/js/functions.js b/js/functions.js index acf352bb15..a3a33c546c 100644 --- a/js/functions.js +++ b/js/functions.js @@ -1808,121 +1808,6 @@ function getJSConfirmCommonParam (elem, params) { return params; } -/** - * Unbind all event handlers before tearing down a page - */ -AJAX.registerTeardown('functions.js', function () { - $(document).off('click', 'a.inline_edit_sql'); - $(document).off('click', 'input#sql_query_edit_save'); - $(document).off('click', 'input#sql_query_edit_discard'); - $('input.sqlbutton').off('click'); - if (codemirror_editor) { - codemirror_editor.off('blur'); - } else { - $(document).off('blur', '#sqlquery'); - } - $(document).off('change', '#parameterized'); - $(document).off('click', 'input.sqlbutton'); - $('#sqlquery').off('keydown'); - $('#sql_query_edit').off('keydown'); - - if (codemirror_inline_editor) { - // Copy the sql query to the text area to preserve it. - $('#sql_query_edit').text(codemirror_inline_editor.getValue()); - $(codemirror_inline_editor.getWrapperElement()).off('keydown'); - codemirror_inline_editor.toTextArea(); - codemirror_inline_editor = false; - } - if (codemirror_editor) { - $(codemirror_editor.getWrapperElement()).off('keydown'); - } -}); - -/** - * Jquery Coding for inline editing SQL_QUERY - */ -AJAX.registerOnload('functions.js', function () { - // If we are coming back to the page by clicking forward button - // of the browser, bind the code mirror to inline query editor. - bindCodeMirrorToInlineEditor(); - $(document).on('click', 'a.inline_edit_sql', function () { - if ($('#sql_query_edit').length) { - // An inline query editor is already open, - // we don't want another copy of it - return false; - } - - var $form = $(this).prev('form'); - var sql_query = $form.find('input[name=\'sql_query\']').val().trim(); - var $inner_sql = $(this).parent().prev().find('code.sql'); - var old_text = $inner_sql.html(); - - var new_content = '\n'; - new_content += getForeignKeyCheckboxLoader(); - new_content += '\n'; - new_content += '\n'; - var $editor_area = $('div#inline_editor'); - if ($editor_area.length === 0) { - $editor_area = $('
'); - $editor_area.insertBefore($inner_sql); - } - $editor_area.html(new_content); - loadForeignKeyCheckbox(); - $inner_sql.hide(); - - bindCodeMirrorToInlineEditor(); - return false; - }); - - $(document).on('click', 'input#sql_query_edit_save', function () { - // hide already existing success message - var sql_query; - if (codemirror_inline_editor) { - codemirror_inline_editor.save(); - sql_query = codemirror_inline_editor.getValue(); - } else { - sql_query = $(this).parent().find('#sql_query_edit').val(); - } - var fk_check = $(this).parent().find('#fk_checks').is(':checked'); - - var $form = $('a.inline_edit_sql').prev('form'); - var $fake_form = $('
', { action: 'import.php', method: 'post' }) - .append($form.find('input[name=server], input[name=db], input[name=table], input[name=token]').clone()) - .append($('', { type: 'hidden', name: 'show_query', value: 1 })) - .append($('', { type: 'hidden', name: 'is_js_confirmed', value: 0 })) - .append($('', { type: 'hidden', name: 'sql_query', value: sql_query })) - .append($('', { type: 'hidden', name: 'fk_checks', value: fk_check ? 1 : 0 })); - if (! checkSqlQuery($fake_form[0])) { - return false; - } - $('.success').hide(); - $fake_form.appendTo($('body')).submit(); - }); - - $(document).on('click', 'input#sql_query_edit_discard', function () { - var $divEditor = $('div#inline_editor_outer'); - $divEditor.siblings('code.sql').show(); - $divEditor.remove(); - }); - - $(document).on('click', 'input.sqlbutton', function (evt) { - insertQuery(evt.target.id); - PMA_handleSimulateQueryButton(); - return false; - }); - - $(document).on('change', '#parameterized', updateQueryParameters); - - var $inputUsername = $('#input_username'); - if ($inputUsername) { - if ($inputUsername.val() === '') { - $inputUsername.trigger('focus'); - } else { - $('#input_password').trigger('focus'); - } - } -}); - /** * "inputRead" event handler for CodeMirror SQL query editors for autocompletion */ @@ -4316,30 +4201,6 @@ function PMA_slidingMessage (msg, $obj) { return true; } // end PMA_slidingMessage() -/** - * Attach CodeMirror2 editor to SQL edit area. - */ -AJAX.registerOnload('functions.js', function () { - var $elm = $('#sqlquery'); - if ($elm.length > 0) { - if (typeof CodeMirror !== 'undefined') { - codemirror_editor = PMA_getSQLEditor($elm); - codemirror_editor.focus(); - codemirror_editor.on('blur', updateQueryParameters); - } else { - // without codemirror - $elm.focus().on('blur', updateQueryParameters); - } - } - PMA_highlightSQL($('body')); -}); -AJAX.registerTeardown('functions.js', function () { - if (codemirror_editor) { - $('#sqlquery').text(codemirror_editor.getValue()); - codemirror_editor.toTextArea(); - codemirror_editor = false; - } -}); AJAX.registerOnload('functions.js', function () { // initializes all lock-page elements lock-id and // val-hash data property diff --git a/js/src/ajax.js b/js/src/ajax.js index a07932eebf..337e3ab981 100644 --- a/js/src/ajax.js +++ b/js/src/ajax.js @@ -8,13 +8,14 @@ import { PMA_ajaxShowMessage, } from './utils/show_ajax_messages'; import { PMA_Messages as PMA_messages } from './variables/export_variables'; import CommonParams from './variables/common_params'; -import { jQuery as $ } from './utils/JqueryExtended'; +import { $ } from './utils/JqueryExtended'; import { PMA_getImage } from './functions/get_image'; import { PMA_ensureNaviSettings, PMA_reloadNavigation, PMA_disableNaviSettings } from './functions/navigation'; import { isStorageSupported } from './functions/config'; +import PMA_MicroHistory from './classes/MicroHistory'; /** * This object handles ajax requests for pages. It also * handles the reloading of the main menu and scripts. @@ -367,7 +368,6 @@ export let AJAX = { if (typeof onsubmit !== 'function' || onsubmit.apply(this, [event])) { AJAX.active = true; AJAX.$msgbox = PMA_ajaxShowMessage(); - $.post(url, params, AJAX.responseHandler); } } @@ -599,7 +599,7 @@ export let AJAX = { var fileImports = ['server_privileges', 'server_databases', 'error_report', 'navigation', 'server_status_advisor', 'server_status_processes', 'server_status_variables', 'server_plugins', 'server_status_sorter', 'server_status_queries', 'server_status_monitor', 'server_variables', 'server_user_groups', 'replication', 'export', 'import', 'config', - 'page_settings', 'shortcuts_handler' + 'page_settings', 'shortcuts_handler', 'db_search', 'sql', 'functions', 'multi_column_sort' ]; if ($.inArray(file, fileImports) !== -1) { // Dynamic import to load the files dynamically diff --git a/js/src/classes/Chart.js b/js/src/classes/Chart.js index ddbeb75842..c13b8adb53 100644 --- a/js/src/classes/Chart.js +++ b/js/src/classes/Chart.js @@ -1,7 +1,13 @@ +import { $ } from '../utils/JqueryExtended'; +import 'updated-jqplot'; +import 'updated-jqplot/dist/plugins/jqplot.pieRenderer'; +import 'updated-jqplot/dist/plugins/jqplot.highlighter'; +import 'updated-jqplot/dist/plugins/jqplot.enhancedPieLegendRenderer'; + /** * Chart type enumerations */ -var ChartType = { +export var ChartType = { LINE : 'line', SPLINE : 'spline', AREA : 'area', @@ -15,7 +21,7 @@ var ChartType = { /** * Column type enumeration */ -var ColumnType = { +export var ColumnType = { STRING : 'string', NUMBER : 'number', BOOLEAN : 'boolean', @@ -153,7 +159,7 @@ ScatterChart.prototype.validateColumns = function (dataTable) { /** * The data table contains column information and data for the chart. */ -var DataTable = function () { +export var DataTable = function () { var columns = []; var data = null; diff --git a/js/src/classes/CommonActions.js b/js/src/classes/CommonActions.js new file mode 100644 index 0000000000..35963b424d --- /dev/null +++ b/js/src/classes/CommonActions.js @@ -0,0 +1,59 @@ +import CommonParams from '../variables/common_params'; +import { AJAX } from '../ajax'; + +/** + * Holds common parameters such as server, db, table, etc + * + * The content for this is normally loaded from Header.php or + * Response.php and executed by ajax.js + */ +export var PMA_commonActions = { + /** + * Saves the database name when it's changed + * and reloads the query window, if necessary + * + * @param {string} newDb newDb The name of the new database + * + * @return {void} + */ + setDb: function (newDb) { + if (newDb !== CommonParams.get('db')) { + CommonParams.setAll({ 'db': newDb, 'table': '' }); + } + }, + /** + * Opens a database in the main part of the page + * + * @param {string} newDb The name of the new database + * + * @return void + */ + openDb: function (newDb) { + CommonParams + .set('db', newDb) + .set('table', ''); + this.refreshMain( + CommonParams.get('opendb_url') + ); + }, + /** + * Refreshes the main frame + * + * @param mixed url Undefined to refresh to the same page + * String to go to a different page, e.g: 'index.php' + * + * @return void + */ + refreshMain: function (url, callback) { + if (! url) { + url = $('#selflink').find('a').attr('href'); + url = url.substring(0, url.indexOf('?')); + } + url += CommonParams.getUrlQuery(); + $('', { href: url }) + .appendTo('body') + .trigger('click') + .remove(); + AJAX._callback = callback; + } +}; diff --git a/js/src/classes/Console/PMA_ConsoleResizer.js b/js/src/classes/Console/PMA_ConsoleResizer.js index 215ae31b94..0f2c1193a2 100644 --- a/js/src/classes/Console/PMA_ConsoleResizer.js +++ b/js/src/classes/Console/PMA_ConsoleResizer.js @@ -1,9 +1,12 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ + /** * Resizer object * Careful: this object UI logics highly related with functions under PMA_console * Resizing min-height is 32, if small than it, console will collapse + * @namespace ConsoleResizer */ -export default class PMA_consoleResizer { +export default class ConsoleResizer { constructor (instance) { this._posY = 0; this._height = 0; diff --git a/js/src/classes/Console/PMA_consoleBookmarks.js b/js/src/classes/Console/PMA_consoleBookmarks.js index 72f41dc41b..8cbeb08a83 100644 --- a/js/src/classes/Console/PMA_consoleBookmarks.js +++ b/js/src/classes/Console/PMA_consoleBookmarks.js @@ -1,7 +1,11 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +import CommonParams from '../../variables/common_params'; +import { PMA_Messages as messages } from '../../variables/export_variables'; /** + * @namespace ConsoleBookmarks * Console bookmarks card, and bookmarks items management object */ -export default class PMA_consoleBookmarks { +export default class ConsoleBookmarks { constructor (instance) { this._bookmarks = []; this.pmaConsole = null; @@ -15,7 +19,7 @@ export default class PMA_consoleBookmarks { this.pmaConsole = instance; this.initialize(); } - addBookmark (queryString, targetDb, label, isShared, id) { + addBookmark (queryString, targetDb, label, isShared) { $('#pma_bookmarks').find('.add [name=shared]').prop('checked', false); $('#pma_bookmarks').find('.add [name=label]').val(''); $('#pma_bookmarks').find('.add [name=targetdb]').val(''); @@ -42,7 +46,7 @@ export default class PMA_consoleBookmarks { refresh () { $.get('import.php', { ajax_request: true, - server: PMA_commonParams.get('server'), + server: CommonParams.get('server'), console_bookmark_refresh: 'refresh' }, function (data) { if (data.console_message_bookmark) { @@ -71,7 +75,7 @@ export default class PMA_consoleBookmarks { $('#pma_bookmarks').find('.card.add [name=submit]').click(function () { if ($('#pma_bookmarks').find('.card.add [name=label]').val().length === 0 || self.pmaConsole.pmaConsoleInput.getText('bookmark').length === 0) { - alert(PMA_messages.strFormEmpty); + alert(messages.strFormEmpty); return; } $(this).prop('disabled', true); @@ -80,7 +84,7 @@ export default class PMA_consoleBookmarks { ajax_request: true, console_bookmark_add: 'true', label: $('#pma_bookmarks').find('.card.add [name=label]').val(), - server: PMA_commonParams.get('server'), + server: CommonParams.get('server'), db: $('#pma_bookmarks').find('.card.add [name=targetdb]').val(), bookmark_query: self.pmaConsole.pmaConsoleInput.getText('bookmark'), shared: $('#pma_bookmarks').find('.card.add [name=shared]').prop('checked') }, diff --git a/js/src/classes/Console/PMA_consoleDebug.js b/js/src/classes/Console/PMA_consoleDebug.js index ad063172c4..29523db8dc 100644 --- a/js/src/classes/Console/PMA_consoleDebug.js +++ b/js/src/classes/Console/PMA_consoleDebug.js @@ -1,4 +1,17 @@ -export default class PMA_consoleDebug { +/* vim: set expandtab sw=4 ts=4 sts=4: */ + +/** + * Module import + */ +import { PMA_Messages as messages } from '../../variables/export_variables'; +import { PMA_sprintf } from '../../utils/sprintf'; +import { escapeHtml } from '../../utils/Sanitise'; + +/** + * Console debug object + * @namespace ConsoleDebug + */ +export default class ConsoleDebug { constructor (instance) { this.pmaConsole = null; this._config = { @@ -123,7 +136,7 @@ export default class PMA_consoleDebug { $('
') .text( PMA_sprintf( - PMA_messages.strConsoleDebugArgsSummary, + messages.strConsoleDebugArgsSummary, dbgStep.args.length ) ) @@ -186,12 +199,12 @@ export default class PMA_consoleDebug { $('
') .append( '' + - PMA_messages.strConsoleDebugShowArgs + + messages.strConsoleDebugShowArgs + ' ' ) .append( '' + - PMA_messages.strConsoleDebugHideArgs + + messages.strConsoleDebugHideArgs + ' ' ) ); @@ -259,7 +272,7 @@ export default class PMA_consoleDebug { .text((parseInt(i) + 1) + '.') .append( $('').text( - PMA_messages.strConsoleDebugTimeTaken + + messages.strConsoleDebugTimeTaken + ' ' + queryInfo[i].time + 's' + ' (' + ((queryInfo[i].time * 100) / totalTime).toFixed(3) + '%)' ) @@ -304,7 +317,7 @@ export default class PMA_consoleDebug { } if (debugJson === false) { $('#debug_console').find('.debug>.welcome').text( - PMA_messages.strConsoleDebugError + messages.strConsoleDebugError ); return; } @@ -336,7 +349,7 @@ export default class PMA_consoleDebug { $('#debug_console').find('.debug>.welcome').append( $('').text( PMA_sprintf( - PMA_messages.strConsoleDebugSummary, + messages.strConsoleDebugSummary, totalUnique, totalExec, totalTime @@ -351,7 +364,7 @@ export default class PMA_consoleDebug { // For sorting queries function sortByTime (a, b) { - var order = ((PMA_console.config.Order === 'asc') ? 1 : -1); + var order = ((this.pmaConsole.config.Order === 'asc') ? 1 : -1); if (Array.isArray(a) && Array.isArray(b)) { // It is grouped var timeA = 0; @@ -370,7 +383,7 @@ export default class PMA_consoleDebug { } function sortByCount (a, b) { - var order = ((PMA_console.config.Oorder === 'asc') ? 1 : -1); + var order = ((this.pmaConsole.config.Order === 'asc') ? 1 : -1); return (a.length - b.length) * order; } diff --git a/js/src/classes/Console/PMA_consoleInput.js b/js/src/classes/Console/PMA_consoleInput.js index 5e64be11fd..ebf82ae7c7 100644 --- a/js/src/classes/Console/PMA_consoleInput.js +++ b/js/src/classes/Console/PMA_consoleInput.js @@ -1,7 +1,24 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +import CodeMirror from 'codemirror'; +import 'codemirror/mode/sql/sql.js'; +import 'codemirror/addon/runmode/runmode.js'; +import 'codemirror/addon/hint/show-hint.js'; +import 'codemirror/addon/hint/sql-hint.js'; +import 'codemirror/addon/lint/lint.js'; +import '../../plugins/codemirror/sql-lint'; +import { codemirrorAutocompleteOnInputRead } from '../../utils/sql'; +import CommonParams from '../../variables/common_params'; + /** * Console input object + * @namespace ConsoleInput */ -export default class PMA_consoleInput { +export default class ConsoleInput { + /** + * @constructor + * + * @param {object} pmaConsoleInstance Instance of pma console + */ constructor (pmaConsoleInstance) { /** * @var array, contains Codemirror objects or input jQuery objects @@ -23,9 +40,16 @@ export default class PMA_consoleInput { * @access private */ this._historyPreserveCurrent = null; - + /** + * @var object + * @access private + */ this.pmaConsole = null; + /** + * Bindings for accessing the instance of the class using this + * insde the methods. + */ this.setPmaConsole = this.setPmaConsole.bind(this); this.initialize = this.initialize.bind(this); this._historyNavigate = this._historyNavigate.bind(this); @@ -48,7 +72,7 @@ export default class PMA_consoleInput { if (this._inputs !== null) { return; } - if (typeof CodeMirror !== 'undefined') { + if (CommonParams.get('CodemirrorEnable') === true) { this._codemirror = true; } this._inputs = []; @@ -214,6 +238,7 @@ export default class PMA_consoleInput { * * @param string text * @param string target + * * @return void */ setText (text, target) { @@ -237,6 +262,13 @@ export default class PMA_consoleInput { } } } + /** + * Used for getting the text of input + * + * @param {string} target + * + * @return {string} + */ getText (target) { if (this._codemirror) { switch (target) { diff --git a/js/src/classes/Console/PMA_consoleMessages.js b/js/src/classes/Console/PMA_consoleMessages.js index 2398a0d12b..289d6fec70 100644 --- a/js/src/classes/Console/PMA_consoleMessages.js +++ b/js/src/classes/Console/PMA_consoleMessages.js @@ -1,7 +1,15 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +import CodeMirror from 'codemirror'; +import PMA_commonParams from '../../variables/common_params'; +/** + * Module import + */ +import { PMA_Messages as messages } from '../../variables/export_variables'; /** * Console messages, and message items management object + * @namespace ConsoleMessages */ -export default class PMA_consoleMessages { +export default class ConsoleMessages { constructor (instance) { this.pmaConsole = null; this.clear = this.clear.bind(this); @@ -167,7 +175,7 @@ export default class PMA_consoleMessages { $targetMessage.find('.action.requery').click(function () { var query = $(this).parent().siblings('.query').text(); var $message = $(this).closest('.message'); - if (confirm(PMA_messages.strConsoleRequeryConfirm + '\n' + + if (confirm(messages.strConsoleRequeryConfirm + '\n' + (query.length < 100 ? query : query.slice(0, 100) + '...')) ) { self.pmaConsole.execute(query, { db: $message.attr('targetdb'), table: $message.attr('targettable') }); @@ -189,7 +197,7 @@ export default class PMA_consoleMessages { }); $targetMessage.find('.action.delete_bookmark').click(function () { var $message = $(this).closest('.message'); - if (confirm(PMA_messages.strConsoleDeleteBookmarkConfirm + '\n' + $message.find('.bookmark_label').text())) { + if (confirm(messages.strConsoleDeleteBookmarkConfirm + '\n' + $message.find('.bookmark_label').text())) { $.post('import.php', { server: PMA_commonParams.get('server'), diff --git a/js/src/classes/ErrorReport.js b/js/src/classes/ErrorReport.js index b490544ceb..fc480bc08e 100644 --- a/js/src/classes/ErrorReport.js +++ b/js/src/classes/ErrorReport.js @@ -8,7 +8,7 @@ import CommonParams from '../variables/common_params'; import { PMA_ajaxShowMessage } from '../utils/show_ajax_messages'; import { PMA_getImage } from '../functions/get_image'; import TraceKit from 'tracekit'; -import { jQuery as $ } from '../utils/JqueryExtended'; +import { $ } from '../utils/JqueryExtended'; /** * This Object uses the library TraceKit to generate the backtrace of the @@ -39,6 +39,7 @@ var ErrorReport = { * @return void */ error_handler: function (exception) { + console.error(exception); if (exception.name === null || typeof(exception.name) === 'undefined') { exception.name = ErrorReport._extractExceptionName(exception); } diff --git a/js/src/classes/MicroHistory.js b/js/src/classes/MicroHistory.js new file mode 100644 index 0000000000..d7b2a49845 --- /dev/null +++ b/js/src/classes/MicroHistory.js @@ -0,0 +1,228 @@ +import { PMA_ajaxShowMessage } from '../utils/show_ajax_messages'; +import { AJAX } from '../ajax'; +import { PMA_Messages as messages } from '../variables/export_variables'; +import CommonParams from '../variables/common_params'; +import SetUrlHash from './SetUrlHash'; +/** + * An implementation of a client-side page cache. + * This object also uses the cache to provide a simple microhistory, + * that is the ability to use the back and forward buttons in the browser + */ +var MicroHistory = { + /** + * @var int The maximum number of pages to keep in the cache + */ + MAX: 6, + /** + * @var object A hash used to prime the cache with data about the initially + * loaded page. This is set in the footer, and then loaded + * by a double-queued event further down this file. + */ + primer: {}, + /** + * @var array Stores the content of the cached pages + */ + pages: [], + /** + * @var int The index of the currently loaded page + * This is used to know at which point in the history we are + */ + current: 0, + /** + * Saves a new page in the cache + * + * @param string hash The hash part of the url that is being loaded + * @param array scripts A list of scripts that is required for the page + * @param string menu A hash that links to a menu stored + * in a dedicated menu cache + * @param array params A list of parameters used by CommonParams() + * @param string rel A relationship to the current page: + * 'samepage': Forces the response to be treated as + * the same page as the current one + * 'newpage': Forces the response to be treated as + * a new page + * undefined: Default behaviour, 'samepage' if the + * selflinks of the two pages are the same. + * 'newpage' otherwise + * + * @return void + */ + add: function (hash, scripts, menu, params, rel) { + if (this.pages.length > MicroHistory.MAX) { + // Trim the cache, to the maximum number of allowed entries + // This way we will have a cached menu for every page + for (var i = 0; i < this.pages.length - this.MAX; i++) { + delete this.pages[i]; + } + } + while (this.current < this.pages.length) { + // trim the cache if we went back in the history + // and are now going forward again + this.pages.pop(); + } + if (rel === 'newpage' || + ( + typeof rel === 'undefined' && ( + typeof this.pages[this.current - 1] === 'undefined' || + this.pages[this.current - 1].hash !== hash + ) + ) + ) { + this.pages.push({ + hash: hash, + content: $('#page_content').html(), + scripts: scripts, + selflink: $('#selflink').html(), + menu: menu, + params: params + }); + SetUrlHash(this.current, hash); + this.current++; + } + }, + /** + * Restores a page from the cache. This is called when the hash + * part of the url changes and it's structure appears to be valid + * + * @param string index Which page from the history to load + * + * @return void + */ + navigate: function (index) { + if (typeof this.pages[index] === 'undefined' || + typeof this.pages[index].content === 'undefined' || + typeof this.pages[index].menu === 'undefined' || + ! MicroHistory.menus.get(this.pages[index].menu) + ) { + PMA_ajaxShowMessage( + '
' + messages.strInvalidPage + '
', + false + ); + } else { + AJAX.active = true; + var record = this.pages[index]; + AJAX.scriptHandler.reset(function () { + $('#page_content').html(record.content); + $('#selflink').html(record.selflink); + MicroHistory.menus.replace(MicroHistory.menus.get(record.menu)); + CommonParams.setAll(record.params); + AJAX.scriptHandler.load(record.scripts); + MicroHistory.current = ++index; + }); + } + }, + /** + * Resaves the content of the current page in the cache. + * Necessary in order not to show the user some outdated version of the page + * + * @return void + */ + update: function () { + var page = this.pages[this.current - 1]; + if (page) { + page.content = $('#page_content').html(); + } + }, + /** + * @var object Dedicated menu cache + */ + menus: { + /** + * Returns the number of items in an associative array + * + * @return int + */ + size: function (obj) { + var size = 0; + var key; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + size++; + } + } + return size; + }, + /** + * @var hash Stores the content of the cached menus + */ + data: {}, + /** + * Saves a new menu in the cache + * + * @param string hash The hash (trimmed md5) of the menu to be saved + * @param string content The HTML code of the menu to be saved + * + * @return void + */ + add: function (hash, content) { + if (this.size(this.data) > MicroHistory.MAX) { + // when the cache grows, we remove the oldest entry + var oldest; + var key; + var init = 0; + for (var i in this.data) { + if (this.data[i]) { + if (! init || this.data[i].timestamp.getTime() < oldest.getTime()) { + oldest = this.data[i].timestamp; + key = i; + init = 1; + } + } + } + delete this.data[key]; + } + this.data[hash] = { + content: content, + timestamp: new Date() + }; + }, + /** + * Retrieves a menu given its hash + * + * @param string hash The hash of the menu to be retrieved + * + * @return string + */ + get: function (hash) { + if (this.data[hash]) { + return this.data[hash].content; + } else { + // This should never happen as long as the number of stored menus + // is larger or equal to the number of pages in the page cache + return ''; + } + }, + /** + * Prepares part of the parameter string used during page requests, + * this is necessary to tell the server which menus we have in the cache + * + * @return string + */ + getRequestParam: function () { + var param = ''; + var menuHashes = []; + for (var i in this.data) { + menuHashes.push(i); + } + var menuHashesParam = menuHashes.join('-'); + if (menuHashesParam) { + param = CommonParams.get('arg_separator') + 'menuHashes=' + menuHashesParam; + } + return param; + }, + /** + * Replaces the menu with new content + * + * @return void + */ + replace: function (content) { + $('#floating_menubar').html(content) + // Remove duplicate wrapper + // TODO: don't send it in the response + .children().first().remove(); + $('#topmenu').menuResizer(PMA_mainMenuResizerCallback); + } + } +}; + +export default MicroHistory; diff --git a/js/src/classes/Server/ProcessList.js b/js/src/classes/Server/ProcessList.js index 1a730f9423..23b7ec031d 100644 --- a/js/src/classes/Server/ProcessList.js +++ b/js/src/classes/Server/ProcessList.js @@ -1,10 +1,10 @@ import { PMA_ajaxShowMessage } from '../../utils/show_ajax_messages'; import { PMA_highlightSQL } from '../../utils/sql'; -import { PMA_Messages as PMA_messages } from '../../variables/export_variables'; +import { PMA_Messages as messages } from '../../variables/export_variables'; import CommonParams from '../../variables/common_params'; import { PMA_getImage } from '../../functions/get_image'; import { escapeHtml } from '../../utils/Sanitise'; -import { jQuery as $ } from '../../utils/JqueryExtended'; +import { $ } from '../../utils/JqueryExtended'; // object to store process list state information class ProcessList { constructor () { @@ -129,10 +129,10 @@ class ProcessList { */ setRefreshLabel () { var img = 'play'; - var label = PMA_messages.strStartRefresh; + var label = messages.strStartRefresh; if (this.autoRefresh) { img = 'pause'; - label = PMA_messages.strStopRefresh; + label = messages.strStopRefresh; this.refresh(); } $('a#toggleRefresh').html(PMA_getImage(img) + escapeHtml(label)); diff --git a/js/src/classes/SetUrlHash.js b/js/src/classes/SetUrlHash.js new file mode 100644 index 0000000000..e0d5ffdaf9 --- /dev/null +++ b/js/src/classes/SetUrlHash.js @@ -0,0 +1,117 @@ +import MicroHistory from './MicroHistory'; +import '../plugins/jquery/jquery.ba-hashchange-1.3'; + +/** + * URL hash management module. + * Allows direct bookmarking and microhistory. + */ +var SetUrlHash = (function (jQuery, window) { + /** + * Indictaes whether we have already completed + * the initialisation of the hash + * + * @access private + */ + var ready = false; + /** + * Stores a hash that needed to be set when we were not ready + * + * @access private + */ + var savedHash = ''; + /** + * Flag to indicate if the change of hash was triggered + * by a user pressing the back/forward button or if + * the change was triggered internally + * + * @access private + */ + var userChange = true; + + // Fix favicon disappearing in Firefox when setting location.hash + function resetFavicon () { + if (navigator.userAgent.indexOf('Firefox') > -1) { + // Move the link tags for the favicon to the bottom + // of the head element to force a reload of the favicon + $('head > link[href="favicon\\.ico"]').appendTo('head'); + } + } + + /** + * Sets the hash part of the URL + * + * @access public + */ + function setUrlHash (index, hash) { + /* + * Known problem: + * Setting hash leads to reload in webkit: + * http://www.quirksmode.org/bugreports/archives/2005/05/Safari_13_visual_anomaly_with_windowlocationhref.html + * + * so we expect that users are not running an ancient Safari version + */ + + userChange = false; + if (ready) { + window.location.hash = 'PMAURL-' + index + ':' + hash; + resetFavicon(); + } else { + savedHash = 'PMAURL-' + index + ':' + hash; + } + } + /** + * Start initialisation + */ + var urlhash = window.location.hash; + if (urlhash.substring(0, 8) === '#PMAURL-') { + // We have a valid hash, let's redirect the user + // to the page that it's pointing to + var colon_position = urlhash.indexOf(':'); + var questionmark_position = urlhash.indexOf('?'); + if (colon_position !== -1 && questionmark_position !== -1 && colon_position < questionmark_position) { + var hash_url = urlhash.substring(colon_position + 1, questionmark_position); + if (window.PMA_gotoWhitelist.indexOf(hash_url) !== -1) { + window.location = urlhash.substring( + colon_position + 1 + ); + } + } + } else { + // We don't have a valid hash, so we'll set it up + // when the page finishes loading + jQuery(function () { + /* Check if we should set URL */ + if (savedHash !== '') { + window.location.hash = savedHash; + savedHash = ''; + resetFavicon(); + } + // Indicate that we're done initialising + ready = true; + }); + } + /** + * Register an event handler for when the url hash changes + */ + + jQuery(function () { + jQuery(window).hashchange(function () { + if (userChange === false) { + // Ignore internally triggered hash changes + userChange = true; + } else if (/^#PMAURL-\d+:/.test(window.location.hash)) { + // Change page if the hash changed was triggered by a user action + var index = window.location.hash.substring( + 8, window.location.hash.indexOf(':') + ); + MicroHistory.navigate(index); + } + }); + }); + /** + * Publicly exposes a reference to the otherwise private setUrlHash function + */ + return setUrlHash; +}(jQuery, window)); + +export default SetUrlHash; diff --git a/js/src/config.js b/js/src/config.js index 40a4a5e49e..1a247a428f 100644 --- a/js/src/config.js +++ b/js/src/config.js @@ -3,13 +3,15 @@ import * as Config from './functions/config'; import { defaultValues } from './variables/get_config'; /** - * Functions used in configuration forms and on user preferences pages + * @package PhpMyAdmin + * + * Config */ /** * Unbind all event handlers before tearing down a page */ -export function teardown1 () { +function teardownConfig () { $('.optbox input[id], .optbox select[id], .optbox textarea[id]').off('change').off('keyup'); $('.optbox input[type=button][name=submit_reset]').off('click'); $('div.tabs_contents').off(); @@ -19,7 +21,7 @@ export function teardown1 () { $('#prefs_autoload').find('a').off('click'); } -export function onload1 () { +function onloadConfigPrefsTab () { var $topmenu_upt = $('#topmenu2.user_prefs_tabs'); $topmenu_upt.find('li.active a').attr('rel', 'samepage'); $topmenu_upt.find('li:not(.active) a').attr('rel', 'newpage'); @@ -29,7 +31,7 @@ export function onload1 () { // Form validation and field operations // -export function onload2 () { +function onloadConfigValidations () { Config.setupValidation(); } @@ -41,7 +43,7 @@ export function onload2 () { // Tabbed forms // -export function onload3 () { +function onloadConfigTabs () { Config.setupConfigTabs(); Config.adjustPrefsNotification(); @@ -72,7 +74,7 @@ export function onload3 () { // Form reset buttons // -export function onload4 () { +function onloadConfigResetDefault () { $('.optbox input[type=button][name=submit_reset]').on('click', function () { var fields = $(this).closest('fieldset').find('input, select, textarea'); for (var i = 0, imax = fields.length; i < imax; i++) { @@ -89,7 +91,7 @@ export function onload4 () { // "Restore default" and "set value" buttons // -export function onload5 () { +function onloadConfigRestore () { Config.setupRestoreField(); } @@ -101,7 +103,7 @@ export function onload5 () { // User preferences import/export // -export function onload6 () { +function onloadPreferenceExport () { Config.offerPrefsAutoimport(); var $radios = $('#import_local_storage, #export_local_storage'); if (!$radios.length) { @@ -113,15 +115,15 @@ export function onload6 () { .prop('disabled', false) .add('#export_text_file, #import_text_file') .on('click', function () { - var enable_id = $(this).attr('id'); - var disable_id; - if (enable_id.match(/local_storage$/)) { - disable_id = enable_id.replace(/local_storage$/, 'text_file'); + var enableId = $(this).attr('id'); + var disableId; + if (enableId.match(/local_storage$/)) { + disableId = enableId.replace(/local_storage$/, 'text_file'); } else { - disable_id = enable_id.replace(/text_file$/, 'local_storage'); + disableId = enableId.replace(/text_file$/, 'local_storage'); } - $('#opts_' + disable_id).addClass('disabled').find('input').prop('disabled', true); - $('#opts_' + enable_id).removeClass('disabled').find('input').prop('disabled', false); + $('#opts_' + disableId).addClass('disabled').find('input').prop('disabled', true); + $('#opts_' + enableId).removeClass('disabled').find('input').prop('disabled', false); }); // detect localStorage state @@ -135,9 +137,9 @@ export function onload6 () { $('form.prefs-form').on('change', function () { var $form = $(this); var disabled = false; - if (!ls_supported) { + if (!lsSupported) { disabled = $form.find('input[type=radio][value$=local_storage]').prop('checked'); - } else if (!ls_exists && $form.attr('name') === 'prefs_import' && + } else if (!lsExists && $form.attr('name') === 'prefs_import' && $('#import_local_storage')[0].checked ) { disabled = true; @@ -168,3 +170,16 @@ export function onload6 () { // // END: User preferences import/export // ------------------------------------------------------------------ + +/** + * Module export + */ +export { + teardownConfig, + onloadConfigPrefsTab, + onloadConfigResetDefault, + onloadConfigRestore, + onloadConfigTabs, + onloadConfigValidations, + onloadPreferenceExport +}; diff --git a/js/src/console.js b/js/src/console.js index 527989cd29..b6a90fa379 100644 --- a/js/src/console.js +++ b/js/src/console.js @@ -4,15 +4,15 @@ * * @package phpMyAdmin-Console */ -import PMA_consoleBookmarks from './classes/Console/PMA_consoleBookmarks'; -import PMA_consoleDebug from './classes/Console/PMA_consoleDebug'; -import PMA_consoleInput from './classes/Console/PMA_consoleInput'; -import PMA_consoleMessages from './classes/Console/PMA_consoleMessages'; -import PMA_consoleResizer from './classes/Console/PMA_ConsoleResizer'; +import ConsoleBookmarks from './classes/Console/PMA_consoleBookmarks'; +import ConsoleDebug from './classes/Console/PMA_consoleDebug'; +import ConsoleInput from './classes/Console/PMA_consoleInput'; +import ConsoleMessages from './classes/Console/PMA_consoleMessages'; +import ConsoleResizer from './classes/Console/PMA_ConsoleResizer'; /** * Console object */ -var PMA_console = { +var Console = { /** * @var object, jQuery object, selector is '#pma_console>.content' * @access private @@ -69,18 +69,18 @@ var PMA_console = { return; } - PMA_console.config = configGet('Console', false); + Console.config = configGet('Console', false); - PMA_console.isEnabled = true; + Console.isEnabled = true; // Vars init - PMA_console.$consoleToolbar = $('#pma_console').find('>.toolbar'); - PMA_console.$consoleContent = $('#pma_console').find('>.content'); - PMA_console.$consoleAllContents = $('#pma_console').find('.content'); - PMA_console.$consoleTemplates = $('#pma_console').find('>.templates'); + Console.$consoleToolbar = $('#pma_console').find('>.toolbar'); + Console.$consoleContent = $('#pma_console').find('>.content'); + Console.$consoleAllContents = $('#pma_console').find('.content'); + Console.$consoleTemplates = $('#pma_console').find('>.templates'); // Generate a from for post - PMA_console.$requestForm = $('' + + Console.$requestForm = $('' + '' + '' + '' + @@ -90,36 +90,37 @@ var PMA_console = { '' + '' ); - PMA_console.$requestForm.children('[name=token]').val(PMA_commonParams.get('token')); - PMA_console.$requestForm.on('submit', AJAX.requestHandler); + Console.$requestForm.children('[name=token]').val(PMA_commonParams.get('token')); + Console.$requestForm.on('submit', AJAX.requestHandler); // Event binds shouldn't run again - if (PMA_console.isInitialized === false) { + if (Console.isInitialized === false) { // Load config first - if (PMA_console.config.AlwaysExpand === true) { + if (Console.config.AlwaysExpand === true) { $('#pma_console_options input[name=always_expand]').prop('checked', true); } - if (PMA_console.config.StartHistory === true) { + if (Console.config.StartHistory === true) { $('#pma_console_options').find('input[name=start_history]').prop('checked', true); } - if (PMA_console.config.CurrentQuery === true) { + if (Console.config.CurrentQuery === true) { $('#pma_console_options').find('input[name=current_query]').prop('checked', true); } - if (PMA_console.config.EnterExecutes === true) { + if (Console.config.EnterExecutes === true) { $('#pma_console_options').find('input[name=enter_executes]').prop('checked', true); } - if (PMA_console.config.DarkTheme === true) { + if (Console.config.DarkTheme === true) { $('#pma_console_options').find('input[name=dark_theme]').prop('checked', true); $('#pma_console').find('>.content').addClass('console_dark_theme'); } - PMA_console.pmaConsoleResizer = new PMA_consoleResizer(PMA_console); - PMA_console.pmaConsoleInput = new PMA_consoleInput(PMA_console); - PMA_console.pmaConsoleMessages = new PMA_consoleMessages(PMA_console); - PMA_console.pmaConsoleBookmarks = new PMA_consoleBookmarks(PMA_console); - PMA_console.pmaConsoleDebug = new PMA_consoleDebug(PMA_console); + // Instances of helper classes + Console.pmaConsoleResizer = new ConsoleResizer(Console); + Console.pmaConsoleInput = new ConsoleInput(Console); + Console.pmaConsoleMessages = new ConsoleMessages(Console); + Console.pmaConsoleBookmarks = new ConsoleBookmarks(Console); + Console.pmaConsoleDebug = new ConsoleDebug(Console); - PMA_console.$consoleToolbar.children('.console_switch').click(PMA_console.toggle); + Console.$consoleToolbar.children('.console_switch').click(Console.toggle); $('#pma_console').find('.toolbar').children().mousedown(function (event) { event.preventDefault(); @@ -127,42 +128,42 @@ var PMA_console = { }); $('#pma_console').find('.button.clear').click(function () { - PMA_console.pmaConsoleMessages.clear(); + Console.pmaConsoleMessages.clear(); }); $('#pma_console').find('.button.history').click(function () { - PMA_console.pmaConsoleMessages.showHistory(); + Console.pmaConsoleMessages.showHistory(); }); $('#pma_console').find('.button.options').click(function () { - PMA_console.showCard('#pma_console_options'); + Console.showCard('#pma_console_options'); }); $('#pma_console').find('.button.debug').click(function () { - PMA_console.showCard('#debug_console'); + Console.showCard('#debug_console'); }); - PMA_console.$consoleContent.click(function (event) { + Console.$consoleContent.click(function (event) { if (event.target === this) { - PMA_console.pmaConsoleInput.focus(); + Console.pmaConsoleInput.focus(); } }); $('#pma_console').find('.mid_layer').click(function () { - PMA_console.hideCard($(this).parent().children('.card')); + Console.hideCard($(this).parent().children('.card')); }); $('#debug_console').find('.switch_button').click(function () { - PMA_console.hideCard($(this).closest('.card')); + Console.hideCard($(this).closest('.card')); }); $('#pma_bookmarks').find('.switch_button').click(function () { - PMA_console.hideCard($(this).closest('.card')); + Console.hideCard($(this).closest('.card')); }); $('#pma_console_options').find('.switch_button').click(function () { - PMA_console.hideCard($(this).closest('.card')); + Console.hideCard($(this).closest('.card')); }); $('#pma_console_options').find('input[type=checkbox]').change(function () { - PMA_console.updateConfig(); + Console.updateConfig(); }); $('#pma_console_options').find('.button.default').click(function () { @@ -171,11 +172,11 @@ var PMA_console = { $('#pma_console_options').find('input[name=current_query]').prop('checked', true); $('#pma_console_options').find('input[name=enter_executes]').prop('checked', false); $('#pma_console_options').find('input[name=dark_theme]').prop('checked', false); - PMA_console.updateConfig(); + Console.updateConfig(); }); $('#pma_console_options').find('input[name=enter_executes]').change(function () { - PMA_console.pmaConsoleMessages.showInstructions(PMA_console.config.EnterExecutes); + Console.pmaConsoleMessages.showInstructions(Console.config.EnterExecutes); }); $(document).ajaxComplete(function (event, xhr, ajaxOptions) { @@ -187,32 +188,32 @@ var PMA_console = { } try { var data = JSON.parse(xhr.responseText); - PMA_console.ajaxCallback(data); + Console.ajaxCallback(data); } catch (e) { console.trace(); console.log('Failed to parse JSON: ' + e.message); } }); - PMA_console.isInitialized = true; + Console.isInitialized = true; } // Change console mode from cookie - switch (PMA_console.config.Mode) { + switch (Console.config.Mode) { case 'collapse': - PMA_console.collapse(); + Console.collapse(); break; /* jshint -W086 */// no break needed in default section case 'info': /* jshint +W086 */ - PMA_console.info(); + Console.info(); break; case 'show': - PMA_console.show(true); - PMA_console.scrollBottom(); + Console.show(true); + Console.scrollBottom(); break; default: - PMA_console.setConfig('Mode', 'info'); + Console.setConfig('Mode', 'info'); } }, /** @@ -224,40 +225,40 @@ var PMA_console = { if (typeof(queryString) !== 'string' || ! /[a-z]|[A-Z]/.test(queryString)) { return; } - PMA_console.$requestForm.children('textarea').val(queryString); - PMA_console.$requestForm.children('[name=server]').attr('value', PMA_commonParams.get('server')); + Console.$requestForm.children('textarea').val(queryString); + Console.$requestForm.children('[name=server]').attr('value', PMA_commonParams.get('server')); if (options && options.db) { - PMA_console.$requestForm.children('[name=db]').val(options.db); + Console.$requestForm.children('[name=db]').val(options.db); if (options.table) { - PMA_console.$requestForm.children('[name=table]').val(options.table); + Console.$requestForm.children('[name=table]').val(options.table); } else { - PMA_console.$requestForm.children('[name=table]').val(''); + Console.$requestForm.children('[name=table]').val(''); } } else { - PMA_console.$requestForm.children('[name=db]').val( + Console.$requestForm.children('[name=db]').val( (PMA_commonParams.get('db').length > 0 ? PMA_commonParams.get('db') : '')); } - PMA_console.$requestForm.find('[name=profiling]').remove(); + Console.$requestForm.find('[name=profiling]').remove(); if (options && options.profiling === true) { - PMA_console.$requestForm.append(''); + Console.$requestForm.append(''); } - if (! confirmQuery(PMA_console.$requestForm[0], PMA_console.$requestForm.children('textarea')[0].value)) { + if (! confirmQuery(Console.$requestForm[0], Console.$requestForm.children('textarea')[0].value)) { return; } - PMA_console.$requestForm.children('[name=console_message_id]') - .val(PMA_console.pmaConsoleMessages.appendQuery({ sql_query: queryString }).message_id); - PMA_console.$requestForm.trigger('submit'); - PMA_console.pmaConsoleInput.clear(); + Console.$requestForm.children('[name=console_message_id]') + .val(Console.pmaConsoleMessages.appendQuery({ sql_query: queryString }).message_id); + Console.$requestForm.trigger('submit'); + Console.pmaConsoleInput.clear(); PMA_reloadNavigation(); }, ajaxCallback: function (data) { if (data && data.console_message_id) { - PMA_console.pmaConsoleMessages.updateQuery(data.console_message_id, data.success, + Console.pmaConsoleMessages.updateQuery(data.console_message_id, data.success, (data._reloadQuerywindow ? data._reloadQuerywindow : false)); } else if (data && data._reloadQuerywindow) { if (data._reloadQuerywindow.sql_query.length > 0) { - PMA_console.pmaConsoleMessages.appendQuery(data._reloadQuerywindow, 'successed') - .$message.addClass(PMA_console.config.CurrentQuery ? '' : 'hide'); + Console.pmaConsoleMessages.appendQuery(data._reloadQuerywindow, 'successed') + .$message.addClass(Console.config.CurrentQuery ? '' : 'hide'); } } }, @@ -267,18 +268,18 @@ var PMA_console = { * @return void */ collapse: function () { - PMA_console.setConfig('Mode', 'collapse'); - var pmaConsoleHeight = Math.max(92, PMA_console.config.Height); + Console.setConfig('Mode', 'collapse'); + var pmaConsoleHeight = Math.max(92, Console.config.Height); - PMA_console.$consoleToolbar.addClass('collapsed'); - PMA_console.$consoleAllContents.height(pmaConsoleHeight); - PMA_console.$consoleContent.stop(); - PMA_console.$consoleContent.animate({ 'margin-bottom': -1 * PMA_console.$consoleContent.outerHeight() + 'px' }, + Console.$consoleToolbar.addClass('collapsed'); + Console.$consoleAllContents.height(pmaConsoleHeight); + Console.$consoleContent.stop(); + Console.$consoleContent.animate({ 'margin-bottom': -1 * Console.$consoleContent.outerHeight() + 'px' }, 'fast', 'easeOutQuart', function () { - PMA_console.$consoleContent.css({ display:'none' }); + Console.$consoleContent.css({ display:'none' }); $(window).trigger('resize'); }); - PMA_console.hideCard(); + Console.hideCard(); }, /** * Show console @@ -287,21 +288,21 @@ var PMA_console = { * @return void */ show: function (inputFocus) { - PMA_console.setConfig('Mode', 'show'); + Console.setConfig('Mode', 'show'); - var pmaConsoleHeight = Math.max(92, PMA_console.config.Height); - pmaConsoleHeight = Math.min(PMA_console.config.Height, (window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight) - 25); - PMA_console.$consoleContent.css({ display:'block' }); - if (PMA_console.$consoleToolbar.hasClass('collapsed')) { - PMA_console.$consoleToolbar.removeClass('collapsed'); + var pmaConsoleHeight = Math.max(92, Console.config.Height); + pmaConsoleHeight = Math.min(Console.config.Height, (window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight) - 25); + Console.$consoleContent.css({ display:'block' }); + if (Console.$consoleToolbar.hasClass('collapsed')) { + Console.$consoleToolbar.removeClass('collapsed'); } - PMA_console.$consoleAllContents.height(pmaConsoleHeight); - PMA_console.$consoleContent.stop(); - PMA_console.$consoleContent.animate({ 'margin-bottom': 0 }, + Console.$consoleAllContents.height(pmaConsoleHeight); + Console.$consoleContent.stop(); + Console.$consoleContent.animate({ 'margin-bottom': 0 }, 'fast', 'easeOutQuart', function () { $(window).trigger('resize'); if (inputFocus) { - PMA_console.pmaConsoleInput.focus(); + Console.pmaConsoleInput.focus(); } }); }, @@ -314,7 +315,7 @@ var PMA_console = { */ info: function () { // Under construction - PMA_console.collapse(); + Console.collapse(); }, /** * Toggle console mode between collapse/show @@ -323,13 +324,13 @@ var PMA_console = { * @return void */ toggle: function () { - switch (PMA_console.config.Mode) { + switch (Console.config.Mode) { case 'collapse': case 'info': - PMA_console.show(true); + Console.show(true); break; case 'show': - PMA_console.collapse(); + Console.collapse(); break; default: PMA_consoleInitialize(); @@ -341,7 +342,7 @@ var PMA_console = { * @return void */ scrollBottom: function () { - PMA_console.$consoleContent.scrollTop(PMA_console.$consoleContent.prop('scrollHeight')); + Console.$consoleContent.scrollTop(Console.$consoleContent.prop('scrollHeight')); }, /** * Show card @@ -367,9 +368,9 @@ var PMA_console = { } $card.parent().children('.mid_layer').show().fadeTo(0, 0.15); $card.addClass('show'); - PMA_console.pmaConsoleInput.blur(); + Console.pmaConsoleInput.blur(); if ($card.parents('.card').length > 0) { - PMA_console.showCard($card.parents('.card')); + Console.showCard($card.parents('.card')); } }, /** @@ -394,20 +395,20 @@ var PMA_console = { * @return void */ updateConfig: function () { - PMA_console.setConfig('AlwaysExpand', $('#pma_console_options input[name=always_expand]').prop('checked')); - PMA_console.setConfig('StartHistory', $('#pma_console_options').find('input[name=start_history]').prop('checked')); - PMA_console.setConfig('CurrentQuery', $('#pma_console_options').find('input[name=current_query]').prop('checked')); - PMA_console.setConfig('EnterExecutes', $('#pma_console_options').find('input[name=enter_executes]').prop('checked')); - PMA_console.setConfig('DarkTheme', $('#pma_console_options').find('input[name=dark_theme]').prop('checked')); + Console.setConfig('AlwaysExpand', $('#pma_console_options input[name=always_expand]').prop('checked')); + Console.setConfig('StartHistory', $('#pma_console_options').find('input[name=start_history]').prop('checked')); + Console.setConfig('CurrentQuery', $('#pma_console_options').find('input[name=current_query]').prop('checked')); + Console.setConfig('EnterExecutes', $('#pma_console_options').find('input[name=enter_executes]').prop('checked')); + Console.setConfig('DarkTheme', $('#pma_console_options').find('input[name=dark_theme]').prop('checked')); /* Setting the dark theme of the console*/ - if (PMA_console.config.DarkTheme) { + if (Console.config.DarkTheme) { $('#pma_console').find('>.content').addClass('console_dark_theme'); } else { $('#pma_console').find('>.content').removeClass('console_dark_theme'); } }, setConfig: function (key, value) { - PMA_console.config[key] = value; + Console.config[key] = value; configSet('Console/' + key, value); }, isSelect: function (queryString) { @@ -416,4 +417,4 @@ var PMA_console = { } }; -export default PMA_console; +export default Console; diff --git a/js/src/consts/files.js b/js/src/consts/files.js index 083a0a1f78..a309f9e0a4 100644 --- a/js/src/consts/files.js +++ b/js/src/consts/files.js @@ -7,7 +7,7 @@ * @type {Object} files */ const files = { - global: ['error_report', 'config', 'navigation', 'page_settings', 'shortcuts_handler'], + global: ['error_report', 'config', 'navigation', 'page_settings', 'shortcuts_handler', 'functions'], server_privileges: ['server_privileges'], server_databases: ['server_databases'], server_status_advisor: ['server_status_advisor'], @@ -21,7 +21,11 @@ const files = { server_user_groups: ['server_user_groups'], server_replication: ['server_privileges', 'replication'], server_export: ['export'], - server_import: ['import'] + server_import: ['import'], + db_search: ['db_search', 'sql'], + server_sql: ['multi_column_sort', 'sql'], + tbl_sql: ['sql'], + db_sql: ['sql'] }; export default files; diff --git a/js/src/db_search.js b/js/src/db_search.js new file mode 100644 index 0000000000..174b4f9f39 --- /dev/null +++ b/js/src/db_search.js @@ -0,0 +1,253 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * JavaScript functions used on Database Search page + * + * @requires jQuery + * @requires js/functions.js + * + * @package PhpMyAdmin + */ + +/** + * AJAX script for the Database Search page. + * + * Actions ajaxified here: + * Retrieve result of SQL query + */ + +import { PMA_Messages as messages } from './variables/export_variables'; +import { PMA_ajaxShowMessage, PMA_sprintf, PMA_ajaxRemoveMessage } from './utils/show_ajax_messages'; +import PMA_commonParams from './variables/common_params'; +import { PMA_makegrid } from './utils/makegrid'; + +/** + * Unbind all event handlers before tearing down a page + */ +export function teardownDbSearch () { + $('a.browse_results').off('click'); + $('a.delete_results').off('click'); + $('#buttonGo').off('click'); + $('#togglesearchresultlink').off('click'); + $('#togglequerybox').off('click'); + $('#togglesearchformlink').off('click'); + $(document).off('submit', '#db_search_form.ajax'); +} + +export function onloadDbSearch () { + /** Hide the table link in the initial search result */ + let icon = PMA_getImage('s_tbl', '', { 'id': 'table-image' }).toString(); + $('#table-info').prepend(icon).hide(); + + /** Hide the browse and deleted results in the new search criteria */ + $('#buttonGo').click(function () { + $('#table-info').hide(); + $('#browse-results').hide(); + $('#sqlqueryform').hide(); + $('#togglequerybox').hide(); + }); + /** + * Prepare a div containing a link for toggle the search results + */ + $('#togglesearchresultsdiv') + /** don't show it until we have results on-screen */ + .hide(); + + /** + * Changing the displayed text according to + * the hide/show criteria in search result forms + */ + $('#togglesearchresultlink') + .html(messages.strHideSearchResults) + .on('click', function () { + let $link = $(this); + $('#searchresults').slideToggle(); + if ($link.text() === messages.strHideSearchResults) { + $link.text(messages.strShowSearchResults); + } else { + $link.text(messages.strHideSearchResults); + } + /** avoid default click action */ + return false; + }); + + /** + * Prepare a div containing a link for toggle the search form, + * otherwise it's incorrectly displayed after a couple of clicks + */ + $('#togglesearchformdiv') + .hide(); // don't show it until we have results on-screen + + /** + * Changing the displayed text according to + * the hide/show criteria in search form + */ + $('#togglequerybox') + .hide() + .on('click', function () { + let $link = $(this); + $('#sqlqueryform').slideToggle('medium'); + if ($link.text() === messages.strHideQueryBox) { + $link.text(messages.strShowQueryBox); + } else { + $link.text(messages.strHideQueryBox); + } + /** avoid default click action */ + return false; + }); + + /** don't show it until we have results on-screen */ + + /** + * Changing the displayed text according to + * the hide/show criteria in search criteria form + */ + $('#togglesearchformlink') + .html(messages.strShowSearchCriteria) + .on('click', function () { + let $link = $(this); + $('#db_search_form').slideToggle(); + if ($link.text() === messages.strHideSearchCriteria) { + $link.text(messages.strShowSearchCriteria); + } else { + $link.text(messages.strHideSearchCriteria); + } + /** avoid default click action */ + return false; + }); + + /* + * Ajax Event handler for retrieving the results from a table + */ + $(document).on('click', 'a.browse_results', function (e) { + e.preventDefault(); + /** Hides the results shown by the delete criteria */ + let $msg = PMA_ajaxShowMessage(messages.strBrowsing, false); + $('#sqlqueryform').hide(); + $('#togglequerybox').hide(); + /** Load the browse results to the page */ + $('#table-info').show(); + let table_name = $(this).data('table-name'); + $('#table-link').attr({ 'href' : $(this).attr('href') }).text(table_name); + + let url = `${$(this).attr('href')} + #searchresults`; + + let browse_sql = $(this).data('browse-sql'); + let params = { + 'ajax_request': true, + 'is_js_confirmed': true, + 'sql_query' : browse_sql + }; + $.post(url, params, function (data) { + if (typeof data !== 'undefined' && data.success) { + $('#browse-results').html(data.message); + PMA_ajaxRemoveMessage($msg); + $('.table_results').each(function () { + PMA_makegrid(this); + }); + $('#browse-results').show(); + PMA_highlightSQL($('#browse-results')); + $('html, body') + .animate({ + scrollTop: $('#browse-results').offset().top + }, 1000); + } else { + PMA_ajaxShowMessage(data.error, false); + } + }); + }); + + /* + * Ajax Event handler for deleting the results from a table + */ + $(document).on('click', 'a.delete_results', function (e) { + e.preventDefault(); + /** Hides the results shown by the browse criteria */ + $('#table-info').hide(); + $('#sqlqueryform').hide(); + $('#togglequerybox').hide(); + /** Conformation message for deletion */ + let msg = PMA_sprintf( + messages.strConfirmDeleteResults, + $(this).data('table-name') + ); + if (confirm(msg)) { + let $msg = PMA_ajaxShowMessage(messages.strDeleting, false); + /** Load the deleted option to the page*/ + $('#sqlqueryform').html(''); + let params = { + 'ajax_request': true, + 'is_js_confirmed': true, + 'sql_query': $(this).data('delete-sql') + }; + let url = $(this).attr('href'); + + $.post(url, params, function (data) { + if (typeof data === 'undefined' || !data.success) { + PMA_ajaxShowMessage(data.error, false); + return; + } + + $('#sqlqueryform').html(data.sql_query); + /** Refresh the search results after the deletion */ + document.getElementById('buttonGo').click(); + $('#togglequerybox').html(messages.strHideQueryBox); + /** Show the results of the deletion option */ + $('#browse-results').hide(); + $('#sqlqueryform').show(); + $('#togglequerybox').show(); + $('html, body') + .animate({ + scrollTop: $('#browse-results').offset().top + }, 1000); + PMA_ajaxRemoveMessage($msg); + }); + } + }); + + /** + * Ajax Event handler for retrieving the result of an SQL Query + */ + $(document).on('submit', '#db_search_form.ajax', function (event) { + event.preventDefault(); + var $msgbox = PMA_ajaxShowMessage(messages.strSearching, false); + // jQuery object to reuse + var $form = $(this); + + PMA_prepareForAjaxRequest($form); + + var url = `${$form.serialize()} + ${PMA_commonParams.get('arg_separator')}submit_search=${$('#buttonGo').val()}`; + $.post($form.attr('action'), url, function (data) { + if (typeof data !== 'undefined' && data.success === true) { + // found results + $('#searchresults').html(data.message); + + $('#togglesearchresultlink') + // always start with the Show message + .text(messages.strHideSearchResults); + $('#togglesearchresultsdiv') + // now it's time to show the div containing the link + .show(); + $('#searchresults').show(); + + + $('#db_search_form') + // workaround for Chrome problem (bug #3168569) + .slideToggle() + .hide(); + $('#togglesearchformlink') + // always start with the Show message + .text(messages.strShowSearchCriteria); + $('#togglesearchformdiv') + // now it's time to show the div containing the link + .show(); + } else { + // error message (zero rows) + $('#searchresults').html(data.error).show(); + } + + PMA_ajaxRemoveMessage($msgbox); + }); + }); +} diff --git a/js/src/export.js b/js/src/export.js index e49b15e345..f14e24185b 100644 --- a/js/src/export.js +++ b/js/src/export.js @@ -251,7 +251,7 @@ function onloadExportOptions () { .parent() .fadeTo('fast', 0.4); - Export.setupTableStructureOrData(); + Export.setupTableStructureOrData(); } /** diff --git a/js/src/functions.js b/js/src/functions.js new file mode 100644 index 0000000000..53c1938ab4 --- /dev/null +++ b/js/src/functions.js @@ -0,0 +1,284 @@ +import { AJAX } from './ajax'; +import CommonParams from './variables/common_params'; +import { PMA_Messages as PMA_messages } from './variables/export_variables'; + +// Sql based imports +import { PMA_getSQLEditor, bindCodeMirrorToInlineEditor } from './functions/Sql/SqlEditor'; +import { sqlQueryOptions, updateQueryParameters, PMA_highlightSQL } from './utils/sql'; +import { PMA_handleSimulateQueryButton, insertQuery, checkSqlQuery } from './functions/Sql/SqlQuery'; +import { escapeHtml } from './utils/Sanitise'; +import { getForeignKeyCheckboxLoader, loadForeignKeyCheckbox } from './functions/Sql/ForeignKey'; + +/** + * Here we register a function that will remove the onsubmit event from all + * forms that will be handled by the generic page loader. We then save this + * event handler in the "jQuery data", so that we can fire it up later in + * AJAX.requestHandler(). + * + * See bug #3583316 + */ +export function onload () { + // Registering the onload event for functions.js + // ensures that it will be fired for all pages + $('form').not('.ajax').not('.disableAjax').each(function () { + if ($(this).attr('onsubmit')) { + $(this).data('onsubmit', this.onsubmit).attr('onsubmit', ''); + } + }); + + var $page_content = $('#page_content'); + /** + * Workaround for passing submit button name,value on ajax form submit + * by appending hidden element with submit button name and value. + */ + $page_content.on('click', 'form input[type=submit]', function () { + var buttonName = $(this).attr('name'); + if (typeof buttonName === 'undefined') { + return; + } + $(this).closest('form').append($('', { + 'type' : 'hidden', + 'name' : buttonName, + 'value': $(this).val() + })); + }); + + /** + * Attach event listener to events when user modify visible + * Input,Textarea and select fields to make changes in forms + */ + $page_content.on( + 'keyup change', + 'form.lock-page textarea, ' + + 'form.lock-page input[type="text"], ' + + 'form.lock-page input[type="number"], ' + + 'form.lock-page select', + { value:1 }, + AJAX.lockPageHandler + ); + $page_content.on( + 'change', + 'form.lock-page input[type="checkbox"], ' + + 'form.lock-page input[type="radio"]', + { value:2 }, + AJAX.lockPageHandler + ); + /** + * Reset lock when lock-page form reset event is fired + * Note: reset does not bubble in all browser so attach to + * form directly. + */ + $('form.lock-page').on('reset', function (event) { + AJAX.resetLock(); + }); +} +/** + * Unbind all event handlers before tearing down a page + */ +export function teardown1 () { + $(document).off('click', 'a.themeselect'); + $(document).off('change', '.autosubmit'); + $('a.take_theme').off('click'); +} + +export function onload1 () { + /** + * Theme selector. + */ + $(document).on('click', 'a.themeselect', function (e) { + window.open( + e.target, + 'themes', + 'left=10,top=20,width=510,height=350,scrollbars=yes,status=yes,resizable=yes' + ); + return false; + }); + + /** + * Automatic form submission on change. + */ + $(document).on('change', '.autosubmit', function (e) { + e.preventDefault(); + debugger; + console.log($(this).closest('form').submit()); + $(this).closest('form').submit(); + debugger; + }); + + /** + * Theme changer. + */ + $('a.take_theme').on('click', function (e) { + var what = this.name; + if (window.opener && window.opener.document.forms.setTheme.elements.set_theme) { + window.opener.document.forms.setTheme.elements.set_theme.value = what; + window.opener.document.forms.setTheme.submit(); + window.close(); + return false; + } + return true; + }); +} + +/** + * Attach CodeMirror2 editor to SQL edit area. + */ +export function onloadSqlEditor () { + var $elm = $('#sqlquery'); + if ($elm.length > 0) { + if (CommonParams.get('CodemirrorEnable') === true) { + sqlQueryOptions.codemirror_editor = PMA_getSQLEditor($elm); + sqlQueryOptions.codemirror_editor.focus(); + sqlQueryOptions.codemirror_editor.on('blur', updateQueryParameters); + } else { + // without codemirror + $elm.focus().on('blur', updateQueryParameters); + } + } + PMA_highlightSQL($('body')); +} +export function teardownSqlEditor () { + if (sqlQueryOptions.codemirror_editor) { + $('#sqlquery').text(sqlQueryOptions.codemirror_editor.getValue()); + sqlQueryOptions.codemirror_editor.toTextArea(); + sqlQueryOptions.codemirror_editor = false; + } +} + +/** + * Unbind all event handlers before tearing down a page + */ +export function teardownSqlInlineEditor () { + $(document).off('click', 'a.inline_edit_sql'); + $(document).off('click', 'input#sql_query_edit_save'); + $(document).off('click', 'input#sql_query_edit_discard'); + $('input.sqlbutton').off('click'); + if (sqlQueryOptions.codemirror_editor) { + sqlQueryOptions.codemirror_editor.off('blur'); + } else { + $(document).off('blur', '#sqlquery'); + } + $(document).off('change', '#parameterized'); + $(document).off('click', 'input.sqlbutton'); + $('#sqlquery').off('keydown'); + $('#sql_query_edit').off('keydown'); + + if (sqlQueryOptions.codemirror_inline_editor) { + // Copy the sql query to the text area to preserve it. + $('#sql_query_edit').text(sqlQueryOptions.codemirror_inline_editor.getValue()); + $(sqlQueryOptions.codemirror_inline_editor.getWrapperElement()).off('keydown'); + sqlQueryOptions.codemirror_inline_editor.toTextArea(); + sqlQueryOptions.codemirror_inline_editor = false; + } + if (sqlQueryOptions.codemirror_editor) { + $(sqlQueryOptions.codemirror_editor.getWrapperElement()).off('keydown'); + } +} + +/** + * Jquery Coding for inline editing SQL_QUERY + */ +export function onloadSqlInlineEditor () { + // If we are coming back to the page by clicking forward button + // of the browser, bind the code mirror to inline query editor. + bindCodeMirrorToInlineEditor(); + $(document).on('click', 'a.inline_edit_sql', function () { + if ($('#sql_query_edit').length) { + // An inline query editor is already open, + // we don't want another copy of it + return false; + } + + var $form = $(this).prev('form'); + var sql_query = $form.find('input[name=\'sql_query\']').val().trim(); + var $inner_sql = $(this).parent().prev().find('code.sql'); + var old_text = $inner_sql.html(); + + var new_content = '\n'; + new_content += getForeignKeyCheckboxLoader(); + new_content += '\n'; + new_content += '\n'; + var $editor_area = $('div#inline_editor'); + if ($editor_area.length === 0) { + $editor_area = $('
'); + $editor_area.insertBefore($inner_sql); + } + $editor_area.html(new_content); + loadForeignKeyCheckbox(); + $inner_sql.hide(); + + bindCodeMirrorToInlineEditor(); + return false; + }); + + $(document).on('click', 'input#sql_query_edit_save', function (e) { + // hide already existing success message + var sql_query; + if (sqlQueryOptions.codemirror_inline_editor) { + sqlQueryOptions.codemirror_inline_editor.save(); + sql_query = sqlQueryOptions.codemirror_inline_editor.getValue(); + } else { + sql_query = $(this).parent().find('#sql_query_edit').val(); + } + var fk_check = $(this).parent().find('#fk_checks').is(':checked'); + + var $form = $('a.inline_edit_sql').prev('form'); + var $fake_form = $('
', { action: 'import.php', method: 'post' }) + .append($form.find('input[name=server], input[name=db], input[name=table], input[name=token]').clone()) + .append($('', { type: 'hidden', name: 'show_query', value: 1 })) + .append($('', { type: 'hidden', name: 'is_js_confirmed', value: 0 })) + .append($('', { type: 'hidden', name: 'sql_query', value: sql_query })) + .append($('', { type: 'hidden', name: 'fk_checks', value: fk_check ? 1 : 0 })); + if (! checkSqlQuery($fake_form[0])) { + return false; + } + $('.success').hide(); + $fake_form.appendTo($('body')).submit(); + }); + + $(document).on('click', 'input#sql_query_edit_discard', function () { + var $divEditor = $('div#inline_editor_outer'); + $divEditor.siblings('code.sql').show(); + $divEditor.remove(); + }); + + $(document).on('click', 'input.sqlbutton', function (evt) { + insertQuery(evt.target.id); + PMA_handleSimulateQueryButton(); + return false; + }); + + $(document).on('change', '#parameterized', updateQueryParameters); + + var $inputUsername = $('#input_username'); + if ($inputUsername) { + if ($inputUsername.val() === '') { + $inputUsername.trigger('focus'); + } else { + $('#input_password').trigger('focus'); + } + } +} + +/** + * Unbind all event handlers before tearing down a page + */ +export function teardownCtrlEnterFormSubmit () { + $(document).off('keydown', 'form input, form textarea, form select'); +} + +export function onloadCtrlEnterFormSubmit () { + /** + * Handle 'Ctrl/Alt + Enter' form submits + */ + $('form input, form textarea, form select').on('keydown', function (e) { + if ((e.ctrlKey && e.which === 13) || (e.altKey && e.which === 13)) { + var $form = $(this).closest('form'); + if (! $form.find('input[type="submit"]') || + ! $form.find('input[type="submit"]').trigger('click') + ) { + $form.submit(); + } + } + }); +} diff --git a/js/src/functions/ColumnSorting.js b/js/src/functions/ColumnSorting.js new file mode 100644 index 0000000000..3bbf631181 --- /dev/null +++ b/js/src/functions/ColumnSorting.js @@ -0,0 +1,49 @@ +import PMA_commonParams from '../variables/common_params'; + +function captureURL (url) { + var URL = {}; + url = '' + url; + // Exclude the url part till HTTP + url = url.substr(url.search('sql.php'), url.length); + // The url part between ORDER BY and &session_max_rows needs to be replaced. + URL.head = url.substr(0, url.indexOf('ORDER+BY') + 9); + URL.tail = url.substr(url.indexOf('&session_max_rows'), url.length); + return URL; +} + +/** + * This function is for navigating to the generated URL + * + * @param object target HTMLAnchor element + * @param object parent HTMLDom Object + */ + +export function removeColumnFromMultiSort (target, parent) { + var URL = captureURL(target); + var begin = target.indexOf('ORDER+BY') + 8; + var end = target.indexOf(PMA_commonParams.get('arg_separator') + 'session_max_rows'); + // get the names of the columns involved + var between_part = target.substr(begin, end - begin); + var columns = between_part.split('%2C+'); + // If the given column is not part of the order clause exit from this function + var index = parent.find('small').length ? parent.find('small').text() : ''; + if (index === '') { + return ''; + } + // Remove the current clicked column + columns.splice(index - 1, 1); + // If all the columns have been removed dont submit a query with nothing + // After order by clause. + if (columns.length === 0) { + var head = URL.head; + head = head.slice(0,head.indexOf('ORDER+BY')); + URL.head = head; + // removing the last sort order should have priority over what + // is remembered via the RememberSorting directive + URL.tail += PMA_commonParams.get('arg_separator') + 'discard_remembered_sort=1'; + } + URL.head = URL.head.substring(URL.head.indexOf('?') + 1); + var middle_part = columns.join('%2C+'); + var params = URL.head + middle_part + URL.tail; + return params; +} diff --git a/js/src/functions/Common.js b/js/src/functions/Common.js new file mode 100644 index 0000000000..750fe342bf --- /dev/null +++ b/js/src/functions/Common.js @@ -0,0 +1,46 @@ +import { PMA_messages as PMA_messages } from '../variables//export_variables'; +import { PMA_sprintf } from '../utils/sprintf'; +import PMA_commonParams from '../variables/common_params'; +/** + * Displays a confirmation box before submitting a "DROP/DELETE/ALTER" query. + * This function is called while clicking links + * + * @param theLink object the link + * @param theSqlQuery object the sql query to submit + * + * @return boolean whether to run the query or not + */ +export function confirmLink (theLink, theSqlQuery) { + // Confirmation is not required in the configuration file + // or browser is Opera (crappy js implementation) + if (PMA_messages.strDoYouReally === '' || typeof(window.opera) !== 'undefined') { + return true; + } + + var is_confirmed = confirm(PMA_sprintf(PMA_messages.strDoYouReally, theSqlQuery)); + if (is_confirmed) { + if (typeof(theLink.href) !== 'undefined') { + theLink.href += PMA_commonParams.get('arg_separator') + 'is_js_confirmed=1'; + } else if (typeof(theLink.form) !== 'undefined') { + theLink.form.action += '?is_js_confirmed=1'; + } + } + + return is_confirmed; +} // end of the 'confirmLink()' function + +export function getJSConfirmCommonParam (elem, params) { + var $elem = $(elem); + var sep = PMA_commonParams.get('arg_separator'); + if (params) { + // Strip possible leading ? + if (params.substring(0,1) === '?') { + params = params.substr(1); + } + params += sep; + } else { + params = ''; + } + params += 'is_js_confirmed=1' + sep + 'ajax_request=true' + sep + 'fk_checks=' + ($elem.find('#fk_checks').is(':checked') ? 1 : 0); + return params; +} diff --git a/js/src/functions/Grid/Cell.js b/js/src/functions/Grid/Cell.js new file mode 100644 index 0000000000..c8acc0d31f --- /dev/null +++ b/js/src/functions/Grid/Cell.js @@ -0,0 +1,16 @@ +/** + * Return value of a cell in a table. + */ +export function PMA_getCellValue (td) { + var $td = $(td); + if ($td.is('.null')) { + return ''; + } else if ((! $td.is('.to_be_saved') + || $td.is('.set')) + && $td.data('original_data') + ) { + return $td.data('original_data'); + } else { + return $td.text(); + } +} diff --git a/js/src/functions/Grid/GetFieldName.js b/js/src/functions/Grid/GetFieldName.js new file mode 100644 index 0000000000..3338728be0 --- /dev/null +++ b/js/src/functions/Grid/GetFieldName.js @@ -0,0 +1,41 @@ +/** + * Get the field name for the current field. Required to construct the query + * for grid editing + * + * @param $table_results enclosing results table + * @param $this_field jQuery object that points to the current field's tr + */ +export function getFieldName ($table_results, $this_field) { + var this_field_index = $this_field.index(); + // ltr or rtl direction does not impact how the DOM was generated + // check if the action column in the left exist + var left_action_exist = !$table_results.find('th:first').hasClass('draggable'); + // number of column span for checkbox and Actions + var left_action_skip = left_action_exist ? $table_results.find('th:first').attr('colspan') - 1 : 0; + + // If this column was sorted, the text of the a element contains something + // like 1 that is useful to indicate the order in case + // of a sort on multiple columns; however, we dont want this as part + // of the column name so we strip it ( .clone() to .end() ) + var field_name = $table_results + .find('thead') + .find('th:eq(' + (this_field_index - left_action_skip) + ') a') + .clone() // clone the element + .children() // select all the children + .remove() // remove all of them + .end() // go back to the selected element + .text(); // grab the text + // happens when just one row (headings contain no a) + if (field_name === '') { + var $heading = $table_results.find('thead').find('th:eq(' + (this_field_index - left_action_skip) + ')').children('span'); + // may contain column comment enclosed in a span - detach it temporarily to read the column name + var $tempColComment = $heading.children().detach(); + field_name = $heading.text(); + // re-attach the column comment + $heading.append($tempColComment); + } + + field_name = $.trim(field_name); + + return field_name; +} diff --git a/js/src/functions/Grid/StickyColumns.js b/js/src/functions/Grid/StickyColumns.js new file mode 100644 index 0000000000..e941940d6c --- /dev/null +++ b/js/src/functions/Grid/StickyColumns.js @@ -0,0 +1,77 @@ +var prevScrollX = 0; +/* + * Set position, left, top, width of sticky_columns div + */ +function setStickyColumnsPosition ($sticky_columns, $table_results, position, top, left, margin_left) { + $sticky_columns + .css('position', position) + .css('top', top) + .css('left', left ? left : 'auto') + .css('margin-left', margin_left ? margin_left : '0px') + .css('width', $table_results.width()); +} + +/* + * Initialize sticky columns + */ +export function initStickyColumns ($table_results) { + return $('
') + .insertBefore($table_results) + .css('position', 'fixed') + .css('z-index', '99') + .css('width', $table_results.width()) + .css('margin-left', $('#page_content').css('margin-left')) + .css('top', $('#floating_menubar').height()) + .css('display', 'none'); +} + +/* + * Arrange/Rearrange columns in sticky header + */ +export function rearrangeStickyColumns ($sticky_columns, $table_results) { + var $originalHeader = $table_results.find('thead'); + var $originalColumns = $originalHeader.find('tr:first').children(); + var $clonedHeader = $originalHeader.clone(); + // clone width per cell + $clonedHeader.find('tr:first').children().width(function (i,val) { + var width = $originalColumns.eq(i).width(); + var is_firefox = navigator.userAgent.indexOf('Firefox') > -1; + if (! is_firefox) { + width += 1; + } + return width; + }); + $sticky_columns.empty().append($clonedHeader); +} + +/* + * Adjust sticky columns on horizontal/vertical scroll for all tables + */ +export function handleAllStickyColumns () { + $('.sticky_columns').each(function () { + handleStickyColumns($(this), $(this).next('.table_results')); + }); +} + +/* + * Adjust sticky columns on horizontal/vertical scroll + */ +export function handleStickyColumns ($sticky_columns, $table_results) { + var currentScrollX = $(window).scrollLeft(); + var windowOffset = $(window).scrollTop(); + var tableStartOffset = $table_results.offset().top; + var tableEndOffset = tableStartOffset + $table_results.height(); + if (windowOffset >= tableStartOffset && windowOffset <= tableEndOffset) { + // for horizontal scrolling + if (prevScrollX !== currentScrollX) { + prevScrollX = currentScrollX; + setStickyColumnsPosition($sticky_columns, $table_results, 'absolute', $('#floating_menubar').height() + windowOffset - tableStartOffset); + // for vertical scrolling + } else { + setStickyColumnsPosition($sticky_columns, $table_results, 'fixed', $('#floating_menubar').height(), $('#pma_navigation').width() - currentScrollX, $('#page_content').css('margin-left')); + } + $sticky_columns.show(); + } else { + $sticky_columns.hide(); + } +} diff --git a/js/src/functions/Print.js b/js/src/functions/Print.js new file mode 100644 index 0000000000..2e85b6bdaa --- /dev/null +++ b/js/src/functions/Print.js @@ -0,0 +1,47 @@ +import { PMA_Messages as PMA_messages } from '../variables/export_variables'; + +/** + * Produce print preview + */ +export function printPreview () { + $('#printcss').attr('media','all'); + createPrintAndBackButtons(); +} + +/** + * Create print and back buttons in preview page + */ +function createPrintAndBackButtons () { + var back_button = $('',{ + type: 'button', + value: PMA_messages.back, + id: 'back_button_print_view' + }); + back_button.on('click', removePrintAndBackButton); + back_button.appendTo('#page_content'); + var print_button = $('',{ + type: 'button', + value: PMA_messages.print, + id: 'print_button_print_view' + }); + print_button.on('click', printPage); + print_button.appendTo('#page_content'); +} + +/** + * Remove print and back buttons and revert to normal view + */ +function removePrintAndBackButton () { + $('#printcss').attr('media','print'); + $('#back_button_print_view').remove(); + $('#print_button_print_view').remove(); +} + +/** + * Print page + */ +function printPage () { + if (typeof(window.print) !== 'undefined') { + window.print(); + } +} diff --git a/js/src/functions/Server/ServerStatusMonitor.js b/js/src/functions/Server/ServerStatusMonitor.js new file mode 100644 index 0000000000..623ad2b106 --- /dev/null +++ b/js/src/functions/Server/ServerStatusMonitor.js @@ -0,0 +1,133 @@ +import { PMA_Messages as PMA_messages } from '../../variables/export_variables'; +export function getOsDetail (server_os, presetCharts) { + /* Add OS specific system info charts to the preset chart list */ + switch (server_os) { + case 'WINNT': + $.extend(presetCharts, { + 'cpu': { + title: PMA_messages.strSystemCPUUsage, + series: [{ + label: PMA_messages.strAverageLoad + }], + nodes: [{ + dataPoints: [{ type: 'cpu', name: 'loadavg' }] + }], + maxYLabel: 100 + }, + + 'memory': { + title: PMA_messages.strSystemMemory, + series: [{ + label: PMA_messages.strTotalMemory, + fill: true + }, { + dataType: 'memory', + label: PMA_messages.strUsedMemory, + fill: true + }], + nodes: [{ dataPoints: [{ type: 'memory', name: 'MemTotal' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'MemUsed' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + }, + + 'swap': { + title: PMA_messages.strSystemSwap, + series: [{ + label: PMA_messages.strTotalSwap, + fill: true + }, { + label: PMA_messages.strUsedSwap, + fill: true + }], + nodes: [{ dataPoints: [{ type: 'memory', name: 'SwapTotal' }] }, + { dataPoints: [{ type: 'memory', name: 'SwapUsed' }] } + ], + maxYLabel: 0 + } + }); + break; + + case 'Linux': + $.extend(presetCharts, { + 'cpu': { + title: PMA_messages.strSystemCPUUsage, + series: [{ + label: PMA_messages.strAverageLoad + }], + nodes: [{ dataPoints: [{ type: 'cpu', name: 'irrelevant' }], transformFn: 'cpu-linux' }], + maxYLabel: 0 + }, + 'memory': { + title: PMA_messages.strSystemMemory, + series: [ + { label: PMA_messages.strBufferedMemory, fill: true }, + { label: PMA_messages.strUsedMemory, fill: true }, + { label: PMA_messages.strCachedMemory, fill: true }, + { label: PMA_messages.strFreeMemory, fill: true } + ], + nodes: [ + { dataPoints: [{ type: 'memory', name: 'Buffers' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'MemUsed' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'Cached' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'MemFree' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + }, + 'swap': { + title: PMA_messages.strSystemSwap, + series: [ + { label: PMA_messages.strCachedSwap, fill: true }, + { label: PMA_messages.strUsedSwap, fill: true }, + { label: PMA_messages.strFreeSwap, fill: true } + ], + nodes: [ + { dataPoints: [{ type: 'memory', name: 'SwapCached' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'SwapUsed' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'SwapFree' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + } + }); + break; + + case 'SunOS': + $.extend(presetCharts, { + 'cpu': { + title: PMA_messages.strSystemCPUUsage, + series: [{ + label: PMA_messages.strAverageLoad + }], + nodes: [{ + dataPoints: [{ type: 'cpu', name: 'loadavg' }] + }], + maxYLabel: 0 + }, + 'memory': { + title: PMA_messages.strSystemMemory, + series: [ + { label: PMA_messages.strUsedMemory, fill: true }, + { label: PMA_messages.strFreeMemory, fill: true } + ], + nodes: [ + { dataPoints: [{ type: 'memory', name: 'MemUsed' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'MemFree' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + }, + 'swap': { + title: PMA_messages.strSystemSwap, + series: [ + { label: PMA_messages.strUsedSwap, fill: true }, + { label: PMA_messages.strFreeSwap, fill: true } + ], + nodes: [ + { dataPoints: [{ type: 'memory', name: 'SwapUsed' }], valueDivisor: 1024 }, + { dataPoints: [{ type: 'memory', name: 'SwapFree' }], valueDivisor: 1024 } + ], + maxYLabel: 0 + } + }); + break; + } +} diff --git a/js/src/functions/Server/SeverStatusSorter.js b/js/src/functions/Server/SeverStatusSorter.js new file mode 100644 index 0000000000..9cf7695961 --- /dev/null +++ b/js/src/functions/Server/SeverStatusSorter.js @@ -0,0 +1,42 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ + +/** + * Module import + */ +import { $ } from '../../utils/JqueryExtended'; +import '../../plugins/jquery/jquery.tablesorter'; +// TODO: tablesorter shouldn't sort already sorted columns +/** + * @access public + * + * @param {string} tabid Table id for chart drawing + * + * @return {void} + */ +function initTableSorter (tabid) { + var $table; + var opts; + switch (tabid) { + case 'statustabs_queries': + $table = $('#serverstatusqueriesdetails'); + opts = { + sortList: [[3, 1]], + headers: { + 1: { sorter: 'fancyNumber' }, + 2: { sorter: 'fancyNumber' } + } + }; + break; + } + $table.tablesorter(opts); + $table.find('tr:first th') + .append('
') + .addClass('header'); +} + +/** + * Module export + */ +export { + initTableSorter +}; diff --git a/js/src/functions/Sql/ForeignKey.js b/js/src/functions/Sql/ForeignKey.js new file mode 100644 index 0000000000..946155511f --- /dev/null +++ b/js/src/functions/Sql/ForeignKey.js @@ -0,0 +1,49 @@ +import { PMA_getImage } from '../get_image'; +import PMA_commonParams from '../../variables/common_params'; +import { PMA_Messages as PMA_messages } from '../../variables/export_variables'; +/** + * Get checkbox for foreign key checks + * + * @return string + */ +export function getForeignKeyCheckboxLoader () { + var html = ''; + html += '
'; + html += '
'; + html += PMA_getImage('ajax_clock_small'); + html += '
'; + html += '
'; + return html; +} + +export function loadForeignKeyCheckbox () { + // Load default foreign key check value + var params = { + 'ajax_request': true, + 'server': PMA_commonParams.get('server'), + 'get_default_fk_check_value': true + }; + $.get('sql.php', params, function (data) { + var html = '' + + '' + + ''; + $('.load-default-fk-check-value').replaceWith(html); + }); +} + +function getJSConfirmCommonParam (elem, params) { + var $elem = $(elem); + var sep = PMA_commonParams.get('arg_separator'); + if (params) { + // Strip possible leading ? + if (params.substring(0,1) === '?') { + params = params.substr(1); + } + params += sep; + } else { + params = ''; + } + params += 'is_js_confirmed=1' + sep + 'ajax_request=true' + sep + 'fk_checks=' + ($elem.find('#fk_checks').is(':checked') ? 1 : 0); + return params; +} diff --git a/js/src/functions/Sql/SqlEditor.js b/js/src/functions/Sql/SqlEditor.js new file mode 100644 index 0000000000..519143d5c7 --- /dev/null +++ b/js/src/functions/Sql/SqlEditor.js @@ -0,0 +1,117 @@ +import { sqlQueryOptions } from '../../utils/sql'; +import CommonParams from '../../variables/common_params'; +import { AJAX } from '../../ajax'; +import { codemirrorAutocompleteOnInputRead } from '../../utils/sql'; + +import CodeMirror from 'codemirror'; +import 'codemirror/mode/sql/sql.js'; +import 'codemirror/addon/runmode/runmode.js'; +import 'codemirror/addon/hint/show-hint.js'; +import 'codemirror/addon/hint/sql-hint.js'; +import 'codemirror/addon/lint/lint.js'; +import '../../plugins/codemirror/sql-lint'; + +function catchKeypressesFromSqlInlineEdit (event) { + // ctrl-enter is 10 in chrome and ie, but 13 in ff + if ((event.ctrlKey || event.metaKey) && (event.keyCode === 13 || event.keyCode === 10)) { + $('#sql_query_edit_save').trigger('click'); + } +} + +/** + * Creates an SQL editor which supports auto completing etc. + * + * @param $textarea jQuery object wrapping the textarea to be made the editor + * @param options optional options for CodeMirror + * @param resize optional resizing ('vertical', 'horizontal', 'both') + * @param lintOptions additional options for lint + */ + +export function PMA_getSQLEditor ($textarea, options, resize, lintOptions) { + if ($textarea.length > 0 && CommonParams.get('CodemirrorEnable') === true) { + // merge options for CodeMirror + var defaults = { + lineNumbers: true, + matchBrackets: true, + extraKeys: { 'Ctrl-Space': 'autocomplete' }, + hintOptions: { 'completeSingle': false, 'completeOnSingleClick': true }, + indentUnit: 4, + mode: 'text/x-mysql', + lineWrapping: true + }; + + if (CommonParams.get('LintEnable')) { + $.extend(defaults, { + gutters: ['CodeMirror-lint-markers'], + lint: { + 'getAnnotations': CodeMirror.sqlLint, + 'async': true, + 'lintOptions': lintOptions + } + }); + } + + $.extend(true, defaults, options); + + // create CodeMirror editor + var codemirrorEditor = CodeMirror.fromTextArea($textarea[0], defaults); + // allow resizing + if (! resize) { + resize = 'vertical'; + } + var handles = ''; + if (resize === 'vertical') { + handles = 's'; + } + if (resize === 'both') { + handles = 'all'; + } + if (resize === 'horizontal') { + handles = 'e, w'; + } + $(codemirrorEditor.getWrapperElement()) + .css('resize', resize) + .resizable({ + handles: handles, + resize: function () { + codemirrorEditor.setSize($(this).width(), $(this).height()); + } + }); + // enable autocomplete + codemirrorEditor.on('inputRead', codemirrorAutocompleteOnInputRead); + + // page locking + codemirrorEditor.on('change', function (e) { + e.data = { + value: 3, + content: codemirrorEditor.isClean(), + }; + AJAX.lockPageHandler(e); + }); + + return codemirrorEditor; + } + return null; +} + +/** + * Binds the CodeMirror to the text area used to inline edit a query. + */ +export function bindCodeMirrorToInlineEditor () { + var $inline_editor = $('#sql_query_edit'); + if ($inline_editor.length > 0) { + if (CommonParams.get('CodemirrorEnable') === true) { + var height = $inline_editor.css('height'); + sqlQueryOptions.codemirror_inline_editor = PMA_getSQLEditor($inline_editor); + sqlQueryOptions.codemirror_inline_editor.getWrapperElement().style.height = height; + sqlQueryOptions.codemirror_inline_editor.refresh(); + sqlQueryOptions.codemirror_inline_editor.focus(); + $(sqlQueryOptions.codemirror_inline_editor.getWrapperElement()) + .on('keydown', catchKeypressesFromSqlInlineEdit); + } else { + $inline_editor + .focus() + .on('keydown', catchKeypressesFromSqlInlineEdit); + } + } +} diff --git a/js/src/functions/Sql/SqlProfiling.js b/js/src/functions/Sql/SqlProfiling.js new file mode 100644 index 0000000000..3fe91495ab --- /dev/null +++ b/js/src/functions/Sql/SqlProfiling.js @@ -0,0 +1,57 @@ +import { createProfilingChart } from '../chart'; + +/* + * Profiling Chart + */ +export function makeProfilingChart () { + if ($('#profilingchart').length === 0 || + $('#profilingchart').html().length !== 0 || + !$.jqplot || !$.jqplot.Highlighter || !$.jqplot.PieRenderer + ) { + return; + } + + var data = []; + $.each(JSON.parse($('#profilingChartData').html()), function (key, value) { + data.push([key, parseFloat(value)]); + }); + + // Remove chart and data divs contents + $('#profilingchart').html('').show(); + $('#profilingChartData').html(''); + + createProfilingChart('profilingchart', data); +} + +/* + * initialize profiling data tables + */ +export function initProfilingTables () { + if (!$.tablesorter) { + return; + } + + $('#profiletable').tablesorter({ + widgets: ['zebra'], + sortList: [[0, 0]], + textExtraction: function (node) { + if (node.children.length > 0) { + return node.children[0].innerHTML; + } else { + return node.innerHTML; + } + } + }); + + $('#profilesummarytable').tablesorter({ + widgets: ['zebra'], + sortList: [[1, 1]], + textExtraction: function (node) { + if (node.children.length > 0) { + return node.children[0].innerHTML; + } else { + return node.innerHTML; + } + } + }); +} diff --git a/js/src/functions/Sql/SqlQuery.js b/js/src/functions/Sql/SqlQuery.js new file mode 100644 index 0000000000..219237129f --- /dev/null +++ b/js/src/functions/Sql/SqlQuery.js @@ -0,0 +1,371 @@ +import { isStorageSupported } from '../config'; +import Cookies from 'js-cookie'; +import { sqlQueryOptions } from '../../utils/sql'; +import { GlobalVariables, PMA_Messages as PMA_messages } from '../../variables/export_variables'; +import { PMA_ajaxShowMessage } from '../../utils/show_ajax_messages'; +import { PMA_sprintf } from '../../utils/sprintf'; + +/** + * Handles 'Simulate query' button on SQL query box. + * + * @return void + */ +export function PMA_handleSimulateQueryButton () { + var update_re = new RegExp('^\\s*UPDATE\\s+((`[^`]+`)|([A-Za-z0-9_$]+))\\s+SET\\s', 'i'); + var delete_re = new RegExp('^\\s*DELETE\\s+FROM\\s', 'i'); + var query = ''; + + if (sqlQueryOptions.codemirror_editor) { + query = sqlQueryOptions.codemirror_editor.getValue(); + } else { + query = $('#sqlquery').val(); + } + + var $simulateDml = $('#simulate_dml'); + if (update_re.test(query) || delete_re.test(query)) { + if (! $simulateDml.length) { + $('#button_submit_query') + .before(''); + } + } else { + if ($simulateDml.length) { + $simulateDml.remove(); + } + } +} + +/** + * Sets current value for query box. + */ +export function setQuery (query) { + if (sqlQueryOptions.codemirror_editor) { + sqlQueryOptions.codemirror_editor.setValue(query); + sqlQueryOptions.codemirror_editor.focus(); + } else if (document.sqlform) { + document.sqlform.sql_query.value = query; + document.sqlform.sql_query.focus(); + } +} + +/** + * Create quick sql statements. + * + */ +export function insertQuery (queryType) { + if (queryType === 'clear') { + setQuery(''); + return; + } else if (queryType === 'format') { + if (sqlQueryOptions.codemirror_editor) { + $('#querymessage').html(PMA_messages.strFormatting + + ' '); + var href = 'db_sql_format.php'; + var params = { + 'ajax_request': true, + 'sql': sqlQueryOptions.codemirror_editor.getValue() + }; + $.ajax({ + type: 'POST', + url: href, + data: params, + success: function (data) { + if (data.success) { + sqlQueryOptions.codemirror_editor.setValue(data.sql); + } + $('#querymessage').html(''); + } + }); + } + return; + } else if (queryType === 'saved') { + if (isStorageSupported('localStorage') + && typeof window.localStorage.auto_saved_sql !== 'undefined' + ) { + setQuery(window.localStorage.auto_saved_sql); + } else if (Cookies.get('auto_saved_sql')) { + setQuery(Cookies.get('auto_saved_sql')); + } else { + PMA_ajaxShowMessage(PMA_messages.strNoAutoSavedQuery); + } + return; + } + + var query = ''; + var myListBox = document.sqlform.dummy; + var table = document.sqlform.table.value; + + if (myListBox.options.length > 0) { + sql_box_locked = true; + var columnsList = ''; + var valDis = ''; + var editDis = ''; + var NbSelect = 0; + for (var i = 0; i < myListBox.options.length; i++) { + NbSelect++; + if (NbSelect > 1) { + columnsList += ', '; + valDis += ','; + editDis += ','; + } + columnsList += myListBox.options[i].value; + valDis += '[value-' + NbSelect + ']'; + editDis += myListBox.options[i].value + '=[value-' + NbSelect + ']'; + } + if (queryType === 'selectall') { + query = 'SELECT * FROM `' + table + '` WHERE 1'; + } else if (queryType === 'select') { + query = 'SELECT ' + columnsList + ' FROM `' + table + '` WHERE 1'; + } else if (queryType === 'insert') { + query = 'INSERT INTO `' + table + '`(' + columnsList + ') VALUES (' + valDis + ')'; + } else if (queryType === 'update') { + query = 'UPDATE `' + table + '` SET ' + editDis + ' WHERE 1'; + } else if (queryType === 'delete') { + query = 'DELETE FROM `' + table + '` WHERE 0'; + } + setQuery(query); + sql_box_locked = false; + } +} + +/** + * Confirms a "DROP/DELETE/ALTER" query before + * submitting it if required. + * This function is called by the 'checkSqlQuery()' js function. + * + * @param theForm1 object the form + * @param sqlQuery1 string the sql query string + * + * @return boolean whether to run the query or not + * + * @see checkSqlQuery() + */ +function confirmQuery (theForm1, sqlQuery1) { + // Confirmation is not required in the configuration file + if (PMA_messages.strDoYouReally === '') { + return true; + } + + // Confirms a "DROP/DELETE/ALTER/TRUNCATE" statement + // + // TODO: find a way (if possible) to use the parser-analyser + // for this kind of verification + // For now, I just added a ^ to check for the statement at + // beginning of expression + + var do_confirm_re_0 = new RegExp('^\\s*DROP\\s+(IF EXISTS\\s+)?(TABLE|PROCEDURE)\\s', 'i'); + var do_confirm_re_1 = new RegExp('^\\s*ALTER\\s+TABLE\\s+((`[^`]+`)|([A-Za-z0-9_$]+))\\s+DROP\\s', 'i'); + var do_confirm_re_2 = new RegExp('^\\s*DELETE\\s+FROM\\s', 'i'); + var do_confirm_re_3 = new RegExp('^\\s*TRUNCATE\\s', 'i'); + + if (do_confirm_re_0.test(sqlQuery1) || + do_confirm_re_1.test(sqlQuery1) || + do_confirm_re_2.test(sqlQuery1) || + do_confirm_re_3.test(sqlQuery1)) { + var message; + if (sqlQuery1.length > 100) { + message = sqlQuery1.substr(0, 100) + '\n ...'; + } else { + message = sqlQuery1; + } + var is_confirmed = confirm(PMA_sprintf(PMA_messages.strDoYouReally, message)); + // statement is confirmed -> update the + // "is_js_confirmed" form field so the confirm test won't be + // run on the server side and allows to submit the form + if (is_confirmed) { + theForm1.elements.is_js_confirmed.value = 1; + return true; + } else { + // statement is rejected -> do not submit the form + window.focus(); + return false; + } // end if (handle confirm box result) + } // end if (display confirm box) + + return true; +} // end of the 'confirmQuery()' function + +/** + * Displays an error message if the user submitted the sql query form with no + * sql query, else checks for "DROP/DELETE/ALTER" statements + * + * @param theForm object the form + * + * @return boolean always false + * + * @see confirmQuery() + */ +export function checkSqlQuery (theForm) { + // get the textarea element containing the query + var sqlQuery; + if (sqlQueryOptions.codemirror_editor) { + sqlQueryOptions.codemirror_editor.save(); + sqlQuery = sqlQueryOptions.codemirror_editor.getValue(); + } else { + sqlQuery = theForm.elements.sql_query.value; + } + var space_re = new RegExp('\\s+'); + if (typeof(theForm.elements.sql_file) !== 'undefined' && + theForm.elements.sql_file.value.replace(space_re, '') !== '') { + return true; + } + if (typeof(theForm.elements.id_bookmark) !== 'undefined' && + (theForm.elements.id_bookmark.value !== null || theForm.elements.id_bookmark.value !== '') && + theForm.elements.id_bookmark.selectedIndex !== 0) { + return true; + } + var result = false; + // Checks for "DROP/DELETE/ALTER" statements + if (sqlQuery.replace(space_re, '') !== '') { + result = confirmQuery(theForm, sqlQuery); + } else { + alert(PMA_messages.strFormEmpty); + } + + if (sqlQueryOptions.codemirror_editor) { + sqlQueryOptions.codemirror_editor.focus(); + } else if (sqlQueryOptions.codemirror_inline_editor) { + sqlQueryOptions.codemirror_inline_editor.focus(); + } + return result; +} // end of the 'checkSqlQuery()' function + +export function checkSavedQuery () { + if (isStorageSupported('localStorage') + && window.localStorage.auto_saved_sql !== undefined + ) { + PMA_ajaxShowMessage(PMA_messages.strPreviousSaveQuery); + } +} + +/** + * Set query to codemirror if show this query is + * checked and query for the db and table pair exists + */ +export function setShowThisQuery () { + var db = $('input[name="db"]').val(); + var table = $('input[name="table"]').val(); + if (isStorageSupported('localStorage')) { + if (window.localStorage.show_this_query_object !== undefined) { + var stored_db = JSON.parse(window.localStorage.show_this_query_object).db; + var stored_table = JSON.parse(window.localStorage.show_this_query_object).table; + var stored_query = JSON.parse(window.localStorage.show_this_query_object).query; + } + if (window.localStorage.show_this_query !== undefined + && window.localStorage.show_this_query === '1') { + $('input[name="show_query"]').prop('checked', true); + if ((db === stored_db && table === stored_table) || (db === undefined && table === undefined)) { + if (sqlQueryOptions.codemirror_editor) { + sqlQueryOptions.codemirror_editor.setValue(stored_query); + } else if (document.sqlform) { + document.sqlform.sql_query.value = stored_query; + } + } + } else { + $('input[name="show_query"]').prop('checked', false); + } + } +} + + +/** + * Saves SQL query in local storage or cookie + * + * @param string database name + * @param string table name + * @param string SQL query + * @return void + */ +export function PMA_showThisQuery (db, table, query) { + var show_this_query_object = { + 'db': db, + 'table': table, + 'query': query + }; + if (isStorageSupported('localStorage')) { + window.localStorage.show_this_query = 1; + window.localStorage.show_this_query_object = JSON.stringify(show_this_query_object); + } else { + Cookies.set('show_this_quey', 1); + Cookies.set('show_this_query_object', JSON.stringify(show_this_query_object)); + } +} + +/** + * Saves SQL query in local storage or cookie + * + * @param string SQL query + * @return void + */ +export function PMA_autosaveSQL (query) { + if (isStorageSupported('localStorage')) { + window.localStorage.auto_saved_sql = query; + } else { + Cookies.set('auto_saved_sql', query); + } +} + +/** + * Saves SQL query with sort in local storage or cookie + * + * @param string SQL query + * @return void + */ +export function PMA_autosaveSQLSort (query) { + if (query) { + if (isStorageSupported('localStorage')) { + window.localStorage.auto_saved_sql_sort = query; + } else { + Cookies.set('auto_saved_sql_sort', query); + } + } +} + +/** + * Inserts multiple fields. + * + */ +export function insertValueQuery () { + var myQuery = document.sqlform.sql_query; + var myListBox = document.sqlform.dummy; + + if (myListBox.options.length > 0) { + sql_box_locked = true; + var columnsList = ''; + var NbSelect = 0; + for (var i = 0; i < myListBox.options.length; i++) { + if (myListBox.options[i].selected) { + NbSelect++; + if (NbSelect > 1) { + columnsList += ', '; + } + columnsList += myListBox.options[i].value; + } + } + + /* CodeMirror support */ + if (sqlQueryOptions.codemirror_editor) { + sqlQueryOptions.codemirror_editor.replaceSelection(columnsList); + sqlQueryOptions.codemirror_editor.focus(); + // IE support + } else if (document.selection) { + myQuery.focus(); + var sel = document.selection.createRange(); + sel.text = columnsList; + // MOZILLA/NETSCAPE support + } else if (document.sqlform.sql_query.selectionStart || document.sqlform.sql_query.selectionStart === '0') { + var startPos = document.sqlform.sql_query.selectionStart; + var endPos = document.sqlform.sql_query.selectionEnd; + var SqlString = document.sqlform.sql_query.value; + + myQuery.value = SqlString.substring(0, startPos) + columnsList + SqlString.substring(endPos, SqlString.length); + myQuery.focus(); + } else { + myQuery.value += columnsList; + } + sql_box_locked = false; + } +} diff --git a/js/src/functions/UpdateCode.js b/js/src/functions/UpdateCode.js new file mode 100644 index 0000000000..7513a1ffa6 --- /dev/null +++ b/js/src/functions/UpdateCode.js @@ -0,0 +1,50 @@ +/** + * Updates an element containing code. + * + * @param jQuery Object $base base element which contains the raw and the + * highlighted code. + * + * @param string htmlValue code in HTML format, displayed if code cannot be + * highlighted + * + * @param string rawValue raw code, used as a parameter for highlighter + * + * @return bool whether content was updated or not + */ +export function PMA_updateCode ($base, htmlValue, rawValue) { + var $code = $base.find('code'); + if ($code.length === 0) { + return false; + } + + // Determines the type of the content and appropriate CodeMirror mode. + var type = ''; + var mode = ''; + if ($code.hasClass('json')) { + type = 'json'; + mode = 'application/json'; + } else if ($code.hasClass('sql')) { + type = 'sql'; + mode = 'text/x-mysql'; + } else if ($code.hasClass('xml')) { + type = 'xml'; + mode = 'application/xml'; + } else { + return false; + } + + // Element used to display unhighlighted code. + var $notHighlighted = $('
' + htmlValue + '
'); + + // Tries to highlight code using CodeMirror. + if (typeof CodeMirror !== 'undefined') { + var $highlighted = $('
'); + CodeMirror.runMode(rawValue, mode, $highlighted[0]); + $notHighlighted.hide(); + $code.html('').append($notHighlighted, $highlighted[0]); + } else { + $code.html('').append($notHighlighted); + } + + return true; +} diff --git a/js/src/functions/chart.js b/js/src/functions/chart.js index 8c2f00864d..20d6698d0c 100644 --- a/js/src/functions/chart.js +++ b/js/src/functions/chart.js @@ -2,11 +2,14 @@ * Creates a Profiling Chart. Used in sql.js * and in server_status_monitor.js */ +import { $ } from '../utils/JqueryExtended'; import JQPlotChartFactory from '../classes/Chart'; +import { ChartType, ColumnType, DataTable } from '../classes/Chart'; -export function PMA_createProfilingChart (target, data) { +export function createProfilingChart (target, data) { // create the chart var factory = new JQPlotChartFactory(); + console.log(factory); var chart = factory.createChart(ChartType.PIE, target); // create the data table and add columns @@ -62,5 +65,6 @@ export function PMA_createProfilingChart (target, data) { '#2e3436' ] }); + console.log(chart); return chart; } diff --git a/js/src/functions/config.js b/js/src/functions/config.js index c9769174e3..522189c6c7 100644 --- a/js/src/functions/config.js +++ b/js/src/functions/config.js @@ -26,7 +26,7 @@ export function isStorageSupported (type, warn) { } return false; } -/** ******************** Common Functions for Srttings page ****************** */ +/** ******************** Common Functions for Settings page ****************** */ /** * Checks whether field has its default value * @@ -126,6 +126,8 @@ export function setFieldValue (field, field_type, value) { * Returns field type * * @param {Element} field + * + * @return {string} */ export function getFieldType (field) { var $field = $(field); @@ -321,7 +323,8 @@ export function setupRestoreField () { var field_sel; if ($(this).hasClass('restore-default')) { field_sel = href; - restoreField(field_sel.substr(1), defaultValues); + console.log(field_sel); + restoreField(field_sel.substr(1)); } else { field_sel = href.match(/^[^=]+/)[0]; var value = href.match(/\=(.+)$/)[1]; diff --git a/js/src/index.js b/js/src/index.js index b6ddf95105..24c29d5a28 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -4,7 +4,7 @@ * Module import */ import './variables/import_variables'; -import { jQuery as $ } from './utils/JqueryExtended'; +import { $ } from './utils/JqueryExtended'; import { AJAX } from './ajax'; import './variables/get_config'; import files from './consts/files'; @@ -15,9 +15,6 @@ import { escapeHtml } from './utils/Sanitise'; import { PMA_ajaxShowMessage } from './utils/show_ajax_messages'; import PMA_commonParams from './variables/common_params'; -// console.log(PMA_messages); -// console.log(PMA_ajaxShowMessage); - /** * Page load event handler */ @@ -116,7 +113,6 @@ $(document).ajaxError(function (event, request, settings) { for (let i in files.global) { AJAX.scriptHandler.add(files.global[i]); } - /** * This block of code is for importing javascript files needed * for the first time loading of the page. @@ -133,10 +129,6 @@ if (typeof files[firstPage] !== 'undefined' && firstPage.toLocaleLowerCase() !== for (let i in files[indexPage]) { AJAX.scriptHandler.add(files[indexPage][i], 1); } -} else if (typeof files[indexPage] !== 'undefined' && firstPage.toLocaleLowerCase() === 'index') { - for (let i in files[indexPage]) { - AJAX.scriptHandler.add(files[indexPage][i]); - } } $(function () { diff --git a/js/src/multi_column_sort.js b/js/src/multi_column_sort.js new file mode 100644 index 0000000000..24b5796025 --- /dev/null +++ b/js/src/multi_column_sort.js @@ -0,0 +1,41 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +import PMA_commonParams from './variables/common_params'; +import { PMA_ajaxShowMessage } from './utils/show_ajax_messages'; +import { AJAX } from './ajax'; +import { removeColumnFromMultiSort } from './functions/ColumnSorting'; + +/** + * @fileoverview Implements the shiftkey + click remove column + * from order by clause funcationality + * @name columndelete + * + * @requires jQuery + */ + +export function onloadMultiColumnSort () { + $(document).on('click', 'th.draggable.column_heading.pointer.marker a', function (event) { + var url = $(this).parent().find('input').val(); + var argsep = PMA_commonParams.get('arg_separator'); + if (event.ctrlKey || event.altKey) { + event.preventDefault(); + let params = removeColumnFromMultiSort(url, $(this).parent()); + if (params) { + AJAX.source = $(this); + PMA_ajaxShowMessage(); + params += argsep + 'ajax_request=true' + argsep + 'ajax_page_request=true'; + $.post('sql.php', params, AJAX.responseHandler); + } + } else if (event.shiftKey) { + event.preventDefault(); + AJAX.source = $(this); + PMA_ajaxShowMessage(); + let params = url.substring(url.indexOf('?') + 1); + params += argsep + 'ajax_request=true' + argsep + 'ajax_page_request=true'; + $.post('sql.php', params, AJAX.responseHandler); + } + }); +} + +export function teardownMultiColumnSort () { + $(document).off('click', 'th.draggable.column_heading.pointer.marker a'); +} diff --git a/js/src/plugins/codemirror/sql-lint.js b/js/src/plugins/codemirror/sql-lint.js new file mode 100644 index 0000000000..e52c0e2ced --- /dev/null +++ b/js/src/plugins/codemirror/sql-lint.js @@ -0,0 +1,41 @@ +import CodeMirror from 'codemirror'; +import CommonParams from '../../variables/common_params'; + +CodeMirror.sqlLint = function (text, updateLinting, options, cm) { + // Skipping check if text box is empty. + if (text.trim() === '') { + updateLinting(cm, []); + return; + } + + function handleResponse (response) { + var found = []; + for (var idx in response) { + found.push({ + from: CodeMirror.Pos( + response[idx].fromLine, response[idx].fromColumn + ), + to: CodeMirror.Pos( + response[idx].toLine, response[idx].toColumn + ), + messageHTML: response[idx].message, + severity : response[idx].severity + }); + } + + updateLinting(cm, found); + } + + $.ajax({ + method: 'POST', + url: 'lint.php', + dataType: 'json', + data: { + sql_query: text, + server: CommonParams.get('server'), + options: options.lintOptions, + no_history: true, + }, + success: handleResponse + }); +}; diff --git a/js/src/plugins/jqplot/jqplot.byteFormatter.js b/js/src/plugins/jqplot/jqplot.byteFormatter.js new file mode 100644 index 0000000000..610692d3c7 --- /dev/null +++ b/js/src/plugins/jqplot/jqplot.byteFormatter.js @@ -0,0 +1,46 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * jqplot formatter for byte values + * + * @package phpMyAdmin + */ +(function ($) { + 'use strict'; + var formatByte = function (val, index) { + var units = [ + PMA_messages.strB, + PMA_messages.strKiB, + PMA_messages.strMiB, + PMA_messages.strGiB, + PMA_messages.strTiB, + PMA_messages.strPiB, + PMA_messages.strEiB + ]; + while (val >= 1024 && index <= 6) { + val /= 1024; + index++; + } + var format = '%.1f'; + if (Math.floor(val) === val) { + format = '%.0f'; + } + return $.jqplot.sprintf( + format + ' ' + units[index], val + ); + }; + /** + * The index indicates what unit the incoming data will be in. + * 0 for bytes, 1 for kilobytes and so on... + */ + $.jqplot.byteFormatter = function (index) { + index = index || 0; + return function (format, val) { + if (typeof val === 'number') { + val = parseFloat(val) || 0; + return formatByte(val, index); + } else { + return String(val); + } + }; + }; +}(jQuery)); diff --git a/js/src/plugins/jquery/jquery.ba-hashchange-1.3.js b/js/src/plugins/jquery/jquery.ba-hashchange-1.3.js new file mode 100644 index 0000000000..bba582bfe1 --- /dev/null +++ b/js/src/plugins/jquery/jquery.ba-hashchange-1.3.js @@ -0,0 +1,390 @@ +/*! + * jQuery hashchange event - v1.3 - 7/21/2010 + * http://benalman.com/projects/jquery-hashchange-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ + +// Script: jQuery hashchange event +// +// *Version: 1.3, Last updated: 7/21/2010* +// +// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/ +// GitHub - http://github.com/cowboy/jquery-hashchange/ +// Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js +// (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped) +// +// About: License +// +// Copyright (c) 2010 "Cowboy" Ben Alman, +// Dual licensed under the MIT and GPL licenses. +// http://benalman.com/about/license/ +// +// About: Examples +// +// These working examples, complete with fully commented code, illustrate a few +// ways in which this plugin can be used. +// +// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/ +// document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/ +// +// About: Support and Testing +// +// Information about what version or versions of jQuery this plugin has been +// tested with, what browsers it has been tested in, and where the unit tests +// reside (so you can test it yourself). +// +// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2 +// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5, +// Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5. +// Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/ +// +// About: Known issues +// +// While this jQuery hashchange event implementation is quite stable and +// robust, there are a few unfortunate browser bugs surrounding expected +// hashchange event-based behaviors, independent of any JavaScript +// window.onhashchange abstraction. See the following examples for more +// information: +// +// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/ +// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/ +// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/ +// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/ +// +// Also note that should a browser natively support the window.onhashchange +// event, but not report that it does, the fallback polling loop will be used. +// +// About: Release History +// +// 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more +// "removable" for mobile-only development. Added IE6/7 document.title +// support. Attempted to make Iframe as hidden as possible by using +// techniques from http://www.paciellogroup.com/blog/?p=604. Added +// support for the "shortcut" format $(window).hashchange( fn ) and +// $(window).hashchange() like jQuery provides for built-in events. +// Renamed jQuery.hashchangeDelay to and +// lowered its default value to 50. Added +// and properties plus document-domain.html +// file to address access denied issues when setting document.domain in +// IE6/7. +// 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin +// from a page on another domain would cause an error in Safari 4. Also, +// IE6/7 Iframe is now inserted after the body (this actually works), +// which prevents the page from scrolling when the event is first bound. +// Event can also now be bound before DOM ready, but it won't be usable +// before then in IE6/7. +// 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug +// where browser version is incorrectly reported as 8.0, despite +// inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag. +// 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special +// window.onhashchange functionality into a separate plugin for users +// who want just the basic event & back button support, without all the +// extra awesomeness that BBQ provides. This plugin will be included as +// part of jQuery BBQ, but also be available separately. + +(function($,window,undefined){ + '$:nomunge'; // Used by YUI compressor. + + // Reused string. + var str_hashchange = 'hashchange', + + // Method / object references. + doc = document, + fake_onhashchange, + special = $.event.special, + + // Does the browser support window.onhashchange? Note that IE8 running in + // IE7 compatibility mode reports true for 'onhashchange' in window, even + // though the event isn't supported, so also test document.documentMode. + doc_mode = doc.documentMode, + supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 ); + + // Get location.hash (or what you'd expect location.hash to be) sans any + // leading #. Thanks for making this necessary, Firefox! + function get_fragment( url ) { + url = url || location.href; + return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' ); + }; + + // Method: jQuery.fn.hashchange + // + // Bind a handler to the window.onhashchange event or trigger all bound + // window.onhashchange event handlers. This behavior is consistent with + // jQuery's built-in event handlers. + // + // Usage: + // + // > jQuery(window).hashchange( [ handler ] ); + // + // Arguments: + // + // handler - (Function) Optional handler to be bound to the hashchange + // event. This is a "shortcut" for the more verbose form: + // jQuery(window).bind( 'hashchange', handler ). If handler is omitted, + // all bound window.onhashchange event handlers will be triggered. This + // is a shortcut for the more verbose + // jQuery(window).trigger( 'hashchange' ). These forms are described in + // the section. + // + // Returns: + // + // (jQuery) The initial jQuery collection of elements. + + // Allow the "shortcut" format $(elem).hashchange( fn ) for binding and + // $(elem).hashchange() for triggering, like jQuery does for built-in events. + $.fn[ str_hashchange ] = function( fn ) { + return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange ); + }; + + // Property: jQuery.fn.hashchange.delay + // + // The numeric interval (in milliseconds) at which the + // polling loop executes. Defaults to 50. + + // Property: jQuery.fn.hashchange.domain + // + // If you're setting document.domain in your JavaScript, and you want hash + // history to work in IE6/7, not only must this property be set, but you must + // also set document.domain BEFORE jQuery is loaded into the page. This + // property is only applicable if you are supporting IE6/7 (or IE8 operating + // in "IE7 compatibility" mode). + // + // In addition, the property must be set to the + // path of the included "document-domain.html" file, which can be renamed or + // modified if necessary (note that the document.domain specified must be the + // same in both your main JavaScript as well as in this file). + // + // Usage: + // + // jQuery.fn.hashchange.domain = document.domain; + + // Property: jQuery.fn.hashchange.src + // + // If, for some reason, you need to specify an Iframe src file (for example, + // when setting document.domain as in ), you can + // do so using this property. Note that when using this property, history + // won't be recorded in IE6/7 until the Iframe src file loads. This property + // is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7 + // compatibility" mode). + // + // Usage: + // + // jQuery.fn.hashchange.src = 'path/to/file.html'; + + $.fn[ str_hashchange ].delay = 50; + /* + $.fn[ str_hashchange ].domain = null; + $.fn[ str_hashchange ].src = null; + */ + + // Event: hashchange event + // + // Fired when location.hash changes. In browsers that support it, the native + // HTML5 window.onhashchange event is used, otherwise a polling loop is + // initialized, running every milliseconds to + // see if the hash has changed. In IE6/7 (and IE8 operating in "IE7 + // compatibility" mode), a hidden Iframe is created to allow the back button + // and hash-based history to work. + // + // Usage as described in : + // + // > // Bind an event handler. + // > jQuery(window).hashchange( function(e) { + // > var hash = location.hash; + // > ... + // > }); + // > + // > // Manually trigger the event handler. + // > jQuery(window).hashchange(); + // + // A more verbose usage that allows for event namespacing: + // + // > // Bind an event handler. + // > jQuery(window).bind( 'hashchange', function(e) { + // > var hash = location.hash; + // > ... + // > }); + // > + // > // Manually trigger the event handler. + // > jQuery(window).trigger( 'hashchange' ); + // + // Additional Notes: + // + // * The polling loop and Iframe are not created until at least one handler + // is actually bound to the 'hashchange' event. + // * If you need the bound handler(s) to execute immediately, in cases where + // a location.hash exists on page load, via bookmark or page refresh for + // example, use jQuery(window).hashchange() or the more verbose + // jQuery(window).trigger( 'hashchange' ). + // * The event can be bound before DOM ready, but since it won't be usable + // before then in IE6/7 (due to the necessary Iframe), recommended usage is + // to bind it inside a DOM ready handler. + + // Override existing $.event.special.hashchange methods (allowing this plugin + // to be defined after jQuery BBQ in BBQ's source code). + special[ str_hashchange ] = $.extend( special[ str_hashchange ], { + + // Called only when the first 'hashchange' event is bound to window. + setup: function() { + // If window.onhashchange is supported natively, there's nothing to do.. + if ( supports_onhashchange ) { return false; } + + // Otherwise, we need to create our own. And we don't want to call this + // until the user binds to the event, just in case they never do, since it + // will create a polling loop and possibly even a hidden Iframe. + $( fake_onhashchange.start ); + }, + + // Called only when the last 'hashchange' event is unbound from window. + teardown: function() { + // If window.onhashchange is supported natively, there's nothing to do.. + if ( supports_onhashchange ) { return false; } + + // Otherwise, we need to stop ours (if possible). + $( fake_onhashchange.stop ); + } + + }); + + // fake_onhashchange does all the work of triggering the window.onhashchange + // event for browsers that don't natively support it, including creating a + // polling loop to watch for hash changes and in IE 6/7 creating a hidden + // Iframe to enable back and forward. + fake_onhashchange = (function(){ + var self = {}, + timeout_id, + + // Remember the initial hash so it doesn't get triggered immediately. + last_hash = get_fragment(), + + fn_retval = function(val){ return val; }, + history_set = fn_retval, + history_get = fn_retval; + + // Start the polling loop. + self.start = function() { + timeout_id || poll(); + }; + + // Stop the polling loop. + self.stop = function() { + timeout_id && clearTimeout( timeout_id ); + timeout_id = undefined; + }; + + // This polling loop checks every $.fn.hashchange.delay milliseconds to see + // if location.hash has changed, and triggers the 'hashchange' event on + // window when necessary. + function poll() { + var hash = get_fragment(), + history_hash = history_get( last_hash ); + + if ( hash !== last_hash ) { + history_set( last_hash = hash, history_hash ); + + $(window).trigger( str_hashchange ); + + } else if ( history_hash !== last_hash ) { + location.href = location.href.replace( /#.*/, '' ) + history_hash; + } + + timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay ); + }; + + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + (window.navigator.userAgent.indexOf("MSIE ") > -1 || !!window.navigator.userAgent.match(/Trident.*rv\:11\./)) && !supports_onhashchange && (function(){ + // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8 + // when running in "IE7 compatibility" mode. + + var iframe, + iframe_src; + + // When the event is bound and polling starts in IE 6/7, create a hidden + // Iframe for history handling. + self.start = function(){ + if ( !iframe ) { + iframe_src = $.fn[ str_hashchange ].src; + iframe_src = iframe_src && iframe_src + get_fragment(); + + // Create hidden Iframe. Attempt to make Iframe as hidden as possible + // by using techniques from http://www.paciellogroup.com/blog/?p=604. + iframe = $('