UPDATE 4.4.0.0
[phpmyadmin.git] / js / navigation.js
blob9796d1f64c635a42381ac713b0566571003f7bed
1 /* vim: set expandtab sw=4 ts=4 sts=4: */
2 /**
3  * function used in or for navigation panel
4  *
5  * @package phpMyAdmin-Navigation
6  */
8 /**
9  * Loads child items of a node and executes a given callback
10  *
11  * @param isNode
12  * @param $expandElem expander
13  * @param callback    callback function
14  *
15  * @returns void
16  */
17 function loadChildNodes(isNode, $expandElem, callback) {
19     var $destination = null;
20     var params = null;
22     if (isNode) {
23         if (!$expandElem.hasClass('expander')) {
24             return;
25         }
26         $destination = $expandElem.closest('li');
27         params = {
28             aPath: $expandElem.find('span.aPath').text(),
29             vPath: $expandElem.find('span.vPath').text(),
30             pos: $expandElem.find('span.pos').text(),
31             pos2_name: $expandElem.find('span.pos2_name').text(),
32             pos2_value: $expandElem.find('span.pos2_value').text(),
33             searchClause: '',
34             searchClause2: ''
35         };
36         if ($expandElem.closest('ul').hasClass('search_results')) {
37             params.searchClause = PMA_fastFilter.getSearchClause();
38             params.searchClause2 = PMA_fastFilter.getSearchClause2($expandElem);
39         }
40     } else {
41         $destination = $('#pma_navigation_tree_content');
42         params = {
43             aPath: $expandElem.attr('aPath'),
44             vPath: $expandElem.attr('vPath'),
45             pos: $expandElem.attr('pos'),
46             pos2_name: '',
47             pos2_value: '',
48             searchClause: '',
49             searchClause2: ''
50         };
51     }
53     var url = $('#pma_navigation').find('a.navigation_url').attr('href');
54     $.get(url, params, function (data) {
55         if (typeof data !== 'undefined' && data.success === true) {
56             $destination.find('div.list_container').remove(); // FIXME: Hack, there shouldn't be a list container there
57             if (isNode) {
58                 $destination.append(data.message);
59                 $expandElem.addClass('loaded');
60             } else {
61                 $destination.html(data.message);
62                 $destination.children()
63                     .first()
64                     .css({
65                         border: '0px',
66                         margin: '0em',
67                         padding : '0em'
68                     })
69                     .slideDown('slow');
70             }
71             if (data._debug){
72                 $('#session_debug').replaceWith(data._debug);
73             }
74             if (data._errors) {
75                 $errors = $(data._errors);
76                 if ($errors.children().length > 0) {
77                     $('#pma_errors').replaceWith(data._errors);
78                 }
79             }
80             if (callback && typeof callback == 'function') {
81                 callback(data);
82             }
83         } else if(data.redirect_flag == "1") {
84             window.location.href += '&session_expired=1';
85             window.location.reload();
86         } else {
87             var $throbber = $expandElem.find('img.throbber');
88             $throbber.hide();
89             $icon = $expandElem.find('img.ic_b_plus');
90             $icon.show();
91             PMA_ajaxShowMessage(data.error, false);
92         }
93     });
96 /**
97  * Collapses a node in navigation tree.
98  *
99  * @param $expandElem expander
101  * @returns void
102  */
103 function collapseTreeNode($expandElem) {
104     var $children = $expandElem.closest('li').children('div.list_container');
105     var $icon = $expandElem.find('img');
106     if ($expandElem.hasClass('loaded')) {
107         if ($icon.is('.ic_b_minus')) {
108             $icon.removeClass('ic_b_minus').addClass('ic_b_plus');
109             $children.slideUp('fast');
110         }
111     }
112     $expandElem.blur();
113     $children.promise().done(navTreeStateUpdate);
117  * Traverse the navigation tree backwards to generate all the actual
118  * and virtual paths, as well as the positions in the pagination at
119  * various levels, if necessary.
121  * @return Object
122  */
123 function traverseNavigationForPaths() {
124     var params = {
125         pos: $('#pma_navigation_tree div.dbselector select').val()
126     };
127     if ($('#navi_db_select').length) {
128         return params;
129     }
130     var count = 0;
131     $('#pma_navigation_tree').find('a.expander:visible').each(function () {
132         if ($(this).find('img').is('.ic_b_minus') &&
133             $(this).closest('li').find('div.list_container .ic_b_minus').length === 0
134         ) {
135             params['n' + count + '_aPath'] = $(this).find('span.aPath').text();
136             params['n' + count + '_vPath'] = $(this).find('span.vPath').text();
138             var pos2_name = $(this).find('span.pos2_name').text();
139             if (! pos2_name) {
140                 pos2_name = $(this)
141                     .parent()
142                     .parent()
143                     .find('span.pos2_name:last')
144                     .text();
145             }
146             var pos2_value = $(this).find('span.pos2_value').text();
147             if (! pos2_value) {
148                 pos2_value = $(this)
149                     .parent()
150                     .parent()
151                     .find('span.pos2_value:last')
152                     .text();
153             }
155             params['n' + count + '_pos2_name'] = pos2_name;
156             params['n' + count + '_pos2_value'] = pos2_value;
158             params['n' + count + '_pos3_name'] = $(this).find('span.pos3_name').text();
159             params['n' + count + '_pos3_value'] = $(this).find('span.pos3_value').text();
160             count++;
161         }
162     });
163     return params;
167  * Executed on page load
168  */
169 $(function () {
170     if (! $('#pma_navigation').length) {
171         // Don't bother running any code if the navigation is not even on the page
172         return;
173     }
175     // Do not let the page reload on submitting the fast filter
176     $(document).on('submit', '.fast_filter', function (event) {
177         event.preventDefault();
178     });
180     // Fire up the resize handlers
181     new ResizeHandler();
183     /**
184      * opens/closes (hides/shows) tree elements
185      * loads data via ajax
186      */
187     $(document).on('click', '#pma_navigation_tree a.expander', function (event) {
188         event.preventDefault();
189         event.stopImmediatePropagation();
190         var $icon = $(this).find('img');
191         if ($icon.is('.ic_b_plus')) {
192             expandTreeNode($(this));
193         } else {
194             collapseTreeNode($(this));
195         }
196     });
198     /**
199      * Register event handler for click on the reload
200      * navigation icon at the top of the panel
201      */
202     $(document).on('click', '#pma_navigation_reload', function (event) {
203         event.preventDefault();
204         // reload icon object
205         var $icon = $(this).find('img');
206         // source of the hidden throbber icon
207         var icon_throbber_src = $('#pma_navigation .throbber').attr('src');
208         // source of the reload icon
209         var icon_reload_src = $icon.attr('src');
210         // replace the source of the reload icon with the one for throbber
211         $icon.attr('src', icon_throbber_src);
212         PMA_reloadNavigation();
213         // after one second, put back the reload icon
214         setTimeout(function () {
215             $icon.attr('src', icon_reload_src);
216         }, 1000);
217     });
219     $(document).on("change", '#navi_db_select',  function (event) {
220         $(this).closest('form').trigger('submit');
221     });
223     /**
224      * Register event handler for click on the collapse all
225      * navigation icon at the top of the navigation tree
226      */
227     $(document).on('click', '#pma_navigation_collapse', function (event) {
228         event.preventDefault();
229         $('#pma_navigation_tree a.expander').each(function() {
230             var $icon = $(this).find('img');
231             if ($icon.is('.ic_b_minus')) {
232                 $(this).click();
233             }
234         });
235     });
237     /**
238      * Register event handler to toggle
239      * the 'link with main panel' icon on mouseenter.
240      */
241     $(document).on('mouseenter', '#pma_navigation_sync', function (event) {
242         event.preventDefault();
243         var synced = $('#pma_navigation_tree').hasClass('synced');
244         var $img = $('#pma_navigation_sync').children('img');
245         if (synced) {
246             $img.removeClass('ic_s_link').addClass('ic_s_unlink');
247         } else {
248             $img.removeClass('ic_s_unlink').addClass('ic_s_link');
249         }
250     });
252     /**
253      * Register event handler to toggle
254      * the 'link with main panel' icon on mouseout.
255      */
256     $(document).on('mouseout', '#pma_navigation_sync', function (event) {
257         event.preventDefault();
258         var synced = $('#pma_navigation_tree').hasClass('synced');
259         var $img = $('#pma_navigation_sync').children('img');
260         if (synced) {
261             $img.removeClass('ic_s_unlink').addClass('ic_s_link');
262         } else {
263             $img.removeClass('ic_s_link').addClass('ic_s_unlink');
264         }
265     });
267     /**
268      * Register event handler to toggle
269      * the linking with main panel behavior
270      */
271     $(document).on('click', '#pma_navigation_sync', function (event) {
272         event.preventDefault();
273         var synced = $('#pma_navigation_tree').hasClass('synced');
274         var $img = $('#pma_navigation_sync').children('img');
275         if (synced) {
276             $img
277                 .removeClass('ic_s_unlink')
278                 .addClass('ic_s_link')
279                 .attr('alt', PMA_messages.linkWithMain)
280                 .attr('title', PMA_messages.linkWithMain);
281             $('#pma_navigation_tree')
282                 .removeClass('synced')
283                 .find('li.selected')
284                 .removeClass('selected');
285         } else {
286             $img
287                 .removeClass('ic_s_link')
288                 .addClass('ic_s_unlink')
289                 .attr('alt', PMA_messages.unlinkWithMain)
290                 .attr('title', PMA_messages.unlinkWithMain);
291             $('#pma_navigation_tree').addClass('synced');
292             PMA_showCurrentNavigation();
293         }
294     });
296     /**
297      * Bind all "fast filter" events
298      */
299     $(document).on('click', '#pma_navigation_tree li.fast_filter span', PMA_fastFilter.events.clear);
300     $(document).on('focus', '#pma_navigation_tree li.fast_filter input.searchClause', PMA_fastFilter.events.focus);
301     $(document).on('blur', '#pma_navigation_tree li.fast_filter input.searchClause', PMA_fastFilter.events.blur);
302     $(document).on('keyup', '#pma_navigation_tree li.fast_filter input.searchClause', PMA_fastFilter.events.keyup);
303     $(document).on('mouseover', '#pma_navigation_tree li.fast_filter input.searchClause', PMA_fastFilter.events.mouseover);
305     /**
306      * Ajax handler for pagination
307      */
308     $(document).on('click', '#pma_navigation_tree div.pageselector a.ajax', function (event) {
309         event.preventDefault();
310         PMA_navigationTreePagination($(this));
311     });
313     /**
314      * Node highlighting
315      */
316     $(document).on(
317         'mouseover',
318         '#pma_navigation_tree.highlight li:not(.fast_filter)',
319         function () {
320             if ($('li:visible', this).length === 0) {
321                 $(this).addClass('activePointer');
322             }
323         }
324     );
325     $(document).on(
326         'mouseout',
327         '#pma_navigation_tree.highlight li:not(.fast_filter)',
328         function () {
329             $(this).removeClass('activePointer');
330         }
331     );
333     /** Create a Routine, Trigger or Event */
334     $(document).on('click', 'li.new_procedure a.ajax, li.new_function a.ajax', function (event) {
335         event.preventDefault();
336         var dialog = new RTE.object('routine');
337         dialog.editorDialog(1, $(this));
338     });
339     $(document).on('click', 'li.new_trigger a.ajax', function (event) {
340         event.preventDefault();
341         var dialog = new RTE.object('trigger');
342         dialog.editorDialog(1, $(this));
343     });
344     $(document).on('click', 'li.new_event a.ajax', function (event) {
345         event.preventDefault();
346         var dialog = new RTE.object('event');
347         dialog.editorDialog(1, $(this));
348     });
350     /** Execute Routines */
351     $(document).on('click', 'li.procedure > a.ajax, li.function > a.ajax', function (event) {
352         event.preventDefault();
353         var dialog = new RTE.object('routine');
354         dialog.executeDialog($(this));
355     });
356     /** Edit Triggers and Events */
357     $(document).on('click', 'li.trigger > a.ajax', function (event) {
358         event.preventDefault();
359         var dialog = new RTE.object('trigger');
360         dialog.editorDialog(0, $(this));
361     });
362     $(document).on('click', 'li.event > a.ajax', function (event) {
363         event.preventDefault();
364         var dialog = new RTE.object('event');
365         dialog.editorDialog(0, $(this));
366     });
368     /** Edit Routines */
369     $(document).on('click', 'li.procedure div a.ajax img,' +
370         ' li.function div a.ajax img', function (event) {
371         event.preventDefault();
372         var dialog = new RTE.object('routine');
373         dialog.editorDialog(0, $(this).parent());
374     });
375     /** Export Triggers and Events */
376     $(document).on('click', 'li.trigger div:eq(1) a.ajax img, li.event div:eq(1) a.ajax img', function (event) {
377         event.preventDefault();
378         var dialog = new RTE.object();
379         dialog.exportDialog($(this).parent());
380     });
382     /** New index */
383     $(document).on('click', '#pma_navigation_tree li.new_index a.ajax', function (event) {
384         event.preventDefault();
385         var url = $(this).attr('href').substr(
386             $(this).attr('href').indexOf('?') + 1
387         ) + '&ajax_request=true';
388         var title = PMA_messages.strAddIndex;
389         indexEditorDialog(url, title);
390     });
392     /** Edit index */
393     $(document).on('click', 'li.index a.ajax', function (event) {
394         event.preventDefault();
395         var url = $(this).attr('href').substr(
396             $(this).attr('href').indexOf('?') + 1
397         ) + '&ajax_request=true';
398         var title = PMA_messages.strEditIndex;
399         indexEditorDialog(url, title);
400     });
402     /** New view */
403     $(document).on('click', 'li.new_view a.ajax', function (event) {
404         event.preventDefault();
405         PMA_createViewDialog($(this));
406     });
408     /** Hide navigation tree item */
409     $(document).on('click', 'a.hideNavItem.ajax', function (event) {
410         event.preventDefault();
411         $.ajax({
412             url: $(this).attr('href') + '&ajax_request=true',
413             success: function (data) {
414                 if (typeof data !== 'undefined' && data.success === true) {
415                     PMA_reloadNavigation();
416                 } else {
417                     PMA_ajaxShowMessage(data.error);
418                 }
419             }
420         });
421     });
423     /** Display a dialog to choose hidden navigation items to show */
424     $(document).on('click', 'a.showUnhide.ajax', function (event) {
425         event.preventDefault();
426         var $msg = PMA_ajaxShowMessage();
427         $.get($(this).attr('href') + '&ajax_request=1', function (data) {
428             if (typeof data !== 'undefined' && data.success === true) {
429                 PMA_ajaxRemoveMessage($msg);
430                 var buttonOptions = {};
431                 buttonOptions[PMA_messages.strClose] = function () {
432                     $(this).dialog("close");
433                 };
434                 $('<div/>')
435                     .attr('id', 'unhideNavItemDialog')
436                     .append(data.message)
437                     .dialog({
438                         width: 400,
439                         minWidth: 200,
440                         modal: true,
441                         buttons: buttonOptions,
442                         title: PMA_messages.strUnhideNavItem,
443                         close: function () {
444                             $(this).remove();
445                         }
446                     });
447             } else {
448                 PMA_ajaxShowMessage(data.error);
449             }
450         });
451     });
453     /** Show a hidden navigation tree item */
454     $(document).on('click', 'a.unhideNavItem.ajax', function (event) {
455         event.preventDefault();
456         var $tr = $(this).parents('tr');
457         var $msg = PMA_ajaxShowMessage();
458         $.ajax({
459             url: $(this).attr('href') + '&ajax_request=true',
460             success: function (data) {
461                 PMA_ajaxRemoveMessage($msg);
462                 if (typeof data !== 'undefined' && data.success === true) {
463                     $tr.remove();
464                     PMA_reloadNavigation();
465                 } else {
466                     PMA_ajaxShowMessage(data.error);
467                 }
468             }
469         });
470     });
472     // Add/Remove favorite table using Ajax.
473     $(document).on("click", ".favorite_table_anchor", function (event) {
474         event.preventDefault();
475         $self = $(this);
476         var anchor_id = $self.attr("id");
477         if($self.data("favtargetn") !== null) {
478             if($('a[data-favtargets="' + $self.data("favtargetn") + '"]').length > 0)
479             {
480                 $('a[data-favtargets="' + $self.data("favtargetn") + '"]').trigger('click');
481                 return;
482             }
483         }
485         $.ajax({
486             url: $self.attr('href'),
487             cache: false,
488             type: 'POST',
489             data: {
490                 favorite_tables: (isStorageSupported('localStorage'))
491                     ? window.localStorage.favorite_tables
492                     : ''
493             },
494             success: function (data) {
495                 if (data.changes) {
496                     $('#pma_favorite_list').html(data.list);
497                     $('#' + anchor_id).parent().html(data.anchor);
498                     PMA_tooltip(
499                         $('#' + anchor_id),
500                         'a',
501                         $('#' + anchor_id).attr("title")
502                     );
503                     // Update localStorage.
504                     if (isStorageSupported('localStorage')) {
505                         window.localStorage.favorite_tables = data.favorite_tables;
506                     }
507                 } else {
508                     PMA_ajaxShowMessage(data.message);
509                 }
510             }
511         });
512     });
514     // Check if session storage is supported
515     if (isStorageSupported('sessionStorage')) {
516         var storage = window.sessionStorage;
517         // remove tree from storage if Navi_panel config form is submitted
518         $(document).on('submit', 'form.config-form', function(event) {
519             storage.removeItem('navTreePaths');
520         });
521         // Initialize if no previous state is defined
522         if ($('#pma_navigation_tree_content').length
523             && typeof storage.navTreePaths === 'undefined'
524         ) {
525             navTreeStateUpdate();
526         } else if (PMA_commonParams.get('server') === storage.server &&
527             PMA_commonParams.get('token') === storage.token
528         ) {
529             // Reload the tree to the state before page refresh
530             PMA_reloadNavigation(null, JSON.parse(storage.navTreePaths));
531         }
532     }
536  * updates the tree state in sessionStorage
538  * @returns void
539  */
540 function navTreeStateUpdate() {
541     // update if session storage is supported
542     if (isStorageSupported('sessionStorage')) {
543         var storage = window.sessionStorage;
544         // try catch necessary here to detect whether
545         // content to be stored exceeds storage capacity
546         try {
547             storage.setItem('navTreePaths', JSON.stringify(traverseNavigationForPaths()));
548             storage.setItem('server', PMA_commonParams.get('server'));
549             storage.setItem('token', PMA_commonParams.get('token'));
550         } catch(error) {
551             // storage capacity exceeded & old navigation tree
552             // state is no more valid, so remove it
553             storage.removeItem('navTreePaths');
554             storage.removeItem('server');
555             storage.removeItem('token');
556         }
557     }
561  * Expands a node in navigation tree.
563  * @param $expandElem expander
564  * @param callback    callback function
566  * @returns void
567  */
568 function expandTreeNode($expandElem, callback) {
569     var $children = $expandElem.closest('li').children('div.list_container');
570     var $icon = $expandElem.find('img');
571     if ($expandElem.hasClass('loaded')) {
572         if ($icon.is('.ic_b_plus')) {
573             $icon.removeClass('ic_b_plus').addClass('ic_b_minus');
574             $children.slideDown('fast');
575         }
576         if (callback && typeof callback == 'function') {
577             callback.call();
578         }
579         $children.promise().done(navTreeStateUpdate);
580     } else {
581         var $throbber = $('#pma_navigation .throbber')
582             .first()
583             .clone()
584             .css({visibility: 'visible', display: 'block'})
585             .click(false);
586         $icon.hide();
587         $throbber.insertBefore($icon);
589         loadChildNodes(true, $expandElem, function (data) {
590             if (typeof data !== 'undefined' && data.success === true) {
591                 var $destination = $expandElem.closest('li');
592                 $icon.removeClass('ic_b_plus').addClass('ic_b_minus');
593                 $children = $destination.children('div.list_container');
594                 $children.slideDown('fast');
595                 if ($destination.find('ul > li').length == 1) {
596                     $destination.find('ul > li')
597                         .find('a.expander.container')
598                         .click();
599                 }
600                 if (callback && typeof callback == 'function') {
601                     callback.call();
602                 }
603                 PMA_showFullName($destination);
604             } else {
605                 PMA_ajaxShowMessage(data.error, false);
606             }
607             $icon.show();
608             $throbber.remove();
609             $children.promise().done(navTreeStateUpdate);
610         });
611     }
612     $expandElem.blur();
616  * Auto-scrolls the newly chosen database
618  * @param  object   $element    The element to set to view
619  * @param  boolean  $forceToTop Whether to force scroll to top
621  */
622 function scrollToView($element, $forceToTop) {
623     var $container = $('#pma_navigation_tree_content');
624     var elemTop = $element.offset().top - $container.offset().top;
625     var textHeight = 20;
626     var scrollPadding = 20; // extra padding from top of bottom when scrolling to view
627     if (elemTop < 0 || $forceToTop) {
628         $container.stop().animate({
629             scrollTop: elemTop + $container.scrollTop() - scrollPadding
630         });
631     } else if (elemTop + textHeight > $container.height()) {
632         $container.stop().animate({
633             scrollTop: elemTop + textHeight - $container.height() + $container.scrollTop() + scrollPadding
634         });
635     }
639  * Expand the navigation and highlight the current database or table/view
641  * @returns void
642  */
643 function PMA_showCurrentNavigation() {
644     var db = PMA_commonParams.get('db');
645     var table = PMA_commonParams.get('table');
646     $('#pma_navigation_tree')
647         .find('li.selected')
648         .removeClass('selected');
649     if (db) {
650         var $dbItem = findLoadedItem(
651             $('#pma_navigation_tree > div'), db, 'database', !table
652         );
653         if ($dbItem) {
654             var $expander = $dbItem.children('div:first').children('a.expander');
655             // if not loaded or loaded but collapsed
656             if (! $expander.hasClass('loaded') ||
657                 $expander.find('img').is('.ic_b_plus')
658             ) {
659                 expandTreeNode($expander, function () {
660                     handleTableOrDb(table, $dbItem);
661                 });
662             } else {
663                 handleTableOrDb(table, $dbItem);
664             }
665         } else if ($('#navi_db_select').length
666                 && $('option:selected', $('#navi_db_select')).length
667         ) {
668             if (! PMA_selectCurrentDb()) {
669                 return;
670             }
671             // If loaded database in navigation is not same as current one
672             if ( $('#pma_navigation_tree_content span.loaded_db:first').text()
673                 !== $('#navi_db_select').val()
674             ) {
675                 loadChildNodes(false, $('option:selected', $('#navi_db_select')), function (data) {
676                     handleTableOrDb(table, $('#pma_navigation_tree_content'));
677                     var $children = $('#pma_navigation_tree_content').children('div.list_container');
678                     $children.promise().done(navTreeStateUpdate);
679                 });
680             } else {
681                 handleTableOrDb(table, $('#pma_navigation_tree_content'));
682             }
683         }
684     }
685     PMA_showFullName($('#pma_navigation_tree'));
687     function handleTableOrDb(table, $dbItem) {
688         if (table) {
689             loadAndHighlightTableOrView($dbItem, table);
690         } else {
691             var $container = $dbItem.children('div.list_container');
692             var $tableContainer = $container.children('ul').children('li.tableContainer');
693             if ($tableContainer.length > 0) {
694                 var $expander = $tableContainer.children('div:first').children('a.expander');
695                 $tableContainer.addClass('selected');
696                 expandTreeNode($expander, function () {
697                     scrollToView($dbItem, true);
698                 });
699             } else {
700                 scrollToView($dbItem, true);
701             }
702         }
703     }
705     function findLoadedItem($container, name, clazz, doSelect) {
706         var ret = false;
707         $container.children('ul').children('li').each(function () {
708             var $li = $(this);
709             // this is a navigation group, recurse
710             if ($li.is('.navGroup')) {
711                 var $container = $li.children('div.list_container');
712                 var $childRet = findLoadedItem(
713                     $container, name, clazz, doSelect
714                 );
715                 if ($childRet) {
716                     ret = $childRet;
717                     return false;
718                 }
719             } else { // this is a real navigation item
720                 // name and class matches
721                 if (((clazz && $li.is('.' + clazz)) || ! clazz) &&
722                         $li.children('a').text() == name) {
723                     if (doSelect) {
724                         $li.addClass('selected');
725                     }
726                     // taverse up and expand and parent navigation groups
727                     $li.parents('.navGroup').each(function () {
728                         $cont = $(this).children('div.list_container');
729                         if (! $cont.is(':visible')) {
730                             $(this)
731                                 .children('div:first')
732                                 .children('a.expander')
733                                 .click();
734                         }
735                     });
736                     ret = $li;
737                     return false;
738                 }
739             }
740         });
741         return ret;
742     }
744     function loadAndHighlightTableOrView($dbItem, itemName) {
745         var $container = $dbItem.children('div.list_container');
746         var $expander;
747         var $whichItem = isItemInContainer($container, itemName, 'li.table, li.view');
748         //If item already there in some container
749         if ($whichItem) {
750             //get the relevant container while may also be a subcontainer
751             var $relatedContainer = $whichItem.closest('li.subContainer').length
752                 ? $whichItem.closest('li.subContainer')
753                 : $dbItem;
754             $whichItem = findLoadedItem(
755                 $relatedContainer.children('div.list_container'),
756                 itemName, null, true
757             );
758             //Show directly
759             showTableOrView($whichItem, $relatedContainer.children('div:first').children('a.expander'));
760         //else if item not there, try loading once
761         } else {
762             var $sub_containers = $dbItem.find('.subContainer');
763             //If there are subContainers i.e. tableContainer or viewContainer
764             if($sub_containers.length > 0) {
765                 var $containers = [];
766                 $sub_containers.each(function (index) {
767                     $containers[index] = $(this);
768                     $expander = $containers[index]
769                         .children('div:first')
770                         .children('a.expander');
771                     collapseTreeNode($expander);
772                     loadAndShowTableOrView($expander, $containers[index], itemName);
773                 });
774             // else if no subContainers
775             } else {
776                 $expander = $dbItem
777                     .children('div:first')
778                     .children('a.expander');
779                 collapseTreeNode($expander);
780                 loadAndShowTableOrView($expander, $dbItem, itemName);
781             }
782         }
783     }
785     function loadAndShowTableOrView($expander, $relatedContainer, itemName) {
786         loadChildNodes(true, $expander, function (data) {
787             var $whichItem = findLoadedItem(
788                 $relatedContainer.children('div.list_container'),
789                 itemName, null, true
790             );
791             if ($whichItem) {
792                 showTableOrView($whichItem, $expander);
793             }
794         });
795     }
797     function showTableOrView($whichItem, $expander) {
798         expandTreeNode($expander, function (data) {
799             if ($whichItem) {
800                 scrollToView($whichItem, false);
801             }
802         });
803     }
805     function isItemInContainer($container, name, clazz)
806     {
807         var $whichItem = null;
808         $items = $container.find(clazz);
809         var found = false;
810         $items.each(function () {
811             if ($(this).children('a').text() == name) {
812                 $whichItem = $(this);
813                 return false;
814             }
815         });
816         return $whichItem;
817     }
821  * Reloads the whole navigation tree while preserving its state
823  * @param  function     the callback function
824  * @param  Object       stored navigation paths
826  * @return void
827  */
828 function PMA_reloadNavigation(callback, paths) {
829     var params = {
830         reload: true
831     };
832     paths = paths || traverseNavigationForPaths();
833     $.extend(params, paths);
834     if ($('#navi_db_select').length) {
835         params.db = PMA_commonParams.get('db');
836         requestNaviReload(params);
837         return;
838     }
839     requestNaviReload(params);
841     function requestNaviReload(params) {
842         var url = $('#pma_navigation').find('a.navigation_url').attr('href');
843         $.post(url, params, function (data) {
844             if (typeof data !== 'undefined' && data.success) {
845                 $('#pma_navigation_tree').html(data.message).children('div').show();
846                 if ($('#pma_navigation_tree').hasClass('synced')) {
847                     PMA_selectCurrentDb();
848                     PMA_showCurrentNavigation();
849                 }
850                 // Fire the callback, if any
851                 if (typeof callback === 'function') {
852                     callback.call();
853                 }
854                 navTreeStateUpdate();
855             } else {
856                 PMA_ajaxShowMessage(data.error);
857             }
858         });
859     }
862 function PMA_selectCurrentDb() {
863     if ($('#navi_db_select').length) {
864         $('#navi_db_select').val(PMA_commonParams.get('db'));
865         if ($('#navi_db_select').val() !== PMA_commonParams.get('db')) {
866             return false;
867         }
868         return true;
869     }
873  * Handles any requests to change the page in a branch of a tree
875  * This can be called from link click or select change event handlers
877  * @param object $this A jQuery object that points to the element that
878  * initiated the action of changing the page
880  * @return void
881  */
882 function PMA_navigationTreePagination($this) {
883     var $msgbox = PMA_ajaxShowMessage();
884     var isDbSelector = $this.closest('div.pageselector').is('.dbselector');
885     var url, params;
886     if ($this[0].tagName == 'A') {
887         url = $this.attr('href');
888         params = 'ajax_request=true';
889     } else { // tagName == 'SELECT'
890         url = 'navigation.php';
891         params = $this.closest("form").serialize() + '&ajax_request=true';
892     }
893     var searchClause = PMA_fastFilter.getSearchClause();
894     if (searchClause) {
895         params += '&searchClause=' + encodeURIComponent(searchClause);
896     }
897     if (isDbSelector) {
898         params += '&full=true';
899     } else {
900         var searchClause2 = PMA_fastFilter.getSearchClause2($this);
901         if (searchClause2) {
902             params += '&searchClause2=' + encodeURIComponent(searchClause2);
903         }
904     }
905     $.post(url, params, function (data) {
906         PMA_ajaxRemoveMessage($msgbox);
907         if (typeof data !== 'undefined' && data.success) {
908             if (isDbSelector) {
909                 var val = PMA_fastFilter.getSearchClause();
910                 $('#pma_navigation_tree')
911                     .html(data.message)
912                     .children('div')
913                     .show();
914                 if (val) {
915                     $('#pma_navigation_tree')
916                         .find('li.fast_filter input.searchClause')
917                         .val(val);
918                 }
919             } else {
920                 var $parent = $this.closest('div.list_container').parent();
921                 var val = PMA_fastFilter.getSearchClause2($this);
922                 $this.closest('div.list_container').html(
923                     $(data.message).children().show()
924                 );
925                 if (val) {
926                     $parent.find('li.fast_filter input.searchClause').val(val);
927                 }
928                 $parent.find('span.pos2_value:first').text(
929                     $parent.find('span.pos2_value:last').text()
930                 );
931                 $parent.find('span.pos3_value:first').text(
932                     $parent.find('span.pos3_value:last').text()
933                 );
934             }
935         } else {
936             PMA_ajaxShowMessage(data.error);
937         }
938         navTreeStateUpdate();
939     });
943  * @var ResizeHandler Custom object that manages the resizing of the navigation
945  * XXX: Must only be ever instanciated once
946  * XXX: Inside event handlers the 'this' object is accessed as 'event.data.resize_handler'
947  */
948 var ResizeHandler = function () {
949     /**
950      * @var int panel_width Used by the collapser to know where to go
951      *                      back to when uncollapsing the panel
952      */
953     this.panel_width = 0;
954     /**
955      * @var string left Used to provide support for RTL languages
956      */
957     this.left = $('html').attr('dir') == 'ltr' ? 'left' : 'right';
958     /**
959      * Adjusts the width of the navigation panel to the specified value
960      *
961      * @param int pos Navigation width in pixels
962      *
963      * @return void
964      */
965     this.setWidth = function (pos) {
966         var $resizer = $('#pma_navigation_resizer');
967         var resizer_width = $resizer.width();
968         var $collapser = $('#pma_navigation_collapser');
969         $('#pma_navigation').width(pos);
970         $('body').css('margin-' + this.left, pos + 'px');
971         $("#floating_menubar, #pma_console")
972             .css('margin-' + this.left, (pos + resizer_width) + 'px');
973         $resizer.css(this.left, pos + 'px');
974         if (pos === 0) {
975             $collapser
976                 .css(this.left, pos + resizer_width)
977                 .html(this.getSymbol(pos))
978                 .prop('title', PMA_messages.strShowPanel);
979         } else {
980             $collapser
981                 .css(this.left, pos)
982                 .html(this.getSymbol(pos))
983                 .prop('title', PMA_messages.strHidePanel);
984         }
985         setTimeout(function () {
986             $(window).trigger('resize');
987         }, 4);
988     };
989     /**
990      * Returns the horizontal position of the mouse,
991      * relative to the outer side of the navigation panel
992      *
993      * @param int pos Navigation width in pixels
994      *
995      * @return void
996      */
997     this.getPos = function (event) {
998         var pos = event.pageX;
999         var windowWidth = $(window).width();
1000         var windowScroll = $(window).scrollLeft();
1001         pos = pos - windowScroll;
1002         if (this.left != 'left') {
1003             pos = windowWidth - event.pageX;
1004         }
1005         if (pos < 0) {
1006             pos = 0;
1007         } else if (pos + 100 >= windowWidth) {
1008             pos = windowWidth - 100;
1009         } else {
1010             this.panel_width = 0;
1011         }
1012         return pos;
1013     };
1014     /**
1015      * Returns the HTML code for the arrow symbol used in the collapser
1016      *
1017      * @param int width The width of the panel
1018      *
1019      * @return string
1020      */
1021     this.getSymbol = function (width) {
1022         if (this.left == 'left') {
1023             if (width === 0) {
1024                 return '&rarr;';
1025             } else {
1026                 return '&larr;';
1027             }
1028         } else {
1029             if (width === 0) {
1030                 return '&larr;';
1031             } else {
1032                 return '&rarr;';
1033             }
1034         }
1035     };
1036     /**
1037      * Event handler for initiating a resize of the panel
1038      *
1039      * @param object e Event data (contains a reference to resizeHandler)
1040      *
1041      * @return void
1042      */
1043     this.mousedown = function (event) {
1044         event.preventDefault();
1045         $(document)
1046             .bind('mousemove', {'resize_handler': event.data.resize_handler},
1047                 $.throttle(event.data.resize_handler.mousemove, 4))
1048             .bind('mouseup', {'resize_handler': event.data.resize_handler},
1049                 event.data.resize_handler.mouseup);
1050         $('body').css('cursor', 'col-resize');
1051     };
1052     /**
1053      * Event handler for terminating a resize of the panel
1054      *
1055      * @param object e Event data (contains a reference to resizeHandler)
1056      *
1057      * @return void
1058      */
1059     this.mouseup = function (event) {
1060         $('body').css('cursor', '');
1061         $.cookie('pma_navi_width', event.data.resize_handler.getPos(event));
1062         $('#topmenu').menuResizer('resize');
1063         $(document)
1064             .unbind('mousemove')
1065             .unbind('mouseup');
1066     };
1067     /**
1068      * Event handler for updating the panel during a resize operation
1069      *
1070      * @param object e Event data (contains a reference to resizeHandler)
1071      *
1072      * @return void
1073      */
1074     this.mousemove = function (event) {
1075         event.preventDefault();
1076         var pos = event.data.resize_handler.getPos(event);
1077         event.data.resize_handler.setWidth(pos);
1078         if ($('.sticky_columns').length !== 0) {
1079             handleAllStickyColumns();
1080         }
1081     };
1082     /**
1083      * Event handler for collapsing the panel
1084      *
1085      * @param object e Event data (contains a reference to resizeHandler)
1086      *
1087      * @return void
1088      */
1089     this.collapse = function (event) {
1090         event.preventDefault();
1091         var panel_width = event.data.resize_handler.panel_width;
1092         var width = $('#pma_navigation').width();
1093         if (width === 0 && panel_width === 0) {
1094             panel_width = 240;
1095         }
1096         event.data.resize_handler.setWidth(panel_width);
1097         event.data.resize_handler.panel_width = width;
1098     };
1099     /**
1100      * Event handler for resizing the navigation tree height on window resize
1101      *
1102      * @return void
1103      */
1104     this.treeResize = function (event) {
1105         var $nav        = $("#pma_navigation"),
1106             $nav_tree   = $("#pma_navigation_tree"),
1107             $nav_header = $("#pma_navigation_header"),
1108             $nav_tree_content = $("#pma_navigation_tree_content");
1109         $nav_tree.height($nav.height() - $nav_header.height());
1110         if ($nav_tree_content.length > 0) {
1111             $nav_tree_content.height($nav_tree.height() - $nav_tree_content.position().top);
1112         } else {
1113             //TODO: in fast filter search response there is no #pma_navigation_tree_content, needs to be added in php
1114             $nav_tree.css({
1115                 'overflow-y': 'auto'
1116             });
1117         }
1118         // Set content bottom space beacuse of console
1119         $('body').css('margin-bottom', $('#pma_console').height() + 'px');
1120     };
1121     /* Initialisation section begins here */
1122     if ($.cookie('pma_navi_width')) {
1123         // If we have a cookie, set the width of the panel to its value
1124         var pos = Math.abs(parseInt($.cookie('pma_navi_width'), 10) || 0);
1125         this.setWidth(pos);
1126         $('#topmenu').menuResizer('resize');
1127     }
1128     // Register the events for the resizer and the collapser
1129     $(document).on('mousedown', '#pma_navigation_resizer', {'resize_handler': this}, this.mousedown);
1130     $(document).on('click', '#pma_navigation_collapser', {'resize_handler': this}, this.collapse);
1132     // Add the correct arrow symbol to the collapser
1133     $('#pma_navigation_collapser').html(this.getSymbol($('#pma_navigation').width()));
1134     // Fix navigation tree height
1135     $(window).on('resize', this.treeResize);
1136     // need to call this now and then, browser might decide
1137     // to show/hide horizontal scrollbars depending on page content width
1138     setInterval(this.treeResize, 2000);
1139     this.treeResize();
1140 }; // End of ResizeHandler
1143  * @var object PMA_fastFilter Handles the functionality that allows filtering
1144  *                            of the items in a branch of the navigation tree
1145  */
1146 var PMA_fastFilter = {
1147     /**
1148      * Construct for the asynchronous fast filter functionality
1149      *
1150      * @param object $this        A jQuery object pointing to the list container
1151      *                            which is the nearest parent of the fast filter
1152      * @param string searchClause The query string for the filter
1153      *
1154      * @return new PMA_fastFilter.filter object
1155      */
1156     filter: function ($this, searchClause) {
1157         /**
1158          * @var object $this A jQuery object pointing to the list container
1159          *                   which is the nearest parent of the fast filter
1160          */
1161         this.$this = $this;
1162         /**
1163          * @var bool searchClause The query string for the filter
1164          */
1165         this.searchClause = searchClause;
1166         /**
1167          * @var object $clone A clone of the original contents
1168          *                    of the navigation branch before
1169          *                    the fast filter was applied
1170          */
1171         this.$clone = $this.clone();
1172         /**
1173          * @var object xhr A reference to the ajax request that is currently running
1174          */
1175         this.xhr = null;
1176         /**
1177          * @var int timeout Used to delay the request for asynchronous search
1178          */
1179         this.timeout = null;
1181         var $filterInput = $this.find('li.fast_filter input.searchClause');
1182         if ($filterInput.length !== 0 &&
1183             $filterInput.val() !== '' &&
1184             $filterInput.val() != $filterInput[0].defaultValue
1185         ) {
1186             this.request();
1187         }
1188     },
1189     /**
1190      * Gets the query string from the database fast filter form
1191      *
1192      * @return string
1193      */
1194     getSearchClause: function () {
1195         var retval = '';
1196         var $input = $('#pma_navigation_tree')
1197             .find('li.fast_filter.db_fast_filter input.searchClause');
1198         if ($input.length && $input.val() != $input[0].defaultValue) {
1199             retval = $input.val();
1200         }
1201         return retval;
1202     },
1203     /**
1204      * Gets the query string from a second level item's fast filter form
1205      * The retrieval is done by trasversing the navigation tree backwards
1206      *
1207      * @return string
1208      */
1209     getSearchClause2: function ($this) {
1210         var $filterContainer = $this.closest('div.list_container');
1211         var $filterInput = $([]);
1212         if ($filterContainer
1213             .find('li.fast_filter:not(.db_fast_filter) input.searchClause')
1214             .length !== 0) {
1215             $filterInput = $filterContainer
1216                 .find('li.fast_filter:not(.db_fast_filter) input.searchClause');
1217         }
1218         var searchClause2 = '';
1219         if ($filterInput.length !== 0 &&
1220             $filterInput.first().val() != $filterInput[0].defaultValue
1221         ) {
1222             searchClause2 = $filterInput.val();
1223         }
1224         return searchClause2;
1225     },
1226     /**
1227      * @var hash events A list of functions that are bound to DOM events
1228      *                  at the top of this file
1229      */
1230     events: {
1231         focus: function (event) {
1232             var $obj = $(this).closest('div.list_container');
1233             if (! $obj.data('fastFilter')) {
1234                 $obj.data(
1235                     'fastFilter',
1236                     new PMA_fastFilter.filter($obj, $(this).val())
1237                 );
1238             }
1239             if ($(this).val() == this.defaultValue) {
1240                 $(this).val('');
1241             } else {
1242                 $(this).select();
1243             }
1244         },
1245         blur: function (event) {
1246             if ($(this).val() === '') {
1247                 $(this).val(this.defaultValue);
1248             }
1249             var $obj = $(this).closest('div.list_container');
1250             if ($(this).val() == this.defaultValue && $obj.data('fastFilter')) {
1251                 $obj.data('fastFilter').restore();
1252             }
1253         },
1254         mouseover: function (event) {
1255             var message = '';
1256             if ($(this).closest('li.fast_filter').is('.db_fast_filter')) {
1257                 message = PMA_messages.strHoverDbFastFilter;
1258             } else {
1259                 var node_type = $(this).siblings("input[name='pos2_name']").val();
1260                 var node_name = PMA_messages.strTables;
1261                 if (node_type == 'views') {
1262                     node_name = PMA_messages.strViews;
1263                 } else if (node_type == 'procedures') {
1264                     node_name = PMA_messages.strProcedures;
1265                 } else if (node_type == 'functions') {
1266                     node_name = PMA_messages.strFunctions;
1267                 } else if (node_type == 'events') {
1268                     node_name = PMA_messages.strEvents;
1269                 }
1270                 message = PMA_sprintf(PMA_messages.strHoverFastFilter, node_name);
1271             }
1272             PMA_tooltip($(this), 'input', message);
1273         },
1274         keyup: function (event) {
1275             var $obj = $(this).closest('div.list_container');
1276             var str = '';
1277             if ($(this).val() != this.defaultValue && $(this).val() !== '') {
1278                 $obj.find('div.pageselector').hide();
1279                 str = $(this).val();
1280             }
1282             /**
1283              * FIXME at the server level a value match is done while on
1284              * the client side it is a regex match. These two should be aligned
1285              */
1287             // regex used for filtering.
1288             var regex;
1289             try {
1290                 regex = new RegExp(str, 'i');
1291             } catch (err) {
1292                 return;
1293             }
1295             // this is the div that houses the items to be filtered by this filter.
1296             var outerContainer;
1297             if ($(this).closest('li.fast_filter').is('.db_fast_filter')) {
1298                 outerContainer = $('#pma_navigation_tree_content');
1299             } else {
1300                 outerContainer = $obj;
1301             }
1303             // filters items that are directly under the div as well as grouped in
1304             // groups. Does not filter child items (i.e. a database search does
1305             // not filter tables)
1306             var item_filter = function($curr) {
1307                 $curr.children('ul').children('li.navGroup').each(function() {
1308                     $(this).children('div.list_container').each(function() {
1309                         item_filter($(this)); // recursive
1310                     });
1311                 });
1312                 $curr.children('ul').children('li').children('a').not('.container').each(function() {
1313                     if (regex.test($(this).text())) {
1314                         $(this).parent().show().removeClass('hidden');
1315                     } else {
1316                         $(this).parent().hide().addClass('hidden');
1317                     }
1318                 });
1319             };
1320             item_filter(outerContainer);
1322             // hides containers that does not have any visible children
1323             var container_filter = function ($curr) {
1324                 $curr.children('ul').children('li.navGroup').each(function() {
1325                     var $group = $(this);
1326                     $group.children('div.list_container').each(function() {
1327                         container_filter($(this)); // recursive
1328                     });
1329                     $group.show().removeClass('hidden');
1330                     if ($group.children('div.list_container').children('ul')
1331                             .children('li').not('.hidden').length === 0) {
1332                         $group.hide().addClass('hidden');
1333                     }
1334                 });
1335             };
1336             container_filter(outerContainer);
1338             if ($(this).val() != this.defaultValue && $(this).val() !== '') {
1339                 if (! $obj.data('fastFilter')) {
1340                     $obj.data(
1341                         'fastFilter',
1342                         new PMA_fastFilter.filter($obj, $(this).val())
1343                     );
1344                 } else {
1345                     if (event.keyCode == 13) {
1346                         $obj.data('fastFilter').update($(this).val());
1347                     }
1348                 }
1349             } else if ($obj.data('fastFilter')) {
1350                 $obj.data('fastFilter').restore(true);
1351             }
1352         },
1353         clear: function (event) {
1354             event.stopPropagation();
1355             // Clear the input and apply the fast filter with empty input
1356             var filter = $(this).closest('div.list_container').data('fastFilter');
1357             if (filter) {
1358                 filter.restore();
1359             }
1360             var value = $(this).prev()[0].defaultValue;
1361             $(this).prev().val(value).trigger('keyup');
1362         }
1363     }
1366  * Handles a change in the search clause
1368  * @param string searchClause The query string for the filter
1370  * @return void
1371  */
1372 PMA_fastFilter.filter.prototype.update = function (searchClause) {
1373     if (this.searchClause != searchClause) {
1374         this.searchClause = searchClause;
1375         this.request();
1376     }
1379  * After a delay of 250mS, initiates a request to retrieve search results
1380  * Multiple calls to this function will always abort the previous request
1382  * @return void
1383  */
1384 PMA_fastFilter.filter.prototype.request = function () {
1385     var self = this;
1386     if (self.$this.find('li.fast_filter').find('img.throbber').length === 0) {
1387         self.$this.find('li.fast_filter').append(
1388             $('<div class="throbber"></div>').append(
1389                 $('#pma_navigation_content')
1390                     .find('img.throbber')
1391                     .clone()
1392                     .css({visibility: 'visible', display: 'block'})
1393             )
1394         );
1395     }
1396     if (self.xhr) {
1397         self.xhr.abort();
1398     }
1399     var url = $('#pma_navigation').find('a.navigation_url').attr('href');
1400     var params = self.$this.find('> ul > li > form.fast_filter').first().serialize();
1401     if (self.$this.find('> ul > li > form.fast_filter:first input[name=searchClause]').length === 0) {
1402         var $input = $('#pma_navigation_tree').find('li.fast_filter.db_fast_filter input.searchClause');
1403         if ($input.length && $input.val() != $input[0].defaultValue) {
1404             params += '&searchClause=' + encodeURIComponent($input.val());
1405         }
1406     }
1407     self.xhr = $.ajax({
1408         url: url,
1409         type: 'post',
1410         dataType: 'json',
1411         data: params,
1412         complete: function (jqXHR, status) {
1413             if (status != 'abort') {
1414                 var data = $.parseJSON(jqXHR.responseText);
1415                 self.$this.find('li.fast_filter').find('div.throbber').remove();
1416                 if (data && data.results) {
1417                     self.swap.apply(self, [data.message]);
1418                 }
1419             }
1420         }
1421     });
1424  * Replaces the contents of the navigation branch with the search results
1426  * @param string list The search results
1428  * @return void
1429  */
1430 PMA_fastFilter.filter.prototype.swap = function (list) {
1431     this.$this
1432         .html($(list).html())
1433         .children()
1434         .show()
1435         .end()
1436         .find('li.fast_filter input.searchClause')
1437         .val(this.searchClause);
1438     this.$this.data('fastFilter', this);
1441  * Restores the navigation to the original state after the fast filter is cleared
1443  * @param bool focus Whether to also focus the input box of the fast filter
1445  * @return void
1446  */
1447 PMA_fastFilter.filter.prototype.restore = function (focus) {
1448     if(this.$this.children('ul').first().hasClass('search_results')) {
1449         this.$this.html(this.$clone.html()).children().show();
1450         this.$this.data('fastFilter', this);
1451         if (focus) {
1452             this.$this.find('li.fast_filter input.searchClause').focus();
1453         }
1454     }
1455     this.searchClause = '';
1456     this.$this.find('div.pageselector').show();
1457     this.$this.find('div.throbber').remove();
1461  * Show full name when cursor hover and name not shown completely
1463  * @param object $containerELem Container element
1465  * @return void
1466  */
1467 function PMA_showFullName($containerELem) {
1469     $containerELem.find('.hover_show_full').mouseenter(function() {
1470         /** mouseenter */
1471         var $this = $(this);
1472         var thisOffset = $this.offset();
1473         if($this.text() === '') {
1474             return;
1475         }
1476         var $parent = $this.parent();
1477         if(  ($parent.offset().left + $parent.outerWidth())
1478            < (thisOffset.left + $this.outerWidth()))
1479         {
1480             var $fullNameLayer = $('#full_name_layer');
1481             if($fullNameLayer.length === 0)
1482             {
1483                 $('body').append('<div id="full_name_layer" class="hide"></div>');
1484                 $('#full_name_layer').mouseleave(function() {
1485                     /** mouseleave */
1486                     $(this).addClass('hide')
1487                            .removeClass('hovering');
1488                 }).mouseenter(function() {
1489                     /** mouseenter */
1490                     $(this).addClass('hovering');
1491                 });
1492                 $fullNameLayer = $('#full_name_layer');
1493             }
1494             $fullNameLayer.removeClass('hide');
1495             $fullNameLayer.css({left: thisOffset.left, top: thisOffset.top});
1496             $fullNameLayer.html($this.clone());
1497             setTimeout(function() {
1498                 if(! $fullNameLayer.hasClass('hovering'))
1499                 {
1500                     $fullNameLayer.trigger('mouseleave');
1501                 }
1502             }, 200);
1503         }
1504     });