Comments and formatting
[phpmyadmin.git] / js / makegrid.js
blob00d839f9ce8fa89a835c713c6e3e4ab2c130f4df
1 (function ($) {
2     $.grid = function(t) {
3         // prepare the grid
4         var g = {
5             // constant
6             minColWidth: 15,
7             
8             // variables, assigned with default value, changed later
9             actionSpan: 5,
10             colOrder: new Array(),      // array of column order
11             colVisib: new Array(),      // array of column visibility
12             tableCreateTime: null,      // table creation time, only available in "Browse tab"
13             qtip: null,                 // qtip API
14             reorderHint: '',            // string, hint for column reordering
15             sortHint: '',               // string, hint for column sorting
16             markHint: '',               // string, hint for column marking
17             colVisibHint: '',           // string, hint for column visibility drop-down
18             showReorderHint: false,
19             showSortHint: false,
20             showMarkHint: false,
21             showColVisibHint: false,
22             showAllColText: '',         // string, text for "show all" button under column visibility list
23             visibleHeadersCount: 0,     // number of visible data headers
24             
25             // functions
26             dragStartRsz: function(e, obj) {    // start column resize
27                 var n = $(this.cRsz).find('div').index(obj);
28                 this.colRsz = {
29                     x0: e.pageX,
30                     n: n,
31                     obj: obj,
32                     objLeft: $(obj).position().left,
33                     objWidth: $(this.t).find('th.draggable:visible:eq(' + n + ') span').outerWidth()
34                 };
35                 $('body').css('cursor', 'col-resize');
36                 $('body').noSelect();
37             },
38             
39             dragStartMove: function(e, obj) {   // start column move
40                 // prepare the cCpy and cPointer from the dragged column
41                 $(this.cCpy).text($(obj).text());
42                 var objPos = $(obj).position();
43                 $(this.cCpy).css({
44                     top: objPos.top + 20,
45                     left: objPos.left,
46                     height: $(obj).height(),
47                     width: $(obj).width()
48                 });
49                 $(this.cPointer).css({
50                     top: objPos.top
51                 });
52                 
53                 // get the column index, zero-based
54                 var n = this.getHeaderIdx(obj);
55                 
56                 this.colMov = {
57                     x0: e.pageX,
58                     y0: e.pageY,
59                     n: n,
60                     newn: n,
61                     obj: obj,
62                     objTop: objPos.top,
63                     objLeft: objPos.left
64                 };
65                 this.qtip.hide();
66                 $('body').css('cursor', 'move');
67                 $('body').noSelect();
68             },
69             
70             dragMove: function(e) {
71                 if (this.colRsz) {
72                     var dx = e.pageX - this.colRsz.x0;
73                     if (this.colRsz.objWidth + dx > this.minColWidth) {
74                         $(this.colRsz.obj).css('left', this.colRsz.objLeft + dx + 'px');
75                     }
76                 } else if (this.colMov) {
77                     // dragged column animation
78                     var dx = e.pageX - this.colMov.x0;
79                     $(this.cCpy)
80                         .css('left', this.colMov.objLeft + dx)
81                         .show();
82                     
83                     // pointer animation
84                     var hoveredCol = this.getHoveredCol(e);
85                     if (hoveredCol) {
86                         var newn = this.getHeaderIdx(hoveredCol);
87                         this.colMov.newn = newn;
88                         if (newn != this.colMov.n) {
89                             // show the column pointer in the right place
90                             var colPos = $(hoveredCol).position();
91                             var newleft = newn < this.colMov.n ?
92                                           colPos.left :
93                                           colPos.left + $(hoveredCol).outerWidth();
94                             $(this.cPointer)
95                                 .css({
96                                     left: newleft,
97                                     visibility: 'visible'
98                                 });
99                         } else {
100                             // no movement to other column, hide the column pointer
101                             $(this.cPointer).css('visibility', 'hidden');
102                         }
103                     }
104                 }
105             },
106             
107             dragEnd: function(e) {
108                 if (this.colRsz) {
109                     var dx = e.pageX - this.colRsz.x0;
110                     var nw = this.colRsz.objWidth + dx;
111                     if (nw < this.minColWidth) {
112                         nw = this.minColWidth;
113                     }
114                     var n = this.colRsz.n;
115                     // do the resizing
116                     this.resize(n, nw);
117                     
118                     $('body').css('cursor', 'default');
119                     this.reposRsz();
120                     this.reposDrop();
121                     this.colRsz = false;
122                 } else if (this.colMov) {
123                     // shift columns
124                     if (this.colMov.newn != this.colMov.n) {
125                         this.shiftCol(this.colMov.n, this.colMov.newn);
126                         // assign new position
127                         var objPos = $(this.colMov.obj).position();
128                         this.colMov.objTop = objPos.top;
129                         this.colMov.objLeft = objPos.left;
130                         this.colMov.n = this.colMov.newn;
131                         // send request to server to remember the column order
132                         if (this.tableCreateTime) {
133                             this.sendColPrefs();
134                         }
135                         this.refreshRestoreButton();
136                     }
137                     
138                     // animate new column position
139                     $(this.cCpy).stop(true, true)
140                         .animate({
141                             top: g.colMov.objTop,
142                             left: g.colMov.objLeft
143                         }, 'fast')
144                         .fadeOut();
145                     $(this.cPointer).css('visibility', 'hidden');
147                     this.colMov = false;
148                 }
149                 $('body').css('cursor', 'default');
150                 $('body').noSelect(false);
151             },
152             
153             /**
154              * Resize column n to new width "nw"
155              */
156             resize: function(n, nw) {
157                 $(this.t).find('tr').each(function() {
158                     $(this).find('th.draggable:visible:eq(' + n + ') span,' +
159                                  'td:visible:eq(' + (g.actionSpan + n) + ') span')
160                            .css('width', nw);
161                 });
162             },
163             
164             /**
165              * Reposition column resize bars.
166              */
167             reposRsz: function() {
168                 $(this.cRsz).find('div').hide();
169                 var $firstRowCols = $(this.t).find('tr:first th.draggable:visible');
170                 for (var n = 0; n < $firstRowCols.length; n++) {
171                     $this = $($firstRowCols[n]);
172                     $cb = $(g.cRsz).find('div:eq(' + n + ')');   // column border
173                     $cb.css('left', $this.position().left + $this.outerWidth(true))
174                        .show();
175                 }
176                 $(this.cRsz).css('height', $(this.t).height());
177             },
178             
179             /**
180              * Shift column from index oldn to newn.
181              */
182             shiftCol: function(oldn, newn) {
183                 $(this.t).find('tr').each(function() {
184                     if (newn < oldn) {
185                         $(this).find('th.draggable:eq(' + newn + '),' +
186                                      'td:eq(' + (g.actionSpan + newn) + ')')
187                                .before($(this).find('th.draggable:eq(' + oldn + '),' +
188                                                     'td:eq(' + (g.actionSpan + oldn) + ')'));
189                     } else {
190                         $(this).find('th.draggable:eq(' + newn + '),' +
191                                      'td:eq(' + (g.actionSpan + newn) + ')')
192                                .after($(this).find('th.draggable:eq(' + oldn + '),' +
193                                                    'td:eq(' + (g.actionSpan + oldn) + ')'));
194                     }
195                 });
196                 // reposition the column resize bars
197                 this.reposRsz();
198                     
199                 // adjust the column visibility list
200                 if (newn < oldn) {
201                     $(g.cList).find('.lDiv div:eq(' + newn + ')')
202                               .before($(g.cList).find('.lDiv div:eq(' + oldn + ')'));
203                 } else {
204                     $(g.cList).find('.lDiv div:eq(' + newn + ')')
205                               .after($(g.cList).find('.lDiv div:eq(' + oldn + ')'));
206                 }
207                 // adjust the colOrder
208                 var tmp = this.colOrder[oldn];
209                 this.colOrder.splice(oldn, 1);
210                 this.colOrder.splice(newn, 0, tmp);
211                 // adjust the colVisib
212                 var tmp = this.colVisib[oldn];
213                 this.colVisib.splice(oldn, 1);
214                 this.colVisib.splice(newn, 0, tmp);
215             },
216             
217             /**
218              * Find currently hovered table column's header (excluding actions column).
219              * @return the hovered column's th object or undefined if no hovered column found.
220              */
221             getHoveredCol: function(e) {
222                 var hoveredCol;
223                 $headers = $(this.t).find('th.draggable:visible');
224                 $headers.each(function() {
225                     var left = $(this).offset().left;
226                     var right = left + $(this).outerWidth();
227                     if (left <= e.pageX && e.pageX <= right) {
228                         hoveredCol = this;
229                     }
230                 });
231                 return hoveredCol;
232             },
233             
234             /**
235              * Get a zero-based index from a <th class="draggable"> tag in a table.
236              */
237             getHeaderIdx: function(obj) {
238                 return $(obj).parents('tr').find('th.draggable').index(obj);
239             },
240             
241             /**
242              * Reposition the table back to normal order.
243              */
244             restoreColOrder: function() {
245                 // use insertion sort, since we already have shiftCol function
246                 for (var i = 1; i < this.colOrder.length; i++) {
247                     var x = this.colOrder[i];
248                     var j = i - 1;
249                     while (j >= 0 && x < this.colOrder[j]) {
250                         j--;
251                     }
252                     if (j != i - 1) {
253                         this.shiftCol(i, j + 1);
254                     }
255                 }
256                 if (this.tableCreateTime) {
257                     // send request to server to remember the column order
258                     this.sendColPrefs();
259                 }
260                 this.refreshRestoreButton();
261             },
262             
263             /**
264              * Send column preferences (column order and visibility) to the server.
265              */
266             sendColPrefs: function() {
267                 $.post('sql.php', {
268                     ajax_request: true,
269                     db: window.parent.db,
270                     table: window.parent.table,
271                     token: window.parent.token,
272                     server: window.parent.server,
273                     set_col_prefs: true,
274                     col_order: this.colOrder.toString(),
275                     col_visib: this.colVisib.toString(),
276                     table_create_time: this.tableCreateTime
277                 });
278             },
279             
280             /**
281              * Refresh restore button state.
282              * Make restore button disabled if the table is similar with initial state.
283              */
284             refreshRestoreButton: function() {
285                 // check if table state is as initial state
286                 var isInitial = true;
287                 for (var i = 0; i < this.colOrder.length; i++) {
288                     if (this.colOrder[i] != i) {
289                         isInitial = false;
290                         break;
291                     }
292                 }
293                 // check if only one visible column left
294                 var isOneColumn = this.visibleHeadersCount == 1;
295                 // enable or disable restore button
296                 if (isInitial || isOneColumn) {
297                     $('.restore_column').hide();
298                 } else {
299                     $('.restore_column').show();
300                 }
301             },
302             
303             /**
304              * Update current hint using the boolean values (showReorderHint, showSortHint, etc.).
305              * It will hide the hint if all the boolean values is false.
306              */
307             updateHint: function(e) {
308                 if (!this.colRsz && !this.colMov) {     // if not resizing or dragging
309                     var text = '';
310                     if (this.showReorderHint && this.reorderHint) {
311                         text += this.reorderHint;
312                     }
313                     if (this.showSortHint && this.sortHint) {
314                         text += text.length > 0 ? '<br />' : '';
315                         text += this.sortHint;
316                     }
317                     if (this.showMarkHint && this.markHint &&
318                         !this.showSortHint      // we do not show mark hint, when sort hint is shown
319                     ) {
320                         text += text.length > 0 ? '<br />' : '';
321                         text += this.markHint;
322                     }
323                     if (this.showColVisibHint && this.colVisibHint) {
324                         text += text.length > 0 ? '<br />' : '';
325                         text += this.colVisibHint;
326                     }
327                     
328                     // hide the hint if no text
329                     this.qtip.disable(!text && e.type == 'mouseenter');
330                     
331                     this.qtip.updateContent(text, false);
332                 } else {
333                     this.qtip.disable(true);
334                 }
335             },
336             
337             /**
338              * Toggle column's visibility.
339              * After calling this function and it returns true, afterToggleCol() must be called.
340              *
341              * @return boolean True if the column is toggled successfully.
342              */
343             toggleCol: function(n) {
344                 if (this.colVisib[n]) {
345                     // can hide if more than one column is visible
346                     if (this.visibleHeadersCount > 1) {
347                         $(this.t).find('tr').each(function() {
348                             $(this).find('th.draggable:eq(' + n + '),' +
349                                          'td:eq(' + (g.actionSpan + n) + ')')
350                                    .hide();
351                         });
352                         this.colVisib[n] = 0;
353                         $(this.cList).find('.lDiv div:eq(' + n + ') input').removeAttr('checked');
354                     } else {
355                         // cannot hide, force the checkbox to stay checked
356                         $(this.cList).find('.lDiv div:eq(' + n + ') input').attr('checked', 'checked');
357                         return false;
358                     }
359                 } else {    // column n is not visible
360                     $(this.t).find('tr').each(function() {
361                         $(this).find('th.draggable:eq(' + n + '),' +
362                                      'td:eq(' + (g.actionSpan + n) + ')')
363                                .show();
364                     });
365                     this.colVisib[n] = 1;
366                     $(this.cList).find('.lDiv div:eq(' + n + ') input').attr('checked', 'checked');
367                 }
368                 return true;
369             },
370             
371             /**
372              * This must be called after calling toggleCol() and the return value is true.
373              *
374              * This function is separated from toggleCol because, sometimes, we want to toggle
375              * some columns together at one time and do one adjustment after it, e.g. in showAllColumns().
376              */
377             afterToggleCol: function() {
378                 // some adjustments after hiding column
379                 this.reposRsz();
380                 this.reposDrop();
381                 this.sendColPrefs();
382                 
383                 // check visible first row headers count
384                 this.visibleHeadersCount = $(this.t).find('tr:first th.draggable:visible').length;
385                 this.refreshRestoreButton();
386             },
387             
388             /**
389              * Show columns' visibility list.
390              */
391             showColList: function(obj) {
392                 // only show when not resizing or reordering
393                 if (!this.colRsz && !this.colMov) {
394                     var pos = $(obj).position();
395                     // check if the list position is too right
396                     if (pos.left + $(this.cList).outerWidth(true) > $(document).width()) {
397                         pos.left = $(document).width() - $(this.cList).outerWidth(true);
398                     }
399                     $(this.cList).css({
400                             left: pos.left,
401                             top: pos.top + $(obj).outerHeight(true)
402                         })
403                         .show();
404                     $(obj).addClass('coldrop-hover');
405                 }
406             },
407             
408             /**
409              * Hide columns' visibility list.
410              */
411             hideColList: function() {
412                 $(this.cList).hide();
413                 $(g.cDrop).find('.coldrop-hover').removeClass('coldrop-hover');
414             },
415             
416             /**
417              * Reposition the column visibility drop-down arrow.
418              */
419             reposDrop: function() {
420                 $th = $(t).find('th:not(.draggable)');
421                 for (var i = 0; i < $th.length; i++) {
422                     var $cd = $(this.cDrop).find('div:eq(' + i + ')');   // column drop-down arrow
423                     var pos = $($th[i]).position();
424                     $cd.css({
425                             left: pos.left + $($th[i]).width() - $cd.width(),
426                             top: pos.top
427                         });
428                 }
429             },
430             
431             /**
432              * Show all hidden columns.
433              */
434             showAllColumns: function() {
435                 for (var i = 0; i < this.colVisib.length; i++) {
436                     if (!this.colVisib[i]) {
437                         this.toggleCol(i);
438                     }
439                 }
440                 this.afterToggleCol();
441             }
442         }
443         
444         // wrap all data cells, except actions cell, with span
445         $(t).find('th, td:not(:has(span))')
446             .wrapInner('<span />');
447         
448         g.gDiv = document.createElement('div');     // create global div
449         g.cRsz = document.createElement('div');     // column resizer
450         g.cCpy = document.createElement('div');     // column copy, to store copy of dragged column header
451         g.cPointer = document.createElement('div'); // column pointer, used when reordering column
452         g.cDrop = document.createElement('div');    // column drop-down arrows
453         g.cList = document.createElement('div');    // column visibility list
454         
455         // adjust g.cCpy
456         g.cCpy.className = 'cCpy';
457         $(g.cCpy).hide();
458         
459         // adjust g.cPoint
460         g.cPointer.className = 'cPointer';
461         $(g.cPointer).css('visibility', 'hidden');
462         
463         // adjust g.cDrop
464         g.cDrop.className = 'cDrop';
465         
466         // adjust g.cList
467         g.cList.className = 'cList';
468         $(g.cList).hide();
469         
470         // chain table and grid together
471         t.grid = g;
472         g.t = t;
473         
474         // get first row data columns
475         var $firstRowCols = $(t).find('tr:first th.draggable');
476         
477         // initialize g.visibleHeadersCount
478         g.visibleHeadersCount = $firstRowCols.filter(':visible').length;
479         
480         // assign first column (actions) span
481         if (! $(t).find('tr:first th:first').hasClass('draggable')) {  // action header exist
482             g.actionSpan = $(t).find('tr:first th:first').prop('colspan');
483         } else {
484             g.actionSpan = 0;
485         }
486         
487         // assign table create time
488         // #table_create_time will only available if we are in "Browse" tab
489         g.tableCreateTime = $('#table_create_time').val();
490         
491         // assign column reorder & column sort hint
492         g.reorderHint = $('#col_order_hint').val();
493         g.sortHint = $('#sort_hint').val();
494         g.markHint = $('#col_mark_hint').val();
495         g.colVisibHint = $('#col_visib_hint').val();
496         g.showAllColText = $('#show_all_col_text').val();
497         
498         // initialize column order
499         $col_order = $('#col_order');
500         if ($col_order.length > 0) {
501             g.colOrder = $col_order.val().split(',');
502             for (var i = 0; i < g.colOrder.length; i++) {
503                 g.colOrder[i] = parseInt(g.colOrder[i]);
504             }
505         } else {
506             g.colOrder = new Array();
507             for (var i = 0; i < $firstRowCols.length; i++) {
508                 g.colOrder.push(i);
509             }
510         }
511         
512         // initialize column visibility
513         $col_visib = $('#col_visib');
514         if ($col_visib.length > 0) {
515             g.colVisib = $col_visib.val().split(',');
516             for (var i = 0; i < g.colVisib.length; i++) {
517                 g.colVisib[i] = parseInt(g.colVisib[i]);
518             }
519         } else {
520             g.colVisib = new Array();
521             for (var i = 0; i < $firstRowCols.length; i++) {
522                 g.colVisib.push(1);
523             }
524         }
525         
526         if ($firstRowCols.length > 1) {
527             // create column drop-down arrow(s)
528             $(t).find('th:not(.draggable)').each(function() {
529                 var cd = document.createElement('div'); // column drop-down arrow
530                 var pos = $(this).position();
531                 $(cd).addClass('coldrop')
532                     .css({
533                         left: pos.left + $(this).width() - $(cd).width(),
534                         top: pos.top
535                     })
536                     .click(function() {
537                         if (g.cList.style.display == 'none') {
538                             g.showColList(this);
539                         } else {
540                             g.hideColList();
541                         }
542                     });
543                 $(g.cDrop).append(cd);
544             });
545             
546             // add column visibility control
547             g.cList.innerHTML = '<div class="lDiv"></div>';
548             var $listDiv = $(g.cList).find('div');
549             for (var i = 0; i < $firstRowCols.length; i++) {
550                 var currHeader = $firstRowCols[i];
551                 var listElmt = document.createElement('div');
552                 $(listElmt).text($(currHeader).text())
553                     .prepend('<input type="checkbox" ' + (g.colVisib[i] ? 'checked="checked" ' : '') + '/>');
554                 $listDiv.append(listElmt);
555                 // add event on click
556                 $(listElmt).click(function() {
557                     if ( g.toggleCol($(this).index()) ) {
558                         g.afterToggleCol();
559                     }
560                 });
561             }
562             // add "show all column" button
563             var showAll = document.createElement('div');
564             $(showAll).addClass('showAllColBtn')
565                 .text(g.showAllColText);
566             $(g.cList).append(showAll);
567             $(showAll).click(function() {
568                 g.showAllColumns();
569             });
570             // prepend "show all column" button at top if the list is too long
571             if ($firstRowCols.length > 10) {
572                 var clone = showAll.cloneNode(true);
573                 $(g.cList).prepend(clone);
574                 $(clone).click(function() {
575                     g.showAllColumns();
576                 });
577             }
578         }
579         
580         // create column borders
581         $firstRowCols.each(function() {
582             $this = $(this);
583             var cb = document.createElement('div'); // column border
584             $(cb).addClass('colborder')
585                 .mousedown(function(e) {
586                     g.dragStartRsz(e, this);
587                 });
588             $(g.cRsz).append(cb);
589         });
590         g.reposRsz();
591         
592         // bind event to update currently hovered qtip API
593         $(t).find('th').mouseenter(function(e) {
594             g.qtip = $(this).qtip('api');
595         });
596         
597         // create qtip for each <th> with draggable class
598         PMA_createqTip($(t).find('th.draggable'));
599         
600         // register events
601         if (g.reorderHint) {    // make sure columns is reorderable
602             $(t).find('th.draggable')
603                 .mousedown(function(e) {
604                     if (g.visibleHeadersCount > 1) {
605                         g.dragStartMove(e, this);
606                     }
607                 })
608                 .mouseenter(function(e) {
609                     if (g.visibleHeadersCount > 1) {
610                         g.showReorderHint = true;
611                         $(this).css('cursor', 'move');
612                     } else {
613                         $(this).css('cursor', 'inherit');
614                     }
615                     g.updateHint(e);
616                 })
617                 .mouseleave(function(e) {
618                     g.showReorderHint = false;
619                     g.updateHint(e);
620                 });
621         }
622         if ($firstRowCols.length > 1) {
623             var $colVisibTh = $(t).find('th:not(.draggable)');
624             
625             PMA_createqTip($colVisibTh);
626             $colVisibTh.mouseenter(function(e) {
627                     g.showColVisibHint = true;
628                     g.updateHint(e);
629                 })
630                 .mouseleave(function(e) {
631                     g.showColVisibHint = false;
632                     g.updateHint(e);
633                 });
634         }
635         $(t).find('th.draggable a')
636             .attr('title', '')          // hide default tooltip for sorting
637             .mouseenter(function(e) {
638                 g.showSortHint = true;
639                 g.updateHint(e);
640             })
641             .mouseleave(function(e) {
642                 g.showSortHint = false;
643                 g.updateHint(e);
644             });
645         $(t).find('th.marker')
646             .mouseenter(function(e) {
647                 g.showMarkHint = true;
648                 g.updateHint(e);
649             })
650             .mouseleave(function(e) {
651                 g.showMarkHint = false;
652                 g.updateHint(e);
653             });
654         $(document).mousemove(function(e) {
655             g.dragMove(e);
656         });
657         $(document).mouseup(function(e) {
658             g.dragEnd(e);
659         });
660         $('.restore_column').click(function() {
661             g.restoreColOrder();
662         });
663         $(t).find('td, th.draggable').mouseenter(function() {
664             g.hideColList();
665         });
666         
667         // add table class
668         $(t).addClass('pma_table');
669         
670         // link all divs
671         $(t).before(g.gDiv);
672         $(g.gDiv).append(t);
673         $(g.gDiv).prepend(g.cRsz);
674         $(g.gDiv).append(g.cPointer);
675         $(g.gDiv).append(g.cDrop);
676         $(g.gDiv).append(g.cList);
677         $(g.gDiv).append(g.cCpy);
679         // some adjustment
680         g.refreshRestoreButton();
681         g.cRsz.className = 'cRsz';
682         $(t).removeClass('data');
683         $(g.gDiv).addClass('data');
684         $(g.cRsz).css('height', $(t).height());
685         $(t).find('th a').bind('dragstart', function() {
686             return false;
687         });
688     };
689     
690     // document ready checking
691     var docready = false;
692     $(document).ready(function() {
693         docready = true;
694     });
695     
696     // Additional jQuery functions
697     /**
698      * Make resizable, reorderable grid.
699      */
700     $.fn.makegrid = function() {
701         return this.each(function() {
702             if (!docready) {
703                 var t = this;
704                 $(document).ready(function() {
705                     $.grid(t);
706                     t.grid.reposDrop();
707                 });
708             } else {
709                 $.grid(this);
710                 this.grid.reposDrop();
711             }
712         });
713     };
714     /**
715      * Refresh grid. This must be called after changing the grid's content.
716      */
717     $.fn.refreshgrid = function() {
718         return this.each(function() {
719             if (!docready) {
720                 var t = this;
721                 $(document).ready(function() {
722                     if (t.grid) {
723                         t.grid.reposRsz();
724                         t.grid.reposDrop();
725                     }
726                 });
727             } else {
728                 if (this.grid) {
729                     this.grid.reposRsz();
730                     this.grid.reposDrop();
731                 }
732             }
733         });
734     }
735     
736 })(jQuery);