2 * function used in or for navigation panel
4 * @package phpMyAdmin-Navigation
7 /* global isStorageSupported, setupConfigTabs, setupRestoreField, setupValidation */ // js/config.js
8 /* global RTE */ // js/rte.js
13 * updates the tree state in sessionStorage
17 Navigation.treeStateUpdate = function () {
18 // update if session storage is supported
19 if (isStorageSupported('sessionStorage')) {
20 var storage = window.sessionStorage;
21 // try catch necessary here to detect whether
22 // content to be stored exceeds storage capacity
24 storage.setItem('navTreePaths', JSON.stringify(Navigation.traverseForPaths()));
25 storage.setItem('server', CommonParams.get('server'));
26 storage.setItem('token', CommonParams.get('token'));
28 // storage capacity exceeded & old navigation tree
29 // state is no more valid, so remove it
30 storage.removeItem('navTreePaths');
31 storage.removeItem('server');
32 storage.removeItem('token');
38 * updates the filter state in sessionStorage
42 Navigation.filterStateUpdate = function (filterName, filterValue) {
43 if (isStorageSupported('sessionStorage')) {
44 var storage = window.sessionStorage;
46 var currentFilter = $.extend({}, JSON.parse(storage.getItem('navTreeSearchFilters')));
48 filter[filterName] = filterValue;
49 currentFilter = $.extend(currentFilter, filter);
50 storage.setItem('navTreeSearchFilters', JSON.stringify(currentFilter));
52 storage.removeItem('navTreeSearchFilters');
58 * restores the filter state on navigation reload
62 Navigation.filterStateRestore = function () {
63 if (isStorageSupported('sessionStorage')
64 && typeof window.sessionStorage.navTreeSearchFilters !== 'undefined'
66 var searchClauses = JSON.parse(window.sessionStorage.navTreeSearchFilters);
67 if (Object.keys(searchClauses).length < 1) {
70 // restore database filter if present and not empty
71 if (searchClauses.hasOwnProperty('dbFilter')
72 && searchClauses.dbFilter.length
74 var $obj = $('#pma_navigation_tree');
75 if (! $obj.data('fastFilter')) {
78 new Navigation.FastFilter.Filter($obj, '')
81 $obj.find('li.fast_filter.db_fast_filter input.searchClause')
82 .val(searchClauses.dbFilter)
85 // find all table filters present in the tree
86 var $tableFilters = $('#pma_navigation_tree li.database')
87 .children('div.list_container')
88 .find('li.fast_filter input.searchClause');
89 // restore table filters
90 $tableFilters.each(function () {
91 $obj = $(this).closest('div.list_container');
92 // aPath associated with this filter
93 var filterName = $(this).siblings('input[name=aPath]').val();
94 // if this table's filter has a state stored in storage
95 if (searchClauses.hasOwnProperty(filterName)
96 && searchClauses[filterName].length
98 // clear state if item is not visible,
99 // happens when table filter becomes invisible
100 // as db filter has already been applied
101 if (! $obj.is(':visible')) {
102 Navigation.filterStateUpdate(filterName, '');
105 if (! $obj.data('fastFilter')) {
108 new Navigation.FastFilter.Filter($obj, '')
111 $(this).val(searchClauses[filterName])
119 * Loads child items of a node and executes a given callback
122 * @param $expandElem expander
123 * @param callback callback function
127 Navigation.loadChildNodes = function (isNode, $expandElem, callback) {
128 var $destination = null;
132 if (!$expandElem.hasClass('expander')) {
135 $destination = $expandElem.closest('li');
136 var pos2Name = $expandElem.find('span.pos2_nav');
137 var pathsNav = $expandElem.find('span.paths_nav');
139 'aPath': pathsNav.attr('data-apath'),
140 'vPath': pathsNav.attr('data-vpath'),
141 'pos': pathsNav.attr('data-pos'),
142 'pos2_name': pos2Name.attr('data-name'),
143 'pos2_value': pos2Name.attr('data-value'),
147 if ($expandElem.closest('ul').hasClass('search_results')) {
148 params.searchClause = Navigation.FastFilter.getSearchClause();
149 params.searchClause2 = Navigation.FastFilter.getSearchClause2($expandElem);
152 $destination = $('#pma_navigation_tree_content');
154 'aPath': $expandElem.attr('data-apath'),
155 'vPath': $expandElem.attr('data-vpath'),
156 'pos': $expandElem.attr('data-pos'),
164 var url = $('#pma_navigation').find('a.navigation_url').attr('href');
165 $.get(url, params, function (data) {
166 if (typeof data !== 'undefined' && data.success === true) {
167 $destination.find('div.list_container').remove(); // FIXME: Hack, there shouldn't be a list container there
169 $destination.append(data.message);
170 $expandElem.addClass('loaded');
172 $destination.html(data.message);
173 $destination.children()
183 var $errors = $(data.errors);
184 if ($errors.children().length > 0) {
185 $('#pma_errors').replaceWith(data.errors);
188 if (callback && typeof callback === 'function') {
191 } else if (data.redirect_flag === '1') {
192 if (window.location.href.indexOf('?') === -1) {
193 window.location.href += '?session_expired=1';
195 window.location.href += CommonParams.get('arg_separator') + 'session_expired=1';
197 window.location.reload();
199 var $throbber = $expandElem.find('img.throbber');
201 var $icon = $expandElem.find('img.ic_b_plus');
203 Functions.ajaxShowMessage(data.error, false);
209 * Collapses a node in navigation tree.
211 * @param $expandElem expander
215 Navigation.collapseTreeNode = function ($expandElem) {
216 var $children = $expandElem.closest('li').children('div.list_container');
217 var $icon = $expandElem.find('img');
218 if ($expandElem.hasClass('loaded')) {
219 if ($icon.is('.ic_b_minus')) {
220 $icon.removeClass('ic_b_minus').addClass('ic_b_plus');
221 $children.slideUp('fast');
224 $expandElem.trigger('blur');
225 $children.promise().done(Navigation.treeStateUpdate);
229 * Traverse the navigation tree backwards to generate all the actual
230 * and virtual paths, as well as the positions in the pagination at
231 * various levels, if necessary.
235 Navigation.traverseForPaths = function () {
237 pos: $('#pma_navigation_tree').find('div.dbselector select').val()
239 if ($('#navi_db_select').length) {
243 $('#pma_navigation_tree').find('a.expander:visible').each(function () {
244 if ($(this).find('img').is('.ic_b_minus') &&
245 $(this).closest('li').find('div.list_container .ic_b_minus').length === 0
247 var pathsNav = $(this).find('span.paths_nav');
248 params['n' + count + '_aPath'] = pathsNav.attr('data-apath');
249 params['n' + count + '_vPath'] = pathsNav.attr('data-vpath');
251 var pos2Nav = $(this).find('span.pos2_nav');
253 if (pos2Nav.length === 0) {
257 .find('span.pos2_nav').last();
260 params['n' + count + '_pos2_name'] = pos2Nav.attr('data-name');
261 params['n' + count + '_pos2_value'] = pos2Nav.attr('data-value');
263 var pos3Nav = $(this).find('span.pos3_nav');
265 params['n' + count + '_pos3_name'] = pos3Nav.attr('data-name');
266 params['n' + count + '_pos3_value'] = pos3Nav.attr('data-value');
274 * Executed on page load
277 if (! $('#pma_navigation').length) {
278 // Don't bother running any code if the navigation is not even on the page
282 // Do not let the page reload on submitting the fast filter
283 $(document).on('submit', '.fast_filter', function (event) {
284 event.preventDefault();
287 // Fire up the resize handlers
288 new Navigation.ResizeHandler();
291 * opens/closes (hides/shows) tree elements
292 * loads data via ajax
294 $(document).on('click', '#pma_navigation_tree a.expander', function (event) {
295 event.preventDefault();
296 event.stopImmediatePropagation();
297 var $icon = $(this).find('img');
298 if ($icon.is('.ic_b_plus')) {
299 Navigation.expandTreeNode($(this));
301 Navigation.collapseTreeNode($(this));
306 * Register event handler for click on the reload
307 * navigation icon at the top of the panel
309 $(document).on('click', '#pma_navigation_reload', function (event) {
310 event.preventDefault();
312 // Find the loading symbol and show it
313 var $iconThrobberSrc = $('#pma_navigation').find('.throbber');
314 $iconThrobberSrc.show();
315 // TODO Why is a loading symbol both hidden, and invisible?
316 $iconThrobberSrc.css('visibility', '');
318 // Callback to be used to hide the loading symbol when done reloading
319 function hideNav () {
320 $iconThrobberSrc.hide();
323 // Reload the navigation
324 Navigation.reload(hideNav);
327 $(document).on('change', '#navi_db_select', function () {
328 if (! $(this).val()) {
329 CommonParams.set('db', '');
332 $(this).closest('form').trigger('submit');
336 * Register event handler for click on the collapse all
337 * navigation icon at the top of the navigation tree
339 $(document).on('click', '#pma_navigation_collapse', function (event) {
340 event.preventDefault();
341 $('#pma_navigation_tree').find('a.expander').each(function () {
342 var $icon = $(this).find('img');
343 if ($icon.is('.ic_b_minus')) {
344 $(this).trigger('click');
350 * Register event handler to toggle
351 * the 'link with main panel' icon on mouseenter.
353 $(document).on('mouseenter', '#pma_navigation_sync', function (event) {
354 event.preventDefault();
355 var synced = $('#pma_navigation_tree').hasClass('synced');
356 var $img = $('#pma_navigation_sync').children('img');
358 $img.removeClass('ic_s_link').addClass('ic_s_unlink');
360 $img.removeClass('ic_s_unlink').addClass('ic_s_link');
365 * Register event handler to toggle
366 * the 'link with main panel' icon on mouseout.
368 $(document).on('mouseout', '#pma_navigation_sync', function (event) {
369 event.preventDefault();
370 var synced = $('#pma_navigation_tree').hasClass('synced');
371 var $img = $('#pma_navigation_sync').children('img');
373 $img.removeClass('ic_s_unlink').addClass('ic_s_link');
375 $img.removeClass('ic_s_link').addClass('ic_s_unlink');
380 * Register event handler to toggle
381 * the linking with main panel behavior
383 $(document).on('click', '#pma_navigation_sync', function (event) {
384 event.preventDefault();
385 var synced = $('#pma_navigation_tree').hasClass('synced');
386 var $img = $('#pma_navigation_sync').children('img');
389 .removeClass('ic_s_unlink')
390 .addClass('ic_s_link')
391 .attr('alt', Messages.linkWithMain)
392 .attr('title', Messages.linkWithMain);
393 $('#pma_navigation_tree')
394 .removeClass('synced')
396 .removeClass('selected');
399 .removeClass('ic_s_link')
400 .addClass('ic_s_unlink')
401 .attr('alt', Messages.unlinkWithMain)
402 .attr('title', Messages.unlinkWithMain);
403 $('#pma_navigation_tree').addClass('synced');
404 Navigation.showCurrent();
409 * Bind all "fast filter" events
411 $(document).on('click', '#pma_navigation_tree li.fast_filter span', Navigation.FastFilter.events.clear);
412 $(document).on('focus', '#pma_navigation_tree li.fast_filter input.searchClause', Navigation.FastFilter.events.focus);
413 $(document).on('blur', '#pma_navigation_tree li.fast_filter input.searchClause', Navigation.FastFilter.events.blur);
414 $(document).on('keyup', '#pma_navigation_tree li.fast_filter input.searchClause', Navigation.FastFilter.events.keyup);
417 * Ajax handler for pagination
419 $(document).on('click', '#pma_navigation_tree div.pageselector a.ajax', function (event) {
420 event.preventDefault();
421 Navigation.treePagination($(this));
429 '#pma_navigation_tree.highlight li:not(.fast_filter)',
431 if ($('li:visible', this).length === 0) {
432 $(this).addClass('activePointer');
438 '#pma_navigation_tree.highlight li:not(.fast_filter)',
440 $(this).removeClass('activePointer');
444 /** Create a Routine, Trigger or Event */
445 $(document).on('click', 'li.new_procedure a.ajax, li.new_function a.ajax', function (event) {
446 event.preventDefault();
447 var dialog = new RTE.Object('routine');
448 dialog.editorDialog(1, $(this));
450 $(document).on('click', 'li.new_trigger a.ajax', function (event) {
451 event.preventDefault();
452 var dialog = new RTE.Object('trigger');
453 dialog.editorDialog(1, $(this));
455 $(document).on('click', 'li.new_event a.ajax', function (event) {
456 event.preventDefault();
457 var dialog = new RTE.Object('event');
458 dialog.editorDialog(1, $(this));
461 /** Edit Routines, Triggers or Events */
462 $(document).on('click', 'li.procedure > a.ajax, li.function > a.ajax', function (event) {
463 event.preventDefault();
464 var dialog = new RTE.Object('routine');
465 dialog.editorDialog(0, $(this));
467 $(document).on('click', 'li.trigger > a.ajax', function (event) {
468 event.preventDefault();
469 var dialog = new RTE.Object('trigger');
470 dialog.editorDialog(0, $(this));
472 $(document).on('click', 'li.event > a.ajax', function (event) {
473 event.preventDefault();
474 var dialog = new RTE.Object('event');
475 dialog.editorDialog(0, $(this));
478 /** Execute Routines */
479 $(document).on('click', 'li.procedure div a.ajax img,' +
480 ' li.function div a.ajax img', function (event) {
481 event.preventDefault();
482 var dialog = new RTE.Object('routine');
483 dialog.executeDialog($(this).parent());
485 /** Export Triggers and Events */
486 $(document).on('click', 'li.trigger div.second a.ajax img,' +
487 ' li.event div.second a.ajax img', function (event) {
488 event.preventDefault();
489 var dialog = new RTE.Object();
490 dialog.exportDialog($(this).parent());
494 $(document).on('click', '#pma_navigation_tree li.new_index a.ajax', function (event) {
495 event.preventDefault();
496 var url = $(this).attr('href').substr(
497 $(this).attr('href').indexOf('?') + 1
498 ) + CommonParams.get('arg_separator') + 'ajax_request=true';
499 var title = Messages.strAddIndex;
500 Functions.indexEditorDialog(url, title);
504 $(document).on('click', 'li.index a.ajax', function (event) {
505 event.preventDefault();
506 var url = $(this).attr('href').substr(
507 $(this).attr('href').indexOf('?') + 1
508 ) + CommonParams.get('arg_separator') + 'ajax_request=true';
509 var title = Messages.strEditIndex;
510 Functions.indexEditorDialog(url, title);
514 $(document).on('click', 'li.new_view a.ajax', function (event) {
515 event.preventDefault();
516 Functions.createViewDialog($(this));
519 /** Hide navigation tree item */
520 $(document).on('click', 'a.hideNavItem.ajax', function (event) {
521 event.preventDefault();
522 var argSep = CommonParams.get('arg_separator');
523 var params = $(this).getPostData();
524 params += argSep + 'ajax_request=true' + argSep + 'server=' + CommonParams.get('server');
528 url: $(this).attr('href'),
529 success: function (data) {
530 if (typeof data !== 'undefined' && data.success === true) {
533 Functions.ajaxShowMessage(data.error);
539 /** Display a dialog to choose hidden navigation items to show */
540 $(document).on('click', 'a.showUnhide.ajax', function (event) {
541 event.preventDefault();
542 var $msg = Functions.ajaxShowMessage();
543 var argSep = CommonParams.get('arg_separator');
544 var params = $(this).getPostData();
545 params += argSep + 'ajax_request=true';
546 $.post($(this).attr('href'), params, function (data) {
547 if (typeof data !== 'undefined' && data.success === true) {
548 Functions.ajaxRemoveMessage($msg);
549 var buttonOptions = {};
550 buttonOptions[Messages.strClose] = function () {
551 $(this).dialog('close');
554 .attr('id', 'unhideNavItemDialog')
555 .append(data.message)
560 buttons: buttonOptions,
561 title: Messages.strUnhideNavItem,
567 Functions.ajaxShowMessage(data.error);
572 /** Show a hidden navigation tree item */
573 $(document).on('click', 'a.unhideNavItem.ajax', function (event) {
574 event.preventDefault();
575 var $tr = $(this).parents('tr');
576 var $hiddenTableCount = $tr.parents('tbody').children().length;
577 var $hideDialogBox = $tr.closest('div.ui-dialog');
578 var $msg = Functions.ajaxShowMessage();
579 var argSep = CommonParams.get('arg_separator');
580 var params = $(this).getPostData();
581 params += argSep + 'ajax_request=true' + argSep + 'server=' + CommonParams.get('server');
585 url: $(this).attr('href'),
586 success: function (data) {
587 Functions.ajaxRemoveMessage($msg);
588 if (typeof data !== 'undefined' && data.success === true) {
590 if ($hiddenTableCount === 1) {
591 $hideDialogBox.remove();
595 Functions.ajaxShowMessage(data.error);
601 // Add/Remove favorite table using Ajax.
602 $(document).on('click', '.favorite_table_anchor', function (event) {
603 event.preventDefault();
605 var anchorId = $self.attr('id');
606 if ($self.data('favtargetn') !== null) {
607 var $dataFavTargets = $('a[data-favtargets="' + $self.data('favtargetn') + '"]');
608 if ($dataFavTargets.length > 0) {
609 $dataFavTargets.trigger('click');
614 var hasLocalStorage = isStorageSupported('localStorage') &&
615 typeof window.localStorage.favoriteTables !== 'undefined';
617 url: $self.attr('href'),
621 'favoriteTables': hasLocalStorage ? window.localStorage.favoriteTables : '',
622 'server': CommonParams.get('server'),
624 success: function (data) {
626 $('#pma_favorite_list').html(data.list);
627 $('#' + anchorId).parent().html(data.anchor);
631 $('#' + anchorId).attr('title')
633 // Update localStorage.
634 if (isStorageSupported('localStorage')) {
635 window.localStorage.favoriteTables = data.favoriteTables;
638 Functions.ajaxShowMessage(data.message);
643 // Check if session storage is supported
644 if (isStorageSupported('sessionStorage')) {
645 var storage = window.sessionStorage;
646 // remove tree from storage if Navi_panel config form is submitted
647 $(document).on('submit', 'form.config-form', function () {
648 storage.removeItem('navTreePaths');
650 // Initialize if no previous state is defined
651 if ($('#pma_navigation_tree_content').length &&
652 typeof storage.navTreePaths === 'undefined'
655 } else if (CommonParams.get('server') === storage.server &&
656 CommonParams.get('token') === storage.token
658 // Reload the tree to the state before page refresh
659 Navigation.reload(Navigation.filterStateRestore, JSON.parse(storage.navTreePaths));
661 // If the user is different
662 Navigation.treeStateUpdate();
669 * Expands a node in navigation tree.
671 * @param $expandElem expander
672 * @param callback callback function
676 Navigation.expandTreeNode = function ($expandElem, callback) {
677 var $children = $expandElem.closest('li').children('div.list_container');
678 var $icon = $expandElem.find('img');
679 if ($expandElem.hasClass('loaded')) {
680 if ($icon.is('.ic_b_plus')) {
681 $icon.removeClass('ic_b_plus').addClass('ic_b_minus');
682 $children.slideDown('fast');
684 if (callback && typeof callback === 'function') {
687 $children.promise().done(Navigation.treeStateUpdate);
689 var $throbber = $('#pma_navigation').find('.throbber')
692 .css({ visibility: 'visible', display: 'block' })
695 $throbber.insertBefore($icon);
697 Navigation.loadChildNodes(true, $expandElem, function (data) {
698 if (typeof data !== 'undefined' && data.success === true) {
699 var $destination = $expandElem.closest('li');
700 $icon.removeClass('ic_b_plus').addClass('ic_b_minus');
701 $children = $destination.children('div.list_container');
702 $children.slideDown('fast');
703 if ($destination.find('ul > li').length === 1) {
704 $destination.find('ul > li')
705 .find('a.expander.container')
708 if (callback && typeof callback === 'function') {
711 Navigation.showFullName($destination);
713 Functions.ajaxShowMessage(data.error, false);
717 $children.promise().done(Navigation.treeStateUpdate);
720 $expandElem.trigger('blur');
724 * Auto-scrolls the newly chosen database
726 * @param object $element The element to set to view
727 * @param boolean $forceToTop Whether to force scroll to top
730 Navigation.scrollToView = function ($element, $forceToTop) {
731 Navigation.filterStateRestore();
732 var $container = $('#pma_navigation_tree_content');
733 var elemTop = $element.offset().top - $container.offset().top;
735 var scrollPadding = 20; // extra padding from top of bottom when scrolling to view
736 if (elemTop < 0 || $forceToTop) {
737 $container.stop().animate({
738 scrollTop: elemTop + $container.scrollTop() - scrollPadding
740 } else if (elemTop + textHeight > $container.height()) {
741 $container.stop().animate({
742 scrollTop: elemTop + textHeight - $container.height() + $container.scrollTop() + scrollPadding
748 * Expand the navigation and highlight the current database or table/view
752 Navigation.showCurrent = function () {
753 var db = CommonParams.get('db');
754 var table = CommonParams.get('table');
756 var autoexpand = $('#pma_navigation_tree').hasClass('autoexpand');
758 $('#pma_navigation_tree')
760 .removeClass('selected');
763 $dbItem = findLoadedItem(
764 $('#pma_navigation_tree').find('> div'), db, 'database', !table
766 if ($('#navi_db_select').length &&
767 $('option:selected', $('#navi_db_select')).length
769 if (! Navigation.selectCurrentDatabase()) {
772 // If loaded database in navigation is not same as current one
773 if ($('#pma_navigation_tree_content').find('span.loaded_db').first().text()
774 !== $('#navi_db_select').val()
776 Navigation.loadChildNodes(false, $('option:selected', $('#navi_db_select')), function () {
777 handleTableOrDb(table, $('#pma_navigation_tree_content'));
778 var $children = $('#pma_navigation_tree_content').children('div.list_container');
779 $children.promise().done(Navigation.treeStateUpdate);
782 handleTableOrDb(table, $('#pma_navigation_tree_content'));
784 } else if ($dbItem) {
785 fullExpand(table, $dbItem);
787 } else if ($('#navi_db_select').length && $('#navi_db_select').val()) {
788 $('#navi_db_select').val('').hide().trigger('change');
789 } else if (autoexpand && $('#pma_navigation_tree_content > ul > li.database').length === 1) {
790 // automatically expand the list if there is only single database
792 // find the name of the database
795 $('#pma_navigation_tree_content > ul > li.database').children('a').each(function () {
796 var name = $(this).text();
797 if (!dbItemName && name.trim()) { // if the name is not empty, it is the desired element
802 $dbItem = findLoadedItem(
803 $('#pma_navigation_tree').find('> div'), dbItemName, 'database', !table
806 fullExpand(table, $dbItem);
808 Navigation.showFullName($('#pma_navigation_tree'));
810 function fullExpand (table, $dbItem) {
811 var $expander = $dbItem.children('div').first().children('a.expander');
812 // if not loaded or loaded but collapsed
813 if (! $expander.hasClass('loaded') ||
814 $expander.find('img').is('.ic_b_plus')
816 Navigation.expandTreeNode($expander, function () {
817 handleTableOrDb(table, $dbItem);
820 handleTableOrDb(table, $dbItem);
824 function handleTableOrDb (table, $dbItem) {
826 loadAndHighlightTableOrView($dbItem, table);
828 var $container = $dbItem.children('div.list_container');
829 var $tableContainer = $container.children('ul').children('li.tableContainer');
830 if ($tableContainer.length > 0) {
831 var $expander = $tableContainer.children('div').first().children('a.expander');
832 $tableContainer.addClass('selected');
833 Navigation.expandTreeNode($expander, function () {
834 Navigation.scrollToView($dbItem, true);
837 Navigation.scrollToView($dbItem, true);
842 function findLoadedItem ($container, name, clazz, doSelect) {
844 $container.children('ul').children('li').each(function () {
846 // this is a navigation group, recurse
847 if ($li.is('.navGroup')) {
848 var $container = $li.children('div.list_container');
849 var $childRet = findLoadedItem(
850 $container, name, clazz, doSelect
856 } else { // this is a real navigation item
857 // name and class matches
858 if (((clazz && $li.is('.' + clazz)) || ! clazz) &&
859 $li.children('a').text() === name) {
861 $li.addClass('selected');
863 // taverse up and expand and parent navigation groups
864 $li.parents('.navGroup').each(function () {
865 var $cont = $(this).children('div.list_container');
866 if (! $cont.is(':visible')) {
868 .children('div').first()
869 .children('a.expander')
881 function loadAndHighlightTableOrView ($dbItem, itemName) {
882 var $container = $dbItem.children('div.list_container');
884 var $whichItem = isItemInContainer($container, itemName, 'li.table, li.view');
885 // If item already there in some container
887 // get the relevant container while may also be a subcontainer
888 var $relatedContainer = $whichItem.closest('li.subContainer').length
889 ? $whichItem.closest('li.subContainer')
891 $whichItem = findLoadedItem(
892 $relatedContainer.children('div.list_container'),
896 showTableOrView($whichItem, $relatedContainer.children('div').first().children('a.expander'));
897 // else if item not there, try loading once
899 var $subContainers = $dbItem.find('.subContainer');
900 // If there are subContainers i.e. tableContainer or viewContainer
901 if ($subContainers.length > 0) {
902 var $containers = [];
903 $subContainers.each(function (index) {
904 $containers[index] = $(this);
905 $expander = $containers[index]
906 .children('div').first()
907 .children('a.expander');
908 if (! $expander.hasClass('loaded')) {
909 loadAndShowTableOrView($expander, $containers[index], itemName);
912 // else if no subContainers
915 .children('div').first()
916 .children('a.expander');
917 if (! $expander.hasClass('loaded')) {
918 loadAndShowTableOrView($expander, $dbItem, itemName);
924 function loadAndShowTableOrView ($expander, $relatedContainer, itemName) {
925 Navigation.loadChildNodes(true, $expander, function () {
926 var $whichItem = findLoadedItem(
927 $relatedContainer.children('div.list_container'),
931 showTableOrView($whichItem, $expander);
936 function showTableOrView ($whichItem, $expander) {
937 Navigation.expandTreeNode($expander, function () {
939 Navigation.scrollToView($whichItem, false);
944 function isItemInContainer ($container, name, clazz) {
945 var $whichItem = null;
946 var $items = $container.find(clazz);
947 $items.each(function () {
948 if ($(this).children('a').text() === name) {
949 $whichItem = $(this);
958 * Disable navigation panel settings
962 Navigation.disableSettings = function () {
963 $('#pma_navigation_settings_icon').addClass('hide');
964 $('#pma_navigation_settings').remove();
968 * Ensure that navigation panel settings is properly setup.
973 Navigation.ensureSettings = function (selflink) {
974 $('#pma_navigation_settings_icon').removeClass('hide');
976 if (!$('#pma_navigation_settings').length) {
978 getNaviSettings: true,
979 server: CommonParams.get('server'),
981 var url = $('#pma_navigation').find('a.navigation_url').attr('href');
982 $.post(url, params, function (data) {
983 if (typeof data !== 'undefined' && data.success) {
984 $('#pma_navi_settings_container').html(data.message);
988 $('#pma_navigation_settings').find('form').attr('action', selflink);
990 Functions.ajaxShowMessage(data.error);
994 $('#pma_navigation_settings').find('form').attr('action', selflink);
999 * Reloads the whole navigation tree while preserving its state
1001 * @param function the callback function
1002 * @param Object stored navigation paths
1006 Navigation.reload = function (callback, paths) {
1010 'server': CommonParams.get('server'),
1012 var pathsLocal = paths || Navigation.traverseForPaths();
1013 $.extend(params, pathsLocal);
1014 if ($('#navi_db_select').length) {
1015 params.db = CommonParams.get('db');
1016 requestNaviReload(params);
1019 requestNaviReload(params);
1021 function requestNaviReload (params) {
1022 var url = $('#pma_navigation').find('a.navigation_url').attr('href');
1023 $.post(url, params, function (data) {
1024 if (typeof data !== 'undefined' && data.success) {
1025 $('#pma_navigation_tree').html(data.message).children('div').show();
1026 if ($('#pma_navigation_tree').hasClass('synced')) {
1027 Navigation.selectCurrentDatabase();
1028 Navigation.showCurrent();
1030 // Fire the callback, if any
1031 if (typeof callback === 'function') {
1034 Navigation.treeStateUpdate();
1036 Functions.ajaxShowMessage(data.error);
1042 Navigation.selectCurrentDatabase = function () {
1043 var $naviDbSelect = $('#navi_db_select');
1045 if (!$naviDbSelect.length) {
1049 if (CommonParams.get('db')) { // db selected
1050 $naviDbSelect.show();
1053 $naviDbSelect.val(CommonParams.get('db'));
1054 return $naviDbSelect.val() === CommonParams.get('db');
1058 * Handles any requests to change the page in a branch of a tree
1060 * This can be called from link click or select change event handlers
1062 * @param object $this A jQuery object that points to the element that
1063 * initiated the action of changing the page
1067 Navigation.treePagination = function ($this) {
1068 var $msgbox = Functions.ajaxShowMessage();
1069 var isDbSelector = $this.closest('div.pageselector').is('.dbselector');
1072 if ($this[0].tagName === 'A') {
1073 url = $this.attr('href');
1074 params = 'ajax_request=true';
1075 } else { // tagName === 'SELECT'
1076 url = 'index.php?route=/navigation';
1077 params = $this.closest('form').serialize() + CommonParams.get('arg_separator') + 'ajax_request=true';
1079 var searchClause = Navigation.FastFilter.getSearchClause();
1081 params += CommonParams.get('arg_separator') + 'searchClause=' + encodeURIComponent(searchClause);
1084 params += CommonParams.get('arg_separator') + 'full=true';
1086 var searchClause2 = Navigation.FastFilter.getSearchClause2($this);
1087 if (searchClause2) {
1088 params += CommonParams.get('arg_separator') + 'searchClause2=' + encodeURIComponent(searchClause2);
1091 $.post(url, params, function (data) {
1092 if (typeof data !== 'undefined' && data.success) {
1093 Functions.ajaxRemoveMessage($msgbox);
1096 val = Navigation.FastFilter.getSearchClause();
1097 $('#pma_navigation_tree')
1102 $('#pma_navigation_tree')
1103 .find('li.fast_filter input.searchClause')
1107 var $parent = $this.closest('div.list_container').parent();
1108 val = Navigation.FastFilter.getSearchClause2($this);
1109 $this.closest('div.list_container').html(
1110 $(data.message).children().show()
1113 $parent.find('li.fast_filter input.searchClause').val(val);
1115 $parent.find('span.pos2_value').first().text(
1116 $parent.find('span.pos2_value').last().text()
1118 $parent.find('span.pos3_value').first().text(
1119 $parent.find('span.pos3_value').last().text()
1123 Functions.ajaxShowMessage(data.error);
1124 Functions.handleRedirectAndReload(data);
1126 Navigation.treeStateUpdate();
1131 * @var ResizeHandler Custom object that manages the resizing of the navigation
1133 * XXX: Must only be ever instanciated once
1134 * XXX: Inside event handlers the 'this' object is accessed as 'event.data.resize_handler'
1136 Navigation.ResizeHandler = function () {
1138 * @var int panelWidth Used by the collapser to know where to go
1139 * back to when uncollapsing the panel
1141 this.panelWidth = 0;
1143 * @var string left Used to provide support for RTL languages
1145 this.left = $('html').attr('dir') === 'ltr' ? 'left' : 'right';
1147 * Adjusts the width of the navigation panel to the specified value
1149 * @param {int} position Navigation width in pixels
1153 this.setWidth = function (position) {
1155 if (typeof pos !== 'number') {
1158 var $resizer = $('#pma_navigation_resizer');
1159 var resizerWidth = $resizer.width();
1160 var $collapser = $('#pma_navigation_collapser');
1161 var windowWidth = $(window).width();
1162 $('#pma_navigation').width(pos);
1163 $('body').css('margin-' + this.left, pos + 'px');
1164 // Issue #15127 : Adding fixed positioning to menubar
1165 // Issue #15570 : Panels on homescreen go underneath of floating menubar
1166 $('#floating_menubar')
1167 .css('margin-' + this.left, $('#pma_navigation').width() + $('#pma_navigation_resizer').width())
1170 'position': 'fixed',
1175 .append($('#server-breadcrumb'))
1176 .append($('#topmenucontainer'));
1177 // Allow the DOM to render, then adjust the padding on the body
1178 setTimeout(function () {
1181 $('#floating_menubar').outerHeight(true)
1185 .css('margin-' + this.left, (pos + resizerWidth) + 'px');
1186 $resizer.css(this.left, pos + 'px');
1189 .css(this.left, pos + resizerWidth)
1190 .html(this.getSymbol(pos))
1191 .prop('title', Messages.strShowPanel);
1192 } else if (windowWidth > 768) {
1194 .css(this.left, pos)
1195 .html(this.getSymbol(pos))
1196 .prop('title', Messages.strHidePanel);
1197 $('#pma_navigation_resizer').css({ 'width': '3px' });
1200 .css(this.left, windowWidth - 22)
1201 .html(this.getSymbol(100))
1202 .prop('title', Messages.strHidePanel);
1203 $('#pma_navigation').width(windowWidth);
1204 $('body').css('margin-' + this.left, '0px');
1205 $('#pma_navigation_resizer').css({ 'width': '0px' });
1207 setTimeout(function () {
1208 $(window).trigger('resize');
1212 * Returns the horizontal position of the mouse,
1213 * relative to the outer side of the navigation panel
1215 * @param int pos Navigation width in pixels
1219 this.getPos = function (event) {
1220 var pos = event.pageX;
1221 var windowWidth = $(window).width();
1222 var windowScroll = $(window).scrollLeft();
1223 pos = pos - windowScroll;
1224 if (this.left !== 'left') {
1225 pos = windowWidth - event.pageX;
1229 } else if (pos + 100 >= windowWidth) {
1230 pos = windowWidth - 100;
1232 this.panelWidth = 0;
1237 * Returns the HTML code for the arrow symbol used in the collapser
1239 * @param int width The width of the panel
1243 this.getSymbol = function (width) {
1244 if (this.left === 'left') {
1259 * Event handler for initiating a resize of the panel
1261 * @param object e Event data (contains a reference to Navigation.ResizeHandler)
1265 this.mousedown = function (event) {
1266 event.preventDefault();
1268 .on('mousemove', { 'resize_handler': event.data.resize_handler },
1269 $.throttle(event.data.resize_handler.mousemove, 4))
1270 .on('mouseup', { 'resize_handler': event.data.resize_handler },
1271 event.data.resize_handler.mouseup);
1272 $('body').css('cursor', 'col-resize');
1275 * Event handler for terminating a resize of the panel
1277 * @param object e Event data (contains a reference to Navigation.ResizeHandler)
1281 this.mouseup = function (event) {
1282 $('body').css('cursor', '');
1283 Functions.configSet('NavigationWidth', event.data.resize_handler.getPos(event));
1284 $('#topmenu').menuResizer('resize');
1290 * Event handler for updating the panel during a resize operation
1292 * @param object e Event data (contains a reference to Navigation.ResizeHandler)
1296 this.mousemove = function (event) {
1297 event.preventDefault();
1298 if (event.data && event.data.resize_handler) {
1299 var pos = event.data.resize_handler.getPos(event);
1300 event.data.resize_handler.setWidth(pos);
1302 if ($('.sticky_columns').length !== 0) {
1303 Sql.handleAllStickyColumns();
1307 * Event handler for collapsing the panel
1309 * @param object e Event data (contains a reference to Navigation.ResizeHandler)
1313 this.collapse = function (event) {
1314 event.preventDefault();
1315 var panelWidth = event.data.resize_handler.panelWidth;
1316 var width = $('#pma_navigation').width();
1317 if (width === 0 && panelWidth === 0) {
1320 Functions.configSet('NavigationWidth', panelWidth);
1321 event.data.resize_handler.setWidth(panelWidth);
1322 event.data.resize_handler.panelWidth = width;
1325 * Event handler for resizing the navigation tree height on window resize
1329 this.treeResize = function () {
1330 var $nav = $('#pma_navigation');
1331 var $navTree = $('#pma_navigation_tree');
1332 var $navHeader = $('#pma_navigation_header');
1333 var $navTreeContent = $('#pma_navigation_tree_content');
1334 var height = ($nav.height() - $navHeader.height());
1336 height = height > 50 ? height : 800; // keep min. height
1337 $navTree.height(height);
1338 if ($navTreeContent.length > 0) {
1339 $navTreeContent.height(height - $navTreeContent.position().top);
1341 // TODO: in fast filter search response there is no #pma_navigation_tree_content, needs to be added in php
1343 'overflow-y': 'auto'
1346 // Set content bottom space beacuse of console
1347 $('body').css('margin-bottom', $('#pma_console').height() + 'px');
1349 // Hide the pma_navigation initially when loaded on mobile
1350 if ($(window).width() < 768) {
1353 this.setWidth(Functions.configGet('NavigationWidth', false));
1354 $('#topmenu').menuResizer('resize');
1356 // Register the events for the resizer and the collapser
1357 $(document).on('mousedown', '#pma_navigation_resizer', { 'resize_handler': this }, this.mousedown);
1358 $(document).on('click', '#pma_navigation_collapser', { 'resize_handler': this }, this.collapse);
1360 // Add the correct arrow symbol to the collapser
1361 $('#pma_navigation_collapser').html(this.getSymbol($('#pma_navigation').width()));
1362 // Fix navigation tree height
1363 $(window).on('resize', this.treeResize);
1364 // need to call this now and then, browser might decide
1365 // to show/hide horizontal scrollbars depending on page content width
1366 setInterval(this.treeResize, 2000);
1371 * @var object FastFilter Handles the functionality that allows filtering
1372 * of the items in a branch of the navigation tree
1374 Navigation.FastFilter = {
1376 * Construct for the asynchronous fast filter functionality
1378 * @param object $this A jQuery object pointing to the list container
1379 * which is the nearest parent of the fast filter
1380 * @param string searchClause The query string for the filter
1382 * @return new Navigation.FastFilter.Filter object
1384 Filter: function ($this, searchClause) {
1386 * @var object $this A jQuery object pointing to the list container
1387 * which is the nearest parent of the fast filter
1391 * @var bool searchClause The query string for the filter
1393 this.searchClause = searchClause;
1395 * @var object $clone A clone of the original contents
1396 * of the navigation branch before
1397 * the fast filter was applied
1399 this.$clone = $this.clone();
1401 * @var object xhr A reference to the ajax request that is currently running
1405 * @var int timeout Used to delay the request for asynchronous search
1407 this.timeout = null;
1409 var $filterInput = $this.find('li.fast_filter input.searchClause');
1410 if ($filterInput.length !== 0 &&
1411 $filterInput.val() !== '' &&
1412 $filterInput.val() !== $filterInput[0].defaultValue
1418 * Gets the query string from the database fast filter form
1422 getSearchClause: function () {
1424 var $input = $('#pma_navigation_tree')
1425 .find('li.fast_filter.db_fast_filter input.searchClause');
1426 if ($input.length && $input.val() !== $input[0].defaultValue) {
1427 retval = $input.val();
1432 * Gets the query string from a second level item's fast filter form
1433 * The retrieval is done by trasversing the navigation tree backwards
1437 getSearchClause2: function ($this) {
1438 var $filterContainer = $this.closest('div.list_container');
1439 var $filterInput = $([]);
1440 if ($filterContainer
1441 .find('li.fast_filter:not(.db_fast_filter) input.searchClause')
1443 $filterInput = $filterContainer
1444 .find('li.fast_filter:not(.db_fast_filter) input.searchClause');
1446 var searchClause2 = '';
1447 if ($filterInput.length !== 0 &&
1448 $filterInput.first().val() !== $filterInput[0].defaultValue
1450 searchClause2 = $filterInput.val();
1452 return searchClause2;
1455 * @var hash events A list of functions that are bound to DOM events
1456 * at the top of this file
1459 focus: function () {
1460 var $obj = $(this).closest('div.list_container');
1461 if (! $obj.data('fastFilter')) {
1464 new Navigation.FastFilter.Filter($obj, $(this).val())
1467 if ($(this).val() === this.defaultValue) {
1470 $(this).trigger('select');
1474 if ($(this).val() === '') {
1475 $(this).val(this.defaultValue);
1477 var $obj = $(this).closest('div.list_container');
1478 if ($(this).val() === this.defaultValue && $obj.data('fastFilter')) {
1479 $obj.data('fastFilter').restore();
1482 keyup: function (event) {
1483 var $obj = $(this).closest('div.list_container');
1485 if ($(this).val() !== this.defaultValue && $(this).val() !== '') {
1486 $obj.find('div.pageselector').hide();
1487 str = $(this).val();
1491 * FIXME at the server level a value match is done while on
1492 * the client side it is a regex match. These two should be aligned
1495 // regex used for filtering.
1498 regex = new RegExp(str, 'i');
1503 // this is the div that houses the items to be filtered by this filter.
1505 if ($(this).closest('li.fast_filter').is('.db_fast_filter')) {
1506 outerContainer = $('#pma_navigation_tree_content');
1508 outerContainer = $obj;
1511 // filters items that are directly under the div as well as grouped in
1512 // groups. Does not filter child items (i.e. a database search does
1513 // not filter tables)
1514 var itemFilter = function ($curr) {
1515 $curr.children('ul').children('li.navGroup').each(function () {
1516 $(this).children('div.list_container').each(function () {
1517 itemFilter($(this)); // recursive
1520 $curr.children('ul').children('li').children('a').not('.container').each(function () {
1521 if (regex.test($(this).text())) {
1522 $(this).parent().show().removeClass('hidden');
1524 $(this).parent().hide().addClass('hidden');
1528 itemFilter(outerContainer);
1530 // hides containers that does not have any visible children
1531 var containerFilter = function ($curr) {
1532 $curr.children('ul').children('li.navGroup').each(function () {
1533 var $group = $(this);
1534 $group.children('div.list_container').each(function () {
1535 containerFilter($(this)); // recursive
1537 $group.show().removeClass('hidden');
1538 if ($group.children('div.list_container').children('ul')
1539 .children('li').not('.hidden').length === 0) {
1540 $group.hide().addClass('hidden');
1544 containerFilter(outerContainer);
1546 if ($(this).val() !== this.defaultValue && $(this).val() !== '') {
1547 if (! $obj.data('fastFilter')) {
1550 new Navigation.FastFilter.Filter($obj, $(this).val())
1553 if (event.keyCode === 13) {
1554 $obj.data('fastFilter').update($(this).val());
1557 } else if ($obj.data('fastFilter')) {
1558 $obj.data('fastFilter').restore(true);
1560 // update filter state
1562 if ($(this).attr('name') === 'searchClause2') {
1563 filterName = $(this).siblings('input[name=aPath]').val();
1565 filterName = 'dbFilter';
1567 Navigation.filterStateUpdate(filterName, $(this).val());
1569 clear: function (event) {
1570 event.stopPropagation();
1571 // Clear the input and apply the fast filter with empty input
1572 var filter = $(this).closest('div.list_container').data('fastFilter');
1576 var value = $(this).prev()[0].defaultValue;
1577 $(this).prev().val(value).trigger('keyup');
1582 * Handles a change in the search clause
1584 * @param string searchClause The query string for the filter
1588 Navigation.FastFilter.Filter.prototype.update = function (searchClause) {
1589 if (this.searchClause !== searchClause) {
1590 this.searchClause = searchClause;
1595 * After a delay of 250mS, initiates a request to retrieve search results
1596 * Multiple calls to this function will always abort the previous request
1600 Navigation.FastFilter.Filter.prototype.request = function () {
1602 if (self.$this.find('li.fast_filter').find('img.throbber').length === 0) {
1603 self.$this.find('li.fast_filter').append(
1604 $('<div class="throbber"></div>').append(
1605 $('#pma_navigation_content')
1606 .find('img.throbber')
1608 .css({ visibility: 'visible', display: 'block' })
1615 var url = $('#pma_navigation').find('a.navigation_url').attr('href');
1616 var params = self.$this.find('> ul > li > form.fast_filter').first().serialize();
1618 if (self.$this.find('> ul > li > form.fast_filter').first().find('input[name=searchClause]').length === 0) {
1619 var $input = $('#pma_navigation_tree').find('li.fast_filter.db_fast_filter input.searchClause');
1620 if ($input.length && $input.val() !== $input[0].defaultValue) {
1621 params += CommonParams.get('arg_separator') + 'searchClause=' + encodeURIComponent($input.val());
1629 complete: function (jqXHR, status) {
1630 if (status !== 'abort') {
1631 var data = JSON.parse(jqXHR.responseText);
1632 self.$this.find('li.fast_filter').find('div.throbber').remove();
1633 if (data && data.results) {
1634 self.swap.apply(self, [data.message]);
1641 * Replaces the contents of the navigation branch with the search results
1643 * @param string list The search results
1647 Navigation.FastFilter.Filter.prototype.swap = function (list) {
1649 .html($(list).html())
1653 .find('li.fast_filter input.searchClause')
1654 .val(this.searchClause);
1655 this.$this.data('fastFilter', this);
1658 * Restores the navigation to the original state after the fast filter is cleared
1660 * @param bool focus Whether to also focus the input box of the fast filter
1664 Navigation.FastFilter.Filter.prototype.restore = function (focus) {
1665 if (this.$this.children('ul').first().hasClass('search_results')) {
1666 this.$this.html(this.$clone.html()).children().show();
1667 this.$this.data('fastFilter', this);
1669 this.$this.find('li.fast_filter input.searchClause').trigger('focus');
1672 this.searchClause = '';
1673 this.$this.find('div.pageselector').show();
1674 this.$this.find('div.throbber').remove();
1678 * Show full name when cursor hover and name not shown completely
1680 * @param object $containerELem Container element
1684 Navigation.showFullName = function ($containerELem) {
1685 $containerELem.find('.hover_show_full').on('mouseenter', function () {
1687 var $this = $(this);
1688 var thisOffset = $this.offset();
1689 if ($this.text() === '') {
1692 var $parent = $this.parent();
1693 if (($parent.offset().left + $parent.outerWidth())
1694 < (thisOffset.left + $this.outerWidth())) {
1695 var $fullNameLayer = $('#full_name_layer');
1696 if ($fullNameLayer.length === 0) {
1697 $('body').append('<div id="full_name_layer" class="hide"></div>');
1698 $('#full_name_layer').on('mouseleave', function () {
1700 $(this).addClass('hide')
1701 .removeClass('hovering');
1702 }).on('mouseenter', function () {
1704 $(this).addClass('hovering');
1706 $fullNameLayer = $('#full_name_layer');
1708 $fullNameLayer.removeClass('hide');
1709 $fullNameLayer.css({ left: thisOffset.left, top: thisOffset.top });
1710 $fullNameLayer.html($this.clone());
1711 setTimeout(function () {
1712 if (! $fullNameLayer.hasClass('hovering')) {
1713 $fullNameLayer.trigger('mouseleave');