Fixed the NULL not applied after clearing nullable field
[phpmyadmin.git] / js / src / makegrid.js
blob1d012c9fea96c4df453f350ea0b88fcdc97188f8
1 /* global firstDayOfCalendar */ // templates/javascript/variables.twig
3 /**
4  * Create advanced table (resize, reorder, and show/hide columns; and also grid editing).
5  * This function is designed mainly for table DOM generated from browsing a table in the database.
6  * For using this function in other table DOM, you may need to:
7  * - add "draggable" class in the table header <th>, in order to make it resizable, sortable or hidable
8  * - have at least one non-"draggable" header in the table DOM for placing column visibility drop-down arrow
9  * - pass the value "false" for the parameter "enableGridEdit"
10  * - adjust other parameter value, to select which features that will be enabled
11  *
12  * @param t the table DOM element
13  * @param enableResize Optional, if false, column resizing feature will be disabled
14  * @param enableReorder Optional, if false, column reordering feature will be disabled
15  * @param enableVisib Optional, if false, show/hide column feature will be disabled
16  * @param enableGridEdit Optional, if false, grid editing feature will be disabled
17  */
18 // eslint-disable-next-line no-unused-vars
19 var makeGrid = function (t, enableResize, enableReorder, enableVisib, enableGridEdit) {
20     var isResizeEnabled = enableResize === undefined ? true : enableResize;
21     var isReorderEnabled = enableReorder === undefined ? true : enableReorder;
22     var isVisibEnabled = enableVisib === undefined ? true : enableVisib;
23     var isGridEditEnabled = enableGridEdit === undefined ? true : enableGridEdit;
25     var g = {
26         /** *********
27          * Constant
28          ***********/
29         minColWidth: 15,
32         /** *********
33          * Variables, assigned with default value, changed later
34          ***********/
35         actionSpan: 5,              // number of colspan in Actions header in a table
36         tableCreateTime: null,      // table creation time, used for saving column order and visibility to server, only available in "Browse tab"
38         // Column reordering variables
39         colOrder: [],      // array of column order
41         // Column visibility variables
42         colVisib: [],      // array of column visibility
43         showAllColText: '',         // string, text for "show all" button under column visibility list
44         visibleHeadersCount: 0,     // number of visible data headers
46         // Table hint variables
47         reorderHint: '',            // string, hint for column reordering
48         sortHint: '',               // string, hint for column sorting
49         markHint: '',               // string, hint for column marking
50         copyHint: '',               // string, hint for copy column name
51         showReorderHint: false,
52         showSortHint: false,
53         showMarkHint: false,
55         // Grid editing
56         isCellEditActive: false,    // true if current focus is in edit cell
57         isEditCellTextEditable: false,  // true if current edit cell is editable in the text input box (not textarea)
58         currentEditCell: null,      // reference to <td> that currently being edited
59         cellEditHint: '',           // hint shown when doing grid edit
60         gotoLinkText: '',           // "Go to link" text
61         wasEditedCellNull: false,   // true if last value of the edited cell was NULL
62         maxTruncatedLen: 0,         // number of characters that can be displayed in a cell
63         saveCellsAtOnce: false,     // $cfg[saveCellsAtOnce]
64         isCellEdited: false,        // true if at least one cell has been edited
65         saveCellWarning: '',        // string, warning text when user want to leave a page with unsaved edited data
66         lastXHR : null,             // last XHR object used in AJAX request
67         isSaving: false,            // true when currently saving edited data, used to handle double posting caused by pressing ENTER in grid edit text box in Chrome browser
68         alertNonUnique: '',         // string, alert shown when saving edited nonunique table
70         // Common hidden inputs
71         token: null,
72         server: null,
73         db: null,
74         table: null,
77         /** **********
78          * Functions
79          ************/
81         /**
82          * Start to resize column. Called when clicking on column separator.
83          *
84          * @param e event
85          * @param obj dragged div object
86          */
87         dragStartRsz: function (e, obj) {
88             var n = $(g.cRsz).find('div').index(obj);    // get the index of separator (i.e., column index)
89             $(obj).addClass('colborder_active');
90             g.colRsz = {
91                 x0: e.pageX,
92                 n: n,
93                 obj: obj,
94                 objLeft: $(obj).position().left,
95                 objWidth: $(g.t).find('th.draggable:visible').eq(n).find('span').outerWidth()
96             };
97             // eslint-disable-next-line compat/compat
98             $(document.body).css('cursor', 'col-resize').noSelect();
99             if (g.isCellEditActive) {
100                 g.hideEditCell();
101             }
102         },
104         /**
105          * Start to reorder column. Called when clicking on table header.
106          *
107          * @param e event
108          * @param obj table header object
109          */
110         dragStartReorder: function (e, obj) {
111             // prepare the cCpy (column copy) and cPointer (column pointer) from the dragged column
112             $(g.cCpy).text($(obj).text());
113             var objPos = $(obj).position();
114             $(g.cCpy).css({
115                 top: objPos.top + 20,
116                 left: objPos.left,
117                 height: $(obj).height(),
118                 width: $(obj).width()
119             });
120             $(g.cPointer).css({
121                 top: objPos.top
122             });
124             // get the column index, zero-based
125             var n = g.getHeaderIdx(obj);
127             g.colReorder = {
128                 x0: e.pageX,
129                 y0: e.pageY,
130                 n: n,
131                 newn: n,
132                 obj: obj,
133                 objTop: objPos.top,
134                 objLeft: objPos.left
135             };
137             // eslint-disable-next-line compat/compat
138             $(document.body).css('cursor', 'move').noSelect();
139             if (g.isCellEditActive) {
140                 g.hideEditCell();
141             }
142         },
144         /**
145          * Handle mousemove event when dragging.
146          *
147          * @param e event
148          */
149         dragMove: function (e) {
150             var dx;
151             if (g.colRsz) {
152                 dx = e.pageX - g.colRsz.x0;
153                 if (g.colRsz.objWidth + dx > g.minColWidth) {
154                     $(g.colRsz.obj).css('left', g.colRsz.objLeft + dx + 'px');
155                 }
156             } else if (g.colReorder) {
157                 // dragged column animation
158                 dx = e.pageX - g.colReorder.x0;
159                 $(g.cCpy)
160                     .css('left', g.colReorder.objLeft + dx)
161                     .show();
163                 // pointer animation
164                 var hoveredCol = g.getHoveredCol(e);
165                 if (hoveredCol) {
166                     var newn = g.getHeaderIdx(hoveredCol);
167                     g.colReorder.newn = newn;
168                     if (newn !== g.colReorder.n) {
169                         // show the column pointer in the right place
170                         var colPos = $(hoveredCol).position();
171                         var newleft = newn < g.colReorder.n ?
172                             colPos.left :
173                             colPos.left + $(hoveredCol).outerWidth();
174                         $(g.cPointer)
175                             .css({
176                                 left: newleft,
177                                 visibility: 'visible'
178                             });
179                     } else {
180                         // no movement to other column, hide the column pointer
181                         $(g.cPointer).css('visibility', 'hidden');
182                     }
183                 }
184             }
185         },
187         /**
188          * Stop the dragging action.
189          *
190          * @param e event
191          */
192         dragEnd: function (e) {
193             if (g.colRsz) {
194                 var dx = e.pageX - g.colRsz.x0;
195                 var nw = g.colRsz.objWidth + dx;
196                 if (nw < g.minColWidth) {
197                     nw = g.minColWidth;
198                 }
199                 var n = g.colRsz.n;
200                 // do the resizing
201                 g.resize(n, nw);
203                 g.reposRsz();
204                 g.reposDrop();
205                 g.colRsz = false;
206                 $(g.cRsz).find('div').removeClass('colborder_active');
207             } else if (g.colReorder) {
208                 // shift columns
209                 if (g.colReorder.newn !== g.colReorder.n) {
210                     g.shiftCol(g.colReorder.n, g.colReorder.newn);
211                     // assign new position
212                     var objPos = $(g.colReorder.obj).position();
213                     g.colReorder.objTop = objPos.top;
214                     g.colReorder.objLeft = objPos.left;
215                     g.colReorder.n = g.colReorder.newn;
216                     // send request to server to remember the column order
217                     if (g.tableCreateTime) {
218                         g.sendColPrefs();
219                     }
220                     g.refreshRestoreButton();
221                 }
223                 // animate new column position
224                 $(g.cCpy).stop(true, true)
225                     .animate({
226                         top: g.colReorder.objTop,
227                         left: g.colReorder.objLeft
228                     }, 'fast')
229                     .fadeOut();
230                 $(g.cPointer).css('visibility', 'hidden');
232                 g.colReorder = false;
233             }
234             // eslint-disable-next-line compat/compat
235             $(document.body).css('cursor', 'inherit').noSelect(false);
236         },
238         /**
239          * Resize column n to new width "nw"
240          *
241          * @param n zero-based column index
242          * @param nw new width of the column in pixel
243          */
244         resize: function (n, nw) {
245             $(g.t).find('tr').each(function () {
246                 $(this).find('th.draggable:visible').eq(n).find('span')
247                     .add($(this).find('td:visible').eq(g.actionSpan + n).find('span'))
248                     .css('width', nw);
249             });
250         },
252         /**
253          * Reposition column resize bars.
254          */
255         reposRsz: function () {
256             $(g.cRsz).find('div').hide();
257             var $firstRowCols = $(g.t).find('tr').first().find('th.draggable:visible');
258             var $resizeHandles = $(g.cRsz).find('div').removeClass('condition');
259             $(g.t).find('table.pma_table').find('thead th').first().removeClass('before-condition');
260             for (var n = 0, l = $firstRowCols.length; n < l; n++) {
261                 var $col = $($firstRowCols[n]);
262                 var colWidth;
263                 if (navigator.userAgent.toLowerCase().indexOf('safari') !== -1) {
264                     colWidth = $col.outerWidth();
265                 } else {
266                     colWidth = $col.outerWidth(true);
267                 }
268                 $($resizeHandles[n]).css('left', $col.position().left + colWidth)
269                     .show();
270                 if ($col.hasClass('condition')) {
271                     $($resizeHandles[n]).addClass('condition');
272                     if (n > 0) {
273                         $($resizeHandles[n - 1]).addClass('condition');
274                     }
275                 }
276             }
277             if ($($resizeHandles[0]).hasClass('condition')) {
278                 $(g.t).find('thead th').first().addClass('before-condition');
279             }
280             $(g.cRsz).css('height', $(g.t).height());
281         },
283         /**
284          * Shift column from index oldn to newn.
285          *
286          * @param oldn old zero-based column index
287          * @param newn new zero-based column index
288          */
289         shiftCol: function (oldn, newn) {
290             $(g.t).find('tr').each(function () {
291                 if (newn < oldn) {
292                     $(this).find('th.draggable').eq(newn)
293                         .add($(this).find('td').eq(g.actionSpan + newn))
294                         .before($(this).find('th.draggable').eq(oldn)
295                             .add($(this).find('td').eq(g.actionSpan + oldn)));
296                 } else {
297                     $(this).find('th.draggable').eq(newn)
298                         .add($(this).find('td').eq(g.actionSpan + newn))
299                         .after($(this).find('th.draggable').eq(oldn)
300                             .add($(this).find('td').eq(g.actionSpan + oldn)));
301                 }
302             });
303             // reposition the column resize bars
304             g.reposRsz();
306             // adjust the column visibility list
307             if (newn < oldn) {
308                 $(g.cList).find('.lDiv div').eq(newn)
309                     .before($(g.cList).find('.lDiv div').eq(oldn));
310             } else {
311                 $(g.cList).find('.lDiv div').eq(newn)
312                     .after($(g.cList).find('.lDiv div').eq(oldn));
313             }
314             // adjust the colOrder
315             var tmp = g.colOrder[oldn];
316             g.colOrder.splice(oldn, 1);
317             g.colOrder.splice(newn, 0, tmp);
318             // adjust the colVisib
319             if (g.colVisib.length > 0) {
320                 tmp = g.colVisib[oldn];
321                 g.colVisib.splice(oldn, 1);
322                 g.colVisib.splice(newn, 0, tmp);
323             }
324         },
326         /**
327          * Find currently hovered table column's header (excluding actions column).
328          *
329          * @param e event
330          * @return {object|undefined} the hovered column's th object or undefined if no hovered column found.
331          */
332         getHoveredCol: function (e) {
333             var hoveredCol;
334             var $headers = $(g.t).find('th.draggable:visible');
335             $headers.each(function () {
336                 var left = $(this).offset().left;
337                 var right = left + $(this).outerWidth();
338                 if (left <= e.pageX && e.pageX <= right) {
339                     hoveredCol = this;
340                 }
341             });
342             return hoveredCol;
343         },
345         /**
346          * Get a zero-based index from a <th class="draggable"> tag in a table.
347          *
348          * @param obj table header <th> object
349          * @return {number} zero-based index of the specified table header in the set of table headers (visible or not)
350          */
351         getHeaderIdx: function (obj) {
352             return $(obj).parents('tr').find('th.draggable').index(obj);
353         },
355         /**
356          * Reposition the columns back to normal order.
357          */
358         restoreColOrder: function () {
359             // use insertion sort, since we already have shiftCol function
360             for (var i = 1; i < g.colOrder.length; i++) {
361                 var x = g.colOrder[i];
362                 var j = i - 1;
363                 while (j >= 0 && x < g.colOrder[j]) {
364                     j--;
365                 }
366                 if (j !== i - 1) {
367                     g.shiftCol(i, j + 1);
368                 }
369             }
370             if (g.tableCreateTime) {
371                 // send request to server to remember the column order
372                 g.sendColPrefs();
373             }
374             g.refreshRestoreButton();
375         },
377         /**
378          * Send column preferences (column order and visibility) to the server.
379          */
380         sendColPrefs: function () {
381             if ($(g.t).is('.ajax')) {   // only send preferences if ajax class
382                 if (typeof g.db !== 'string' && typeof g.table !== 'string') {
383                     // The server has nothing to do with it
384                     // Issue: https://github.com/phpmyadmin/phpmyadmin/issues/15658
385                     return;
386                 }
387                 var postParams = {
388                     'ajax_request': true,
389                     'db': g.db,
390                     'table': g.table,
391                     'token': g.token,
392                     'server': g.server,
393                     'table_create_time': g.tableCreateTime
394                 };
395                 if (g.colOrder.length > 0) {
396                     $.extend(postParams, { 'col_order': g.colOrder.toString() });
397                 }
398                 if (g.colVisib.length > 0) {
399                     $.extend(postParams, { 'col_visib': g.colVisib.toString() });
400                 }
401                 $.post('index.php?route=/sql/set-column-preferences', postParams, function (data) {
402                     if (data.success !== true) {
403                         var $tempDiv = $(document.createElement('div'));
404                         $tempDiv.html(data.error);
405                         $tempDiv.addClass('alert alert-danger');
406                         Functions.ajaxShowMessage($tempDiv, false);
407                     }
408                 });
409             }
410         },
412         /**
413          * Refresh restore button state.
414          * Make restore button disabled if the table is similar with initial state.
415          */
416         refreshRestoreButton: function () {
417             // check if table state is as initial state
418             var isInitial = true;
419             for (var i = 0; i < g.colOrder.length; i++) {
420                 if (g.colOrder[i] !== i) {
421                     isInitial = false;
422                     break;
423                 }
424             }
425             // check if only one visible column left
426             var isOneColumn = g.visibleHeadersCount === 1;
427             // enable or disable restore button
428             if (isInitial || isOneColumn) {
429                 $(g.o).find('div.restore_column').hide();
430             } else {
431                 $(g.o).find('div.restore_column').show();
432             }
433         },
435         /**
436          * Update current hint using the boolean values (showReorderHint, showSortHint, etc.).
437          *
438          * @return {string}
439          *
440          */
441         updateHint: function () {
442             var text = '';
443             if (!g.colRsz && !g.colReorder) {     // if not resizing or dragging
444                 if (g.visibleHeadersCount > 1) {
445                     g.showReorderHint = true;
446                 }
447                 if ($(t).find('th.marker').length > 0) {
448                     g.showMarkHint = true;
449                 }
450                 if (g.showSortHint && g.sortHint) {
451                     text += text.length > 0 ? '<br>' : '';
452                     text += '- ' + g.sortHint;
453                 }
454                 if (g.showMultiSortHint && g.strMultiSortHint) {
455                     text += text.length > 0 ? '<br>' : '';
456                     text += '- ' + g.strMultiSortHint;
457                 }
458                 if (g.showMarkHint &&
459                     g.markHint &&
460                     ! g.showSortHint && // we do not show mark hint, when sort hint is shown
461                     g.showReorderHint &&
462                     g.reorderHint
463                 ) {
464                     text += text.length > 0 ? '<br>' : '';
465                     text += '- ' + g.reorderHint;
466                     text += text.length > 0 ? '<br>' : '';
467                     text += '- ' + g.markHint;
468                     text += text.length > 0 ? '<br>' : '';
469                     text += '- ' + g.copyHint;
470                 }
471             }
472             return text;
473         },
475         /**
476          * Toggle column's visibility.
477          * After calling this function and it returns true, afterToggleCol() must be called.
478          *
479          * @param {number} n
480          *
481          * @return {boolean} True if the column is toggled successfully.
482          */
483         toggleCol: function (n) {
484             if (g.colVisib[n]) {
485                 // can hide if more than one column is visible
486                 if (g.visibleHeadersCount > 1) {
487                     $(g.t).find('tr').each(function () {
488                         $(this).find('th.draggable').eq(n)
489                             .add($(this).find('td').eq(g.actionSpan + n))
490                             .hide();
491                     });
492                     g.colVisib[n] = 0;
493                     $(g.cList).find('.lDiv div').eq(n).find('input').prop('checked', false);
494                 } else {
495                     // cannot hide, force the checkbox to stay checked
496                     $(g.cList).find('.lDiv div').eq(n).find('input').prop('checked', true);
497                     return false;
498                 }
499             } else {    // column n is not visible
500                 $(g.t).find('tr').each(function () {
501                     $(this).find('th.draggable').eq(n)
502                         .add($(this).find('td').eq(g.actionSpan + n))
503                         .show();
504                 });
505                 g.colVisib[n] = 1;
506                 $(g.cList).find('.lDiv div').eq(n).find('input').prop('checked', true);
507             }
508             return true;
509         },
511         /**
512          * This must be called if toggleCol() returns is true.
513          *
514          * This function is separated from toggleCol because, sometimes, we want to toggle
515          * some columns together at one time and do just one adjustment after it, e.g. in showAllColumns().
516          */
517         afterToggleCol: function () {
518             // some adjustments after hiding column
519             g.reposRsz();
520             g.reposDrop();
521             g.sendColPrefs();
523             // check visible first row headers count
524             g.visibleHeadersCount = $(g.t).find('tr').first().find('th.draggable:visible').length;
525             g.refreshRestoreButton();
527             // Display minimum of one column - disable checkbox for hiding last column
528             if (g.visibleHeadersCount <= 1) {
529                 $(g.cList).find('.lDiv div').each(function () {
530                     $(this).find('input:checkbox:checked').prop('disabled', true);
531                 });
532             } else {
533                 // Remove disabled property if showing more than one column
534                 $(g.cList).find('.lDiv div').each(function () {
535                     $(this).find('input:checkbox:disabled').prop('disabled', false);
536                 });
537             }
538         },
540         /**
541          * Show columns' visibility list.
542          *
543          * @param obj The drop down arrow of column visibility list
544          */
545         showColList: function (obj) {
546             // only show when not resizing or reordering
547             if (!g.colRsz && !g.colReorder) {
548                 var pos = $(obj).position();
549                 $(g.cList).css({
550                     top: pos.top + $(obj).outerHeight(true)
551                 })
552                     .show();
553                 $(obj).addClass('coldrop-hover');
554             }
555         },
557         /**
558          * Hide columns' visibility list.
559          */
560         hideColList: function () {
561             $(g.cList).hide();
562             $(g.cDrop).find('.coldrop-hover').removeClass('coldrop-hover');
563         },
565         /**
566          * Reposition the column visibility drop-down arrow.
567          */
568         reposDrop: function () {
569             var $th = $(t).find('th:not(.draggable)');
570             for (var i = 0; i < $th.length; i++) {
571                 var $cd = $(g.cDrop).find('div').eq(i);   // column drop-down arrow
572                 var pos = $($th[i]).position();
573                 $cd.css({
574                     left: pos.left + $($th[i]).width() - $cd.width(),
575                     top: pos.top
576                 });
577             }
578         },
580         /**
581          * Show all hidden columns.
582          */
583         showAllColumns: function () {
584             for (var i = 0; i < g.colVisib.length; i++) {
585                 if (!g.colVisib[i]) {
586                     g.toggleCol(i);
587                 }
588             }
589             g.afterToggleCol();
590         },
592         /**
593          * Show edit cell, if it can be shown
594          *
595          * @param cell <td> element to be edited
596          */
597         showEditCell: function (cell) {
598             // destroy the date picker instance left if any, see: #17703
599             var $datePickerInstance = $(g.cEdit).find('.hasDatepicker');
600             if ($datePickerInstance.length > 0) {
601                 $datePickerInstance.datepicker('destroy');
602             }
604             if ($(cell).is('.grid_edit') &&
605                 !g.colRsz && !g.colReorder) {
606                 if (!g.isCellEditActive) {
607                     var $cell = $(cell);
609                     if ('string' === $cell.attr('data-type') ||
610                         'blob' === $cell.attr('data-type') ||
611                         'json' === $cell.attr('data-type')
612                     ) {
613                         g.cEdit = g.cEditTextarea;
614                     } else {
615                         g.cEdit = g.cEditStd;
616                     }
618                     // remove all edit area and hide it
619                     $(g.cEdit).find('.edit_area').empty().hide();
620                     // reposition the cEdit element
621                     $(g.cEdit).css({
622                         top: $cell.position().top,
623                         left: $cell.position().left
624                     })
625                         .show()
626                         .find('.edit_box')
627                         .css({
628                             width: $cell.outerWidth(),
629                             height: $cell.outerHeight()
630                         });
631                     // fill the cell edit with text from <td>
632                     var value = Functions.getCellValue(cell);
633                     if ($cell.attr('data-type') === 'json' && $cell.is('.truncated') === false) {
634                         value = Functions.stringifyJSON(value, null, 4);
635                     }
636                     $(g.cEdit).find('.edit_box').val(value);
638                     g.currentEditCell = cell;
639                     $(g.cEdit).find('.edit_box').trigger('focus');
640                     moveCursorToEnd($(g.cEdit).find('.edit_box'));
641                     $(g.cEdit).find('*').prop('disabled', false);
642                 }
643             }
645             function moveCursorToEnd (input) {
646                 var originalValue = input.val();
647                 var originallength = originalValue.length;
648                 input.val('');
649                 input.trigger('blur').trigger('focus').val(originalValue);
650                 input[0].setSelectionRange(originallength, originallength);
651             }
652         },
654         /**
655          * Remove edit cell and the edit area, if it is shown.
656          *
657          * @param force Optional, force to hide edit cell without saving edited field.
658          * @param data  Optional, data from the POST AJAX request to save the edited field
659          *              or just specify "true", if we want to replace the edited field with the new value.
660          * @param field Optional, the edited <td>. If not specified, the function will
661          *              use currently edited <td> from g.currentEditCell.
662          * @param options Optional, this object contains a boolean named move (true, if called from move* functions)
663          *                and a <td> to which the grid_edit should move
664          */
665         hideEditCell: function (force, data, field, options) {
666             if (g.isCellEditActive && !force) {
667                 // cell is being edited, save or post the edited data
668                 if (options !== undefined) {
669                     g.saveOrPostEditedCell(options);
670                 } else {
671                     g.saveOrPostEditedCell();
672                 }
673                 return;
674             }
676             // cancel any previous request
677             if (g.lastXHR !== null) {
678                 g.lastXHR.abort();
679                 g.lastXHR = null;
680             }
682             if (data) {
683                 if (g.currentEditCell) {    // save value of currently edited cell
684                     // replace current edited field with the new value
685                     var $thisField = $(g.currentEditCell);
686                     var isNull = $thisField.data('value') === null;
687                     if (isNull) {
688                         $thisField.find('span').html('NULL');
689                         $thisField.addClass('null');
690                     } else {
691                         $thisField.removeClass('null');
692                         var value = data.isNeedToRecheck
693                             ? data.truncatableFieldValue
694                             : $thisField.data('value');
696                         // Truncates the text.
697                         $thisField.removeClass('truncated');
698                         if (CommonParams.get('pftext') === 'P' && value.length > g.maxTruncatedLen) {
699                             $thisField.addClass('truncated');
700                             value = value.substring(0, g.maxTruncatedLen) + '...';
701                         }
703                         // Add <br> before carriage return.
704                         var newHtml = Functions.escapeHtml(value);
705                         newHtml = newHtml.replace(/\n/g, '<br>\n');
707                         var decimals = parseInt($thisField.attr('data-decimals'));
709                         // remove decimal places if column type not supported
710                         if ((decimals === 0) && ($thisField.attr('data-type').indexOf('time') !== -1)) {
711                             newHtml = newHtml.substring(0, newHtml.indexOf('.'));
712                         }
714                         // remove additional decimal places
715                         if ((decimals > 0) && ($thisField.attr('data-type').indexOf('time') !== -1)) {
716                             newHtml = newHtml.substring(0, newHtml.length - (6 - decimals));
717                         }
719                         var selector = 'span';
720                         if ($thisField.hasClass('hex') && $thisField.find('a').length) {
721                             selector = 'a';
722                         }
724                         // Updates the code keeping highlighting (if any).
725                         var $target = $thisField.find(selector);
726                         if (!Functions.updateCode($target, newHtml, value)) {
727                             $target.html(newHtml);
728                         }
729                     }
730                     if ($thisField.is('.bit')) {
731                         $thisField.find('span').text($thisField.data('value'));
732                     }
733                 }
734                 if (data.transformations !== undefined) {
735                     $.each(data.transformations, function (cellIndex, value) {
736                         var $thisField = $(g.t).find('.to_be_saved').eq(cellIndex);
737                         $thisField.find('span').html(value);
738                     });
739                 }
740                 if (data.relations !== undefined) {
741                     $.each(data.relations, function (cellIndex, value) {
742                         var $thisField = $(g.t).find('.to_be_saved').eq(cellIndex);
743                         $thisField.find('span').html(value);
744                     });
745                 }
747                 // refresh the grid
748                 g.reposRsz();
749                 g.reposDrop();
750             }
752             // hide the cell editing area
753             $(g.cEdit).hide();
754             $(g.cEdit).find('.edit_box').trigger('blur');
755             g.isCellEditActive = false;
756             g.currentEditCell = null;
757             // destroy datepicker in edit area, if exist
758             var $dp = $(g.cEdit).find('.hasDatepicker');
759             if ($dp.length > 0) {
760                 // eslint-disable-next-line no-underscore-dangle
761                 $(document).on('mousedown', $.datepicker._checkExternalClick);
762                 $dp.datepicker('refresh');
764                 // change the cursor in edit box back to normal
765                 // (the cursor become a hand pointer when we add datepicker)
766                 $(g.cEdit).find('.edit_box').css('cursor', 'inherit');
767             }
768         },
770         /**
771          * Show drop-down edit area when edit cell is focused.
772          */
773         showEditArea: function () {
774             if (!g.isCellEditActive) {   // make sure the edit area has not been shown
775                 g.isCellEditActive = true;
776                 g.isEditCellTextEditable = false;
777                 /**
778                  * @var $td current edited cell
779                  */
780                 var $td = $(g.currentEditCell);
781                 /**
782                  * @var $editArea the editing area
783                  */
784                 var $editArea = $(g.cEdit).find('.edit_area');
785                 /**
786                  * @var whereClause WHERE clause for the edited cell
787                  */
788                 var whereClause = $td.parent('tr').find('.where_clause').val();
789                 /**
790                  * @var fieldName  String containing the name of this field.
791                  * @see Sql.getFieldName()
792                  */
793                 var fieldName = Sql.getFieldName($(t), $td);
794                 /**
795                  * @var relationCurrValue String current value of the field (for fields that are foreign keyed).
796                  */
797                 var relationCurrValue = $td.text();
798                 /**
799                  * @var relationKeyOrDisplayColumn String relational key if in 'Relational display column' mode,
800                  * relational display column if in 'Relational key' mode (for fields that are foreign keyed).
801                  */
802                 var relationKeyOrDisplayColumn = $td.find('a').attr('title');
803                 /**
804                  * @var currValue String current value of the field (for fields that are of type enum or set).
805                  */
806                 var currValue = $td.find('span').text();
808                 // empty all edit area, then rebuild it based on $td classes
809                 $editArea.empty();
811                 // remember this instead of testing more than once
812                 var isNull = $td.is('.null');
814                 // add goto link, if this cell contains a link
815                 if ($td.find('a').length > 0) {
816                     var gotoLink = document.createElement('div');
817                     gotoLink.className = 'goto_link';
818                     $(gotoLink).append(g.gotoLinkText + ' ').append($td.find('a').clone());
819                     $editArea.append(gotoLink);
820                 }
822                 g.wasEditedCellNull = false;
823                 if ($td.is(':not(.not_null)')) {
824                     // append a null checkbox
825                     $editArea.append('<div class="null_div"><label>NULL:<input type="checkbox"></label></div>');
827                     var $checkbox = $editArea.find('.null_div input');
828                     // check if current <td> is NULL
829                     if (isNull) {
830                         $checkbox.prop('checked', true);
831                         g.wasEditedCellNull = true;
832                     }
834                     // if the select/editor is changed un-check the 'checkbox_null_<field_name>_<row_index>'.
835                     if ($td.is('.enum, .set')) {
836                         $editArea.on('change', 'select', function () {
837                             $checkbox.prop('checked', false);
838                         });
839                     } else if ($td.is('.relation')) {
840                         $editArea.on('change', 'select', function () {
841                             $checkbox.prop('checked', false);
842                         });
843                         $editArea.on('click', '.browse_foreign', function () {
844                             $checkbox.prop('checked', false);
845                         });
846                     } else {
847                         $(g.cEdit).on('keypress change paste', '.edit_box', function () {
848                             if ($(this).val() !== '') {
849                                 $checkbox.prop('checked', false);
850                             }
851                         });
852                         // Capture ctrl+v (on IE and Chrome)
853                         $(g.cEdit).on('keydown', '.edit_box', function (e) {
854                             if (e.ctrlKey && e.which === 86) {
855                                 $checkbox.prop('checked', false);
856                             }
857                         });
858                         $editArea.on('keydown', 'textarea', function () {
859                             $checkbox.prop('checked', false);
860                         });
861                     }
862                     // if some text is written in textbox automatically unmark the null checkbox and if it is emptied again mark the checkbox.
863                     $(g.cEdit).find('.edit_box').on('input', function () {
864                         if ($(g.cEdit).find('.edit_box').val() !== '') {
865                             $checkbox.prop('checked', false);
866                         } else {
867                             $checkbox.prop('checked', true);
868                         }
869                     });
870                     // if null checkbox is clicked empty the corresponding select/editor.
871                     $checkbox.on('click', function () {
872                         if ($td.is('.enum')) {
873                             $editArea.find('select').val('');
874                         } else if ($td.is('.set')) {
875                             $editArea.find('select').find('option').each(function () {
876                                 var $option = $(this);
877                                 $option.prop('selected', false);
878                             });
879                         } else if ($td.is('.relation')) {
880                             // if the dropdown is there to select the foreign value
881                             if ($editArea.find('select').length > 0) {
882                                 $editArea.find('select').val('');
883                             }
884                         } else {
885                             $editArea.find('textarea').val('');
886                         }
887                         $(g.cEdit).find('.edit_box').val('');
888                     });
889                 }
891                 // reset the position of the edit_area div after closing datetime picker
892                 $(g.cEdit).find('.edit_area').css({ 'top' :'0','position':'' });
894                 var postParams;
895                 if ($td.is('.relation')) {
896                     // handle relations
897                     $editArea.addClass('edit_area_loading');
899                     // initialize the original data
900                     $td.data('original_data', null);
902                     /**
903                      * @var postParams Object containing parameters for the POST request
904                      */
905                     postParams = {
906                         'ajax_request' : true,
907                         'server' : g.server,
908                         'db' : g.db,
909                         'table' : g.table,
910                         'column' : fieldName,
911                         'curr_value' : relationCurrValue,
912                         'relation_key_or_display_column' : relationKeyOrDisplayColumn
913                     };
915                     g.lastXHR = $.post('index.php?route=/sql/get-relational-values', postParams, function (data) {
916                         g.lastXHR = null;
917                         $editArea.removeClass('edit_area_loading');
918                         if ($(data.dropdown).is('select')) {
919                             // save original_data
920                             var value = $(data.dropdown).val();
921                             $td.data('original_data', value);
922                             // update the text input field, in case where the "Relational display column" is checked
923                             $(g.cEdit).find('.edit_box').val(value);
924                         }
926                         $editArea.append(data.dropdown);
927                         $editArea.append('<div class="cell_edit_hint">' + g.cellEditHint + '</div>');
929                         // for 'Browse foreign values' options,
930                         // hide the value next to 'Browse foreign values' link
931                         $editArea.find('span.curr_value').hide();
932                         // handle update for new values selected from new window
933                         $editArea.find('span.curr_value').on('change', function () {
934                             $(g.cEdit).find('.edit_box').val($(this).text());
935                         });
936                     }); // end $.post()
938                     $editArea.show();
939                     $editArea.on('change', 'select', function () {
940                         $(g.cEdit).find('.edit_box').val($(this).val());
941                     });
942                     g.isEditCellTextEditable = true;
943                 } else if ($td.is('.enum')) {
944                     // handle enum fields
945                     $editArea.addClass('edit_area_loading');
947                     /**
948                      * @var postParams Object containing parameters for the POST request
949                      */
950                     postParams = {
951                         'ajax_request' : true,
952                         'server' : g.server,
953                         'db' : g.db,
954                         'table' : g.table,
955                         'column' : fieldName,
956                         'curr_value' : currValue
957                     };
958                     g.lastXHR = $.post('index.php?route=/sql/get-enum-values', postParams, function (data) {
959                         g.lastXHR = null;
960                         if (typeof data === 'object' && data.success === false) {
961                             Functions.ajaxShowMessage(data.error, undefined, 'error');
962                             return;
963                         }
964                         $editArea.removeClass('edit_area_loading');
965                         $editArea.append(data.dropdown);
966                         $editArea.append('<div class="cell_edit_hint">' + g.cellEditHint + '</div>');
967                     }); // end $.post()
969                     $editArea.show();
970                     $editArea.on('change', 'select', function () {
971                         $(g.cEdit).find('.edit_box').val($(this).val());
972                     });
973                 } else if ($td.is('.set')) {
974                     // handle set fields
975                     $editArea.addClass('edit_area_loading');
977                     // if the data is truncated, get the full data
978                     if ($td.is('.truncated')) {
979                         postParams = {
980                             'ajax_request': true,
981                             'server': g.server,
982                             'db': g.db,
983                             'table': g.table,
984                             'column': fieldName,
985                             'curr_value': currValue,
986                             'get_full_values': true,
987                             'where_clause': whereClause
988                         };
989                     } else {
990                         postParams = {
991                             'ajax_request': true,
992                             'server': g.server,
993                             'db': g.db,
994                             'table': g.table,
995                             'column': fieldName,
996                             'curr_value': currValue
997                         };
998                     }
1000                     g.lastXHR = $.post('index.php?route=/sql/get-set-values', postParams, function (data) {
1001                         g.lastXHR = null;
1002                         if (typeof data === 'object' && data.success === false) {
1003                             Functions.ajaxShowMessage(data.error, undefined, 'error');
1004                             return;
1005                         }
1006                         $editArea.removeClass('edit_area_loading');
1007                         $editArea.append(data.select);
1008                         $td.data('original_data', $(data.select).val().join());
1009                         $editArea.append('<div class="cell_edit_hint">' + g.cellEditHint + '</div>');
1010                     }); // end $.post()
1012                     $editArea.show();
1013                     $editArea.on('change', 'select', function () {
1014                         $(g.cEdit).find('.edit_box').val($(this).val());
1015                     });
1016                 } else if ($td.is('.truncated, .transformed')) {
1017                     if ($td.is('.to_be_saved')) {   // cell has been edited
1018                         var value = $td.data('value');
1019                         $(g.cEdit).find('.edit_box').val(value);
1020                         $editArea.append('<textarea></textarea>');
1021                         $editArea.find('textarea').val(value);
1022                         $editArea
1023                             .on('keyup', 'textarea', function () {
1024                                 $(g.cEdit).find('.edit_box').val($(this).val());
1025                             });
1026                         $(g.cEdit).on('keyup', '.edit_box', function () {
1027                             $editArea.find('textarea').val($(this).val());
1028                         });
1029                         $editArea.append('<div class="cell_edit_hint">' + g.cellEditHint + '</div>');
1030                     } else {
1031                         // handle truncated/transformed values values
1032                         $editArea.addClass('edit_area_loading');
1034                         // initialize the original data
1035                         $td.data('original_data', null);
1037                         /**
1038                          * @var sqlQuery   String containing the SQL query used to retrieve value of truncated/transformed data
1039                          */
1040                         var sqlQuery = 'SELECT `' + fieldName + '` FROM `' + g.table + '` WHERE ' + whereClause;
1042                         // Make the Ajax call and get the data, wrap it and insert it
1043                         g.lastXHR = $.post('index.php?route=/sql', {
1044                             'server' : g.server,
1045                             'db' : g.db,
1046                             'ajax_request' : true,
1047                             'sql_query' : sqlQuery,
1048                             'grid_edit' : true
1049                         }, function (data) {
1050                             g.lastXHR = null;
1051                             $editArea.removeClass('edit_area_loading');
1052                             if (typeof data !== 'undefined' && data.success === true) {
1053                                 if ($td.attr('data-type') === 'json') {
1054                                     data.value = Functions.stringifyJSON(data.value, null, 4);
1055                                 }
1056                                 $td.data('original_data', data.value);
1057                                 $(g.cEdit).find('.edit_box').val(data.value);
1058                             } else {
1059                                 Functions.ajaxShowMessage(data.error, false);
1060                             }
1061                         }); // end $.post()
1062                     }
1063                     g.isEditCellTextEditable = true;
1064                 } else if ($td.is('.timefield, .datefield, .datetimefield, .timestampfield')) {
1065                     var $inputField = $(g.cEdit).find('.edit_box');
1067                     // remember current datetime value in $input_field, if it is not null
1068                     var datetimeValue = !isNull ? $inputField.val() : '';
1070                     var showMillisec = false;
1071                     var showMicrosec = false;
1072                     var timeFormat = 'HH:mm:ss';
1073                     // check for decimal places of seconds
1074                     if (($td.attr('data-decimals') > 0) && ($td.attr('data-type').indexOf('time') !== -1)) {
1075                         if (datetimeValue && datetimeValue.indexOf('.') === false) {
1076                             datetimeValue += '.';
1077                         }
1078                         if ($td.attr('data-decimals') > 3) {
1079                             showMillisec = true;
1080                             showMicrosec = true;
1081                             timeFormat = 'HH:mm:ss.lc';
1083                             if (datetimeValue) {
1084                                 datetimeValue += '000000';
1085                                 datetimeValue = datetimeValue.substring(0, datetimeValue.indexOf('.') + 7);
1086                                 $inputField.val(datetimeValue);
1087                             }
1088                         } else {
1089                             showMillisec = true;
1090                             timeFormat = 'HH:mm:ss.l';
1092                             if (datetimeValue) {
1093                                 datetimeValue += '000';
1094                                 datetimeValue = datetimeValue.substring(0, datetimeValue.indexOf('.') + 4);
1095                                 $inputField.val(datetimeValue);
1096                             }
1097                         }
1098                     }
1100                     // add datetime picker
1101                     Functions.addDatepicker($inputField, $td.attr('data-type'), {
1102                         showMillisec: showMillisec,
1103                         showMicrosec: showMicrosec,
1104                         timeFormat: timeFormat,
1105                         firstDay: firstDayOfCalendar
1106                     });
1108                     $inputField.on('keyup', function (e) {
1109                         if (e.which === 13) {
1110                             // post on pressing "Enter"
1111                             e.preventDefault();
1112                             e.stopPropagation();
1113                             g.saveOrPostEditedCell();
1114                         } else if (e.which !== 27) {
1115                             Functions.toggleDatepickerIfInvalid($td, $inputField);
1116                         }
1117                     });
1119                     $inputField.datepicker('show');
1120                     Functions.toggleDatepickerIfInvalid($td, $inputField);
1122                     // unbind the mousedown event to prevent the problem of
1123                     // datepicker getting closed, needs to be checked for any
1124                     // change in names when updating
1125                     // eslint-disable-next-line no-underscore-dangle
1126                     $(document).off('mousedown', $.datepicker._checkExternalClick);
1128                     // move ui-datepicker-div inside cEdit div
1129                     var datepickerDiv = $('#ui-datepicker-div');
1130                     datepickerDiv.css({ 'top': 0, 'left': 0, 'position': 'relative' });
1131                     $(g.cEdit).append(datepickerDiv);
1133                     // cancel any click on the datepicker element
1134                     $editArea.find('> *').on('click', function (e) {
1135                         e.stopPropagation();
1136                     });
1138                     g.isEditCellTextEditable = true;
1139                 } else {
1140                     g.isEditCellTextEditable = true;
1141                     // only append edit area hint if there is a null checkbox
1142                     if ($editArea.children().length > 0) {
1143                         $editArea.append('<div class="cell_edit_hint">' + g.cellEditHint + '</div>');
1144                     }
1145                 }
1146                 if ($editArea.children().length > 0) {
1147                     $editArea.show();
1148                 }
1149             }
1150         },
1152         /**
1153          * Post the content of edited cell.
1154          *
1155          * @param options Optional, this object contains a boolean named move (true, if called from move* functions)
1156          *                and a <td> to which the grid_edit should move
1157          */
1158         postEditedCell: function (options) {
1159             if (g.isSaving) {
1160                 return;
1161             }
1162             g.isSaving = true;
1163             /**
1164              * @var relationFields Array containing the name/value pairs of relational fields
1165              */
1166             var relationFields = {};
1167             /**
1168              * @var relationalDisplay string 'K' if relational key, 'D' if relational display column
1169              */
1170             var relationalDisplay = $(g.o).find('input[name=relational_display]:checked').val();
1171             /**
1172              * @var transformFields    Array containing the name/value pairs for transformed fields
1173              */
1174             var transformFields = {};
1175             /**
1176              * @var transformationFields   Boolean, if there are any transformed fields in the edited cells
1177              */
1178             var transformationFields = false;
1179             /**
1180              * @var fullSqlQuery String containing the complete SQL query to update this table
1181              */
1182             var fullSqlQuery = '';
1183             /**
1184              * @var relFieldsList  String, url encoded representation of {@link relations_fields}
1185              */
1186             var relFieldsList = '';
1187             /**
1188              * @var transformFieldsList  String, url encoded representation of {@link transformFields}
1189              */
1190             var transformFieldsList = '';
1191             /**
1192              * @var fullWhereClause Array containing where clause for updated fields
1193              */
1194             var fullWhereClause = [];
1195             /**
1196              * @var isUnique   Boolean, whether the rows in this table is unique or not
1197              */
1198             var isUnique = $(g.t).find('td.edit_row_anchor').is('.nonunique') ? 0 : 1;
1199             /**
1200              * multi edit variables
1201              */
1202             var multiEditFieldsName = [];
1203             var multiEditFieldsType = [];
1204             var multiEditFields = [];
1205             var multiEditFieldsNull = [];
1207             // alert user if edited table is not unique
1208             if (!isUnique) {
1209                 alert(g.alertNonUnique);
1210             }
1212             // loop each edited row
1213             $(g.t).find('td.to_be_saved').parents('tr').each(function () {
1214                 var $tr = $(this);
1215                 var whereClause = $tr.find('.where_clause').val();
1216                 if (typeof whereClause === 'undefined') {
1217                     whereClause = '';
1218                 }
1219                 fullWhereClause.push(whereClause);
1220                 var conditionArrayContent = $tr.find('.condition_array').val();
1221                 if (typeof conditionArrayContent === 'undefined') {
1222                     conditionArrayContent = '{}';
1223                 }
1224                 var conditionArray = JSON.parse(conditionArrayContent);
1226                 /**
1227                  * multi edit variables, for current row
1228                  * @TODO array indices are still not correct, they should be md5 of field's name
1229                  */
1230                 var fieldsName = [];
1231                 var fieldsType = [];
1232                 var fields = [];
1233                 var fieldsNull = [];
1235                 // loop each edited cell in a row
1236                 $tr.find('.to_be_saved').each(function () {
1237                     /**
1238                      * @var $thisField    Object referring to the td that is being edited
1239                      */
1240                     var $thisField = $(this);
1242                     /**
1243                      * @var fieldName  String containing the name of this field.
1244                      * @see Sql.getFieldName()
1245                      */
1246                     var fieldName = Sql.getFieldName($(g.t), $thisField);
1248                     /**
1249                      * @var thisFieldParams   Array temporary storage for the name/value of current field
1250                      */
1251                     var thisFieldParams = {};
1253                     if ($thisField.is('.transformed')) {
1254                         transformationFields =  true;
1255                     }
1256                     thisFieldParams[fieldName] = $thisField.data('value');
1258                     /**
1259                      * @var isNull String capturing whether 'checkbox_null_<field_name>_<row_index>' is checked.
1260                      */
1261                     var isNull = thisFieldParams[fieldName] === null;
1263                     fieldsName.push(fieldName);
1265                     if (isNull) {
1266                         fieldsNull.push('on');
1267                         fields.push('');
1268                     } else {
1269                         if ($thisField.is('.bit')) {
1270                             fieldsType.push('bit');
1271                         } else if ($thisField.hasClass('hex')) {
1272                             fieldsType.push('hex');
1273                         }
1274                         fieldsNull.push('');
1276                         if ($thisField.attr('data-type') !== 'json') {
1277                             fields.push($thisField.data('value'));
1278                         } else {
1279                             const JSONString = Functions.stringifyJSON($thisField.data('value'));
1280                             fields.push(JSONString);
1281                         }
1283                         var cellIndex = $thisField.index('.to_be_saved');
1284                         if ($thisField.is(':not(.relation, .enum, .set, .bit)')) {
1285                             if ($thisField.is('.transformed')) {
1286                                 transformFields[cellIndex] = {};
1287                                 $.extend(transformFields[cellIndex], thisFieldParams);
1288                             }
1289                         } else if ($thisField.is('.relation')) {
1290                             relationFields[cellIndex] = {};
1291                             $.extend(relationFields[cellIndex], thisFieldParams);
1292                         }
1293                     }
1294                     // check if edited field appears in WHERE clause
1295                     if (whereClause.indexOf(Sql.urlEncode(fieldName)) > -1) {
1296                         var fieldStr = '`' + g.table + '`.' + '`' + fieldName + '`';
1297                         for (var field in conditionArray) {
1298                             if (field.indexOf(fieldStr) > -1) {
1299                                 conditionArray[field] = isNull ? 'IS NULL' : '= \'' + thisFieldParams[fieldName].replace(/'/g, '\'\'') + '\'';
1300                                 break;
1301                             }
1302                         }
1303                     }
1304                 }); // end of loop for every edited cells in a row
1306                 // save new_clause
1307                 var newClause = '';
1308                 for (var field in conditionArray) {
1309                     newClause += field + ' ' + conditionArray[field] + ' AND ';
1310                 }
1311                 newClause = newClause.substring(0, newClause.length - 5); // remove the last AND
1312                 $tr.data('new_clause', newClause);
1313                 // save condition_array
1314                 $tr.find('.condition_array').val(JSON.stringify(conditionArray));
1316                 multiEditFieldsName.push(fieldsName);
1317                 multiEditFieldsType.push(fieldsType);
1318                 multiEditFields.push(fields);
1319                 multiEditFieldsNull.push(fieldsNull);
1320             }); // end of loop for every edited rows
1322             relFieldsList = $.param(relationFields);
1323             transformFieldsList = $.param(transformFields);
1325             // Make the Ajax post after setting all parameters
1326             /**
1327              * @var postParams Object containing parameters for the POST request
1328              */
1329             var postParams = { 'ajax_request' : true,
1330                 'sql_query' : fullSqlQuery,
1331                 'server' : g.server,
1332                 'db' : g.db,
1333                 'table' : g.table,
1334                 'clause_is_unique' : isUnique,
1335                 'where_clause' : fullWhereClause,
1336                 'fields[multi_edit]' : multiEditFields,
1337                 'fields_name[multi_edit]' : multiEditFieldsName,
1338                 'fields_type[multi_edit]' : multiEditFieldsType,
1339                 'fields_null[multi_edit]' : multiEditFieldsNull,
1340                 'rel_fields_list' : relFieldsList,
1341                 'do_transformations' : transformationFields,
1342                 'transform_fields_list' : transformFieldsList,
1343                 'relational_display' : relationalDisplay,
1344                 'goto' : encodeURIComponent('index.php?route=/sql'),
1345                 'submit_type' : 'save'
1346             };
1348             if (!g.saveCellsAtOnce) {
1349                 $(g.cEdit).find('*').prop('disabled', true);
1350                 $(g.cEdit).find('.edit_box').addClass('edit_box_posting');
1351             } else {
1352                 $(g.o).find('div.save_edited').addClass('saving_edited_data')
1353                     .find('input').prop('disabled', true);    // disable the save button
1354             }
1356             $.ajax({
1357                 type: 'POST',
1358                 url: 'index.php?route=/table/replace',
1359                 data: postParams,
1360                 success:
1361                     function (data) {
1362                         g.isSaving = false;
1363                         if (!g.saveCellsAtOnce) {
1364                             $(g.cEdit).find('*').prop('disabled', false);
1365                             $(g.cEdit).find('.edit_box').removeClass('edit_box_posting');
1366                         } else {
1367                             $(g.o).find('div.save_edited').removeClass('saving_edited_data')
1368                                 .find('input').prop('disabled', false);  // enable the save button back
1369                         }
1370                         if (typeof data !== 'undefined' && data.success === true) {
1371                             if (typeof options === 'undefined' || ! options.move) {
1372                                 Functions.ajaxShowMessage(data.message);
1373                             }
1375                             // update where_clause related data in each edited row
1376                             $(g.t).find('td.to_be_saved').parents('tr').each(function () {
1377                                 var newClause = $(this).data('new_clause');
1378                                 var $whereClause = $(this).find('.where_clause');
1379                                 var oldClause = $whereClause.val();
1380                                 var decodedOldClause = oldClause;
1381                                 var decodedNewClause = newClause;
1383                                 $whereClause.val(newClause);
1384                                 // update Edit, Copy, and Delete links also
1385                                 $(this).find('a').each(function () {
1386                                     $(this).attr('href', $(this).attr('href').replace(oldClause, newClause));
1387                                     // update delete confirmation in Delete link
1388                                     if ($(this).attr('href').indexOf('DELETE') > -1) {
1389                                         $(this).removeAttr('onclick')
1390                                             .off('click')
1391                                             .on('click', function () {
1392                                                 return Functions.confirmLink(this, 'DELETE FROM `' + g.db + '`.`' + g.table + '` WHERE ' +
1393                                                        decodedNewClause + (isUnique ? '' : ' LIMIT 1'));
1394                                             });
1395                                     }
1396                                 });
1397                                 // update the multi edit checkboxes
1398                                 $(this).find('input[type=checkbox]').each(function () {
1399                                     var $checkbox = $(this);
1400                                     var checkboxName = $checkbox.attr('name');
1401                                     var checkboxValue = $checkbox.val();
1403                                     $checkbox.attr('name', checkboxName.replace(oldClause, newClause));
1404                                     $checkbox.val(checkboxValue.replace(decodedOldClause, decodedNewClause));
1405                                 });
1406                             });
1407                             // update the display of executed SQL query command
1408                             if (typeof data.sql_query !== 'undefined') {
1409                                 // extract query box
1410                                 var $resultQuery = $($.parseHTML(data.sql_query));
1411                                 var sqlOuter = $resultQuery.find('.sqlOuter').wrap('<p>').parent().html();
1412                                 var tools = $resultQuery.find('.tools').wrap('<p>').parent().html();
1413                                 // sqlOuter and tools will not be present if 'Show SQL queries' configuration is off
1414                                 if (typeof sqlOuter !== 'undefined' && typeof tools !== 'undefined') {
1415                                     $(g.o).find('.result_query').not($(g.o).find('.result_query').last()).remove();
1416                                     var $existingQuery = $(g.o).find('.result_query');
1417                                     // If two query box exists update query in second else add a second box
1418                                     if ($existingQuery.find('div.sqlOuter').length > 1) {
1419                                         $existingQuery.children().eq(3).remove();
1420                                         $existingQuery.children().eq(3).remove();
1421                                         $existingQuery.append(sqlOuter + tools);
1422                                     } else {
1423                                         $existingQuery.append(sqlOuter + tools);
1424                                     }
1425                                     Functions.highlightSql($existingQuery);
1426                                 }
1427                             }
1428                             // hide and/or update the successfully saved cells
1429                             g.hideEditCell(true, data);
1431                             // remove the "Save edited cells" button
1432                             $(g.o).find('div.save_edited').hide();
1433                             // update saved fields
1434                             $(g.t).find('.to_be_saved')
1435                                 .removeClass('to_be_saved')
1436                                 .data('value', null)
1437                                 .data('original_data', null);
1439                             g.isCellEdited = false;
1440                         } else {
1441                             Functions.ajaxShowMessage(data.error, false);
1442                             if (!g.saveCellsAtOnce) {
1443                                 $(g.t).find('.to_be_saved')
1444                                     .removeClass('to_be_saved');
1445                             }
1446                         }
1447                     }
1448             }).done(function () {
1449                 if (options !== undefined && options.move) {
1450                     g.showEditCell(options.cell);
1451                 }
1452             }); // end $.ajax()
1453         },
1455         /**
1456          * Save edited cell, so it can be posted later.
1457          *
1458          * @return {bool}
1459          */
1460         saveEditedCell: function () {
1461             /**
1462              * @var $thisField    Object referring to the td that is being edited
1463              */
1464             var $thisField = $(g.currentEditCell);
1465             var $testElement = ''; // to test the presence of a element
1467             var needToPost = false;
1469             /**
1470              * @var fieldName  String containing the name of this field.
1471              * @see Sql.getFieldName()
1472              */
1473             var fieldName = Sql.getFieldName($(g.t), $thisField);
1475             /**
1476              * @var thisFieldParams   Array temporary storage for the name/value of current field
1477              */
1478             var thisFieldParams = {};
1480             /**
1481              * @var isNull String capturing whether 'checkbox_null_<field_name>_<row_index>' is checked.
1482              */
1483             var isNull = $(g.cEdit).find('input:checkbox').is(':checked');
1485             if ($(g.cEdit).find('.edit_area').is('.edit_area_loading')) {
1486                 // the edit area is still loading (retrieving cell data), no need to post
1487                 needToPost = false;
1488             } else if (isNull) {
1489                 if (!g.wasEditedCellNull) {
1490                     thisFieldParams[fieldName] = null;
1491                     needToPost = true;
1492                 }
1493             } else {
1494                 if ($thisField.is('.bit')) {
1495                     thisFieldParams[fieldName] = $(g.cEdit).find('.edit_box').val();
1496                 } else if ($thisField.is('.set')) {
1497                     $testElement = $(g.cEdit).find('select');
1498                     thisFieldParams[fieldName] = $testElement.map(function () {
1499                         return $(this).val();
1500                     }).get().join(',');
1501                 } else if ($thisField.is('.relation, .enum')) {
1502                     // for relation and enumeration, take the results from edit box value,
1503                     // because selected value from drop-down, new window or multiple
1504                     // selection list will always be updated to the edit box
1505                     thisFieldParams[fieldName] = $(g.cEdit).find('.edit_box').val();
1506                 } else if ($thisField.hasClass('hex')) {
1507                     if ($(g.cEdit).find('.edit_box').val().match(/^(0x)?[a-f0-9]*$/i) !== null) {
1508                         thisFieldParams[fieldName] = $(g.cEdit).find('.edit_box').val();
1509                     } else {
1510                         var hexError = '<div class="alert alert-danger" role="alert">' + Messages.strEnterValidHex + '</div>';
1511                         Functions.ajaxShowMessage(hexError, false);
1512                         thisFieldParams[fieldName] = Functions.getCellValue(g.currentEditCell);
1513                     }
1514                 } else {
1515                     thisFieldParams[fieldName] = $(g.cEdit).find('.edit_box').val();
1516                 }
1518                 let isValueUpdated;
1519                 if ($thisField.attr('data-type') !== 'json') {
1520                     isValueUpdated = thisFieldParams[fieldName] !== Functions.getCellValue(g.currentEditCell);
1521                 } else {
1522                     const JSONString = Functions.stringifyJSON(thisFieldParams[fieldName]);
1523                     isValueUpdated = JSONString !== JSON.stringify(JSON.parse(Functions.getCellValue(g.currentEditCell)));
1524                 }
1526                 if (g.wasEditedCellNull || isValueUpdated) {
1527                     needToPost = true;
1528                 }
1529             }
1531             if (needToPost) {
1532                 $(g.currentEditCell).addClass('to_be_saved')
1533                     .data('value', thisFieldParams[fieldName]);
1534                 if (g.saveCellsAtOnce) {
1535                     $(g.o).find('div.save_edited').show();
1536                 }
1537                 g.isCellEdited = true;
1538             }
1540             return needToPost;
1541         },
1543         /**
1544          * Save or post currently edited cell, depending on the "saveCellsAtOnce" configuration.
1545          *
1546          * @param options Optional, this object contains a boolean named move (true, if called from move* functions)
1547          *                and a <td> to which the grid_edit should move
1548          */
1549         saveOrPostEditedCell: function (options) {
1550             var saved = g.saveEditedCell();
1551             // Check if $cfg['SaveCellsAtOnce'] is false
1552             if (!g.saveCellsAtOnce) {
1553                 // Check if need_to_post is true
1554                 if (saved) {
1555                     // Check if this function called from 'move' functions
1556                     if (options !== undefined && options.move) {
1557                         g.postEditedCell(options);
1558                     } else {
1559                         g.postEditedCell();
1560                     }
1561                 // need_to_post is false
1562                 } else {
1563                     // Check if this function called from 'move' functions
1564                     if (options !== undefined && options.move) {
1565                         g.hideEditCell(true);
1566                         g.showEditCell(options.cell);
1567                     // NOT called from 'move' functions
1568                     } else {
1569                         g.hideEditCell(true);
1570                     }
1571                 }
1572             // $cfg['SaveCellsAtOnce'] is true
1573             } else {
1574                 // If need_to_post
1575                 if (saved) {
1576                     // If this function called from 'move' functions
1577                     if (options !== undefined && options.move) {
1578                         g.hideEditCell(true, true, false, options);
1579                         g.showEditCell(options.cell);
1580                     // NOT called from 'move' functions
1581                     } else {
1582                         g.hideEditCell(true, true);
1583                     }
1584                 } else {
1585                     // If this function called from 'move' functions
1586                     if (options !== undefined && options.move) {
1587                         g.hideEditCell(true, false, false, options);
1588                         g.showEditCell(options.cell);
1589                     // NOT called from 'move' functions
1590                     } else {
1591                         g.hideEditCell(true);
1592                     }
1593                 }
1594             }
1595         },
1597         /**
1598          * Initialize column resize feature.
1599          */
1600         initColResize: function () {
1601             // create column resizer div
1602             g.cRsz = document.createElement('div');
1603             g.cRsz.className = 'cRsz';
1605             // get data columns in the first row of the table
1606             var $firstRowCols = $(g.t).find('tr').first().find('th.draggable');
1608             // create column borders
1609             $firstRowCols.each(function () {
1610                 var cb = document.createElement('div'); // column border
1611                 $(cb).addClass('colborder')
1612                     .on('mousedown', function (e) {
1613                         g.dragStartRsz(e, this);
1614                     });
1615                 $(g.cRsz).append(cb);
1616             });
1617             g.reposRsz();
1619             // attach to global div
1620             $(g.gDiv).prepend(g.cRsz);
1621         },
1623         /**
1624          * Initialize column reordering feature.
1625          */
1626         initColReorder: function () {
1627             g.cCpy = document.createElement('div');     // column copy, to store copy of dragged column header
1628             g.cPointer = document.createElement('div'); // column pointer, used when reordering column
1630             // adjust g.cCpy
1631             g.cCpy.className = 'cCpy';
1632             $(g.cCpy).hide();
1634             // adjust g.cPointer
1635             g.cPointer.className = 'cPointer';
1636             $(g.cPointer).css('visibility', 'hidden');  // set visibility to hidden instead of calling hide() to force browsers to cache the image in cPointer class
1638             // assign column reordering hint
1639             g.reorderHint = Messages.strColOrderHint;
1641             // get data columns in the first row of the table
1642             var $firstRowCols = $(g.t).find('tr').first().find('th.draggable');
1644             // initialize column order
1645             var $colOrder = $(g.o).find('.col_order');   // check if column order is passed from PHP
1646             var i;
1647             if ($colOrder.length > 0) {
1648                 g.colOrder = $colOrder.val().split(',');
1649                 for (i = 0; i < g.colOrder.length; i++) {
1650                     g.colOrder[i] = parseInt(g.colOrder[i], 10);
1651                 }
1652             } else {
1653                 g.colOrder = [];
1654                 for (i = 0; i < $firstRowCols.length; i++) {
1655                     g.colOrder.push(i);
1656                 }
1657             }
1659             // register events
1660             $(g.t).find('th.draggable')
1661                 .on('mousedown', function (e) {
1662                     $(g.o).addClass('turnOffSelect');
1663                     if (g.visibleHeadersCount > 1) {
1664                         g.dragStartReorder(e, this);
1665                     }
1666                 })
1667                 .on('mouseenter', function () {
1668                     if (g.visibleHeadersCount > 1) {
1669                         $(this).css('cursor', 'move');
1670                     } else {
1671                         $(this).css('cursor', 'inherit');
1672                     }
1673                 })
1674                 .on('mouseleave', function () {
1675                     g.showReorderHint = false;
1676                     $(this).uiTooltip('option', {
1677                         content: g.updateHint()
1678                     });
1679                 })
1680                 .on('dblclick', function (e) {
1681                     e.preventDefault();
1682                     var res = Functions.copyToClipboard($(this).data('column'));
1683                     if (res) {
1684                         Functions.ajaxShowMessage(Messages.strCopyColumnSuccess, false, 'success');
1685                     } else {
1686                         Functions.ajaxShowMessage(Messages.strCopyColumnFailure, false, 'error');
1687                     }
1688                 });
1689             $(g.t).find('th.draggable a')
1690                 .on('dblclick', function (e) {
1691                     e.stopPropagation();
1692                 });
1693             // restore column order when the restore button is clicked
1694             $(g.o).find('div.restore_column').on('click', function () {
1695                 g.restoreColOrder();
1696             });
1698             // attach to global div
1699             $(g.gDiv).append(g.cPointer);
1700             $(g.gDiv).append(g.cCpy);
1702             // prevent default "dragstart" event when dragging a link
1703             $(g.t).find('th a').on('dragstart', function () {
1704                 return false;
1705             });
1707             // refresh the restore column button state
1708             g.refreshRestoreButton();
1709         },
1711         /**
1712          * Initialize column visibility feature.
1713          */
1714         initColVisib: function () {
1715             g.cDrop = document.createElement('div');    // column drop-down arrows
1716             g.cList = document.createElement('div');    // column visibility list
1718             // adjust g.cDrop
1719             g.cDrop.className = 'cDrop';
1721             // adjust g.cList
1722             g.cList.className = 'cList';
1723             $(g.cList).hide();
1725             // assign column visibility related hints
1726             g.showAllColText = Messages.strShowAllCol;
1728             // get data columns in the first row of the table
1729             var $firstRowCols = $(g.t).find('tr').first().find('th.draggable');
1731             var i;
1732             // initialize column visibility
1733             var $colVisib = $(g.o).find('.col_visib');   // check if column visibility is passed from PHP
1734             if ($colVisib.length > 0) {
1735                 g.colVisib = $colVisib.val().split(',');
1736                 for (i = 0; i < g.colVisib.length; i++) {
1737                     g.colVisib[i] = parseInt(g.colVisib[i], 10);
1738                 }
1739             } else {
1740                 g.colVisib = [];
1741                 for (i = 0; i < $firstRowCols.length; i++) {
1742                     g.colVisib.push(1);
1743                 }
1744             }
1746             // make sure we have more than one column
1747             if ($firstRowCols.length > 1) {
1748                 var $colVisibTh = $(g.t).find('th:not(.draggable)').slice(0, 1);
1749                 Functions.tooltip(
1750                     $colVisibTh,
1751                     'th',
1752                     Messages.strColVisibHint
1753                 );
1755                 // create column visibility drop-down arrow(s)
1756                 $colVisibTh.each(function () {
1757                     var cd = document.createElement('div'); // column drop-down arrow
1758                     $(cd).addClass('coldrop')
1759                         .on('click', function () {
1760                             if (g.cList.style.display === 'none') {
1761                                 g.showColList(this);
1762                             } else {
1763                                 g.hideColList();
1764                             }
1765                         });
1766                     $(g.cDrop).append(cd);
1767                 });
1769                 // add column visibility control
1770                 g.cList.innerHTML = '<div class="lDiv"></div>';
1771                 var $listDiv = $(g.cList).find('div');
1773                 var tempClick = function () {
1774                     if (g.toggleCol($(this).index())) {
1775                         g.afterToggleCol();
1776                     }
1777                 };
1779                 for (i = 0; i < $firstRowCols.length; i++) {
1780                     var currHeader = $firstRowCols[i];
1781                     var listElmt = document.createElement('div');
1782                     $(listElmt).text($(currHeader).text())
1783                         .prepend('<input type="checkbox" ' + (g.colVisib[i] ? 'checked="checked" ' : '') + '>');
1784                     $listDiv.append(listElmt);
1785                     // add event on click
1786                     $(listElmt).on('click', tempClick);
1787                 }
1788                 // add "show all column" button
1789                 var showAll = document.createElement('div');
1790                 $(showAll).addClass('showAllColBtn')
1791                     .text(g.showAllColText);
1792                 $(g.cList).append(showAll);
1793                 $(showAll).on('click', function () {
1794                     g.showAllColumns();
1795                 });
1796                 // prepend "show all column" button at top if the list is too long
1797                 if ($firstRowCols.length > 10) {
1798                     var clone = showAll.cloneNode(true);
1799                     $(g.cList).prepend(clone);
1800                     $(clone).on('click', function () {
1801                         g.showAllColumns();
1802                     });
1803                 }
1804             }
1806             // hide column visibility list if we move outside the list
1807             $(g.t).find('td, th.draggable').on('mouseenter', function () {
1808                 g.hideColList();
1809             });
1811             // attach to first row first col of the grid
1812             var thFirst = $(g.t).find('th.d-print-none');
1813             $(thFirst).append(g.cDrop);
1814             $(thFirst).append(g.cList);
1816             // some adjustment
1817             g.reposDrop();
1818         },
1820         /**
1821          * Move currently Editing Cell to Up
1822          *
1823          * @param e
1824          *
1825          */
1826         moveUp: function (e) {
1827             e.preventDefault();
1828             var $thisField = $(g.currentEditCell);
1829             var fieldName = Sql.getFieldName($(g.t), $thisField);
1831             var whereClause = $thisField.parents('tr').first().find('.where_clause').val();
1832             if (typeof whereClause === 'undefined') {
1833                 whereClause = '';
1834             }
1835             var found = false;
1836             var $prevRow;
1838             $thisField.parents('tr').first().parents('tbody').children().each(function () {
1839                 if ($(this).find('.where_clause').val() === whereClause) {
1840                     found = true;
1841                 }
1842                 if (!found) {
1843                     $prevRow = $(this);
1844                 }
1845             });
1847             var newCell;
1849             if (found && $prevRow) {
1850                 $prevRow.children('td').each(function () {
1851                     if (Sql.getFieldName($(g.t), $(this)) === fieldName) {
1852                         newCell = this;
1853                     }
1854                 });
1855             }
1857             if (newCell) {
1858                 g.hideEditCell(false, false, false, { move : true, cell : newCell });
1859             }
1860         },
1862         /**
1863          * Move currently Editing Cell to Down
1864          *
1865          * @param e
1866          *
1867          */
1868         moveDown: function (e) {
1869             e.preventDefault();
1871             var $thisField = $(g.currentEditCell);
1872             var fieldName = Sql.getFieldName($(g.t), $thisField);
1874             var whereClause = $thisField.parents('tr').first().find('.where_clause').val();
1875             if (typeof whereClause === 'undefined') {
1876                 whereClause = '';
1877             }
1878             var found = false;
1879             var $nextRow;
1880             var j = 0;
1881             var nextRowFound = false;
1882             $thisField.parents('tr').first().parents('tbody').children().each(function () {
1883                 if ($(this).find('.where_clause').val() === whereClause) {
1884                     found = true;
1885                 }
1886                 if (found) {
1887                     if (j >= 1 && ! nextRowFound) {
1888                         $nextRow = $(this);
1889                         nextRowFound = true;
1890                     } else {
1891                         j++;
1892                     }
1893                 }
1894             });
1896             var newCell;
1897             if (found && $nextRow) {
1898                 $nextRow.children('td').each(function () {
1899                     if (Sql.getFieldName($(g.t), $(this)) === fieldName) {
1900                         newCell = this;
1901                     }
1902                 });
1903             }
1905             if (newCell) {
1906                 g.hideEditCell(false, false, false, { move : true, cell : newCell });
1907             }
1908         },
1910         /**
1911          * Move currently Editing Cell to Left
1912          *
1913          * @param e
1914          *
1915          */
1916         moveLeft: function (e) {
1917             e.preventDefault();
1919             var $thisField = $(g.currentEditCell);
1920             var fieldName = Sql.getFieldName($(g.t), $thisField);
1922             var whereClause = $thisField.parents('tr').first().find('.where_clause').val();
1923             if (typeof whereClause === 'undefined') {
1924                 whereClause = '';
1925             }
1926             var found = false;
1927             var $foundRow;
1928             $thisField.parents('tr').first().parents('tbody').children().each(function () {
1929                 if ($(this).find('.where_clause').val() === whereClause) {
1930                     found = true;
1931                     $foundRow = $(this);
1932                 }
1933             });
1935             var leftCell;
1936             var cellFound = false;
1937             if (found) {
1938                 $foundRow.children('td.grid_edit').each(function () {
1939                     if (Sql.getFieldName($(g.t), $(this)) === fieldName) {
1940                         cellFound = true;
1941                     }
1942                     if (!cellFound) {
1943                         leftCell = this;
1944                     }
1945                 });
1946             }
1948             if (leftCell) {
1949                 g.hideEditCell(false, false, false, { move : true, cell : leftCell });
1950             }
1951         },
1953         /**
1954          * Move currently Editing Cell to Right
1955          *
1956          * @param e
1957          *
1958          */
1959         moveRight: function (e) {
1960             e.preventDefault();
1962             var $thisField = $(g.currentEditCell);
1963             var fieldName = Sql.getFieldName($(g.t), $thisField);
1965             var whereClause = $thisField.parents('tr').first().find('.where_clause').val();
1966             if (typeof whereClause === 'undefined') {
1967                 whereClause = '';
1968             }
1969             var found = false;
1970             var $foundRow;
1971             var j = 0;
1972             $thisField.parents('tr').first().parents('tbody').children().each(function () {
1973                 if ($(this).find('.where_clause').val() === whereClause) {
1974                     found = true;
1975                     $foundRow = $(this);
1976                 }
1977             });
1979             var rightCell;
1980             var cellFound = false;
1981             var nextCellFound = false;
1982             if (found) {
1983                 $foundRow.children('td.grid_edit').each(function () {
1984                     if (Sql.getFieldName($(g.t), $(this)) === fieldName) {
1985                         cellFound = true;
1986                     }
1987                     if (cellFound) {
1988                         if (j >= 1 && ! nextCellFound) {
1989                             rightCell = this;
1990                             nextCellFound = true;
1991                         } else {
1992                             j++;
1993                         }
1994                     }
1995                 });
1996             }
1998             if (rightCell) {
1999                 g.hideEditCell(false, false, false, { move : true, cell : rightCell });
2000             }
2001         },
2003         /**
2004          * Initialize grid editing feature.
2005          */
2006         initGridEdit: function () {
2007             function startGridEditing (e, cell) {
2008                 if (g.isCellEditActive) {
2009                     g.saveOrPostEditedCell();
2010                 } else {
2011                     g.showEditCell(cell);
2012                 }
2013                 e.stopPropagation();
2014             }
2016             function handleCtrlNavigation (e) {
2017                 if ((e.ctrlKey && e.which === 38) || (e.altKey && e.which === 38)) {
2018                     g.moveUp(e);
2019                 } else if ((e.ctrlKey && e.which === 40)  || (e.altKey && e.which === 40)) {
2020                     g.moveDown(e);
2021                 } else if ((e.ctrlKey && e.which === 37) || (e.altKey && e.which === 37)) {
2022                     g.moveLeft(e);
2023                 } else if ((e.ctrlKey && e.which === 39)  || (e.altKey && e.which === 39)) {
2024                     g.moveRight(e);
2025                 }
2026             }
2028             // create cell edit wrapper element
2029             g.cEditStd = document.createElement('div');
2030             g.cEdit = g.cEditStd;
2031             g.cEditTextarea = document.createElement('div');
2033             // adjust g.cEditStd
2034             g.cEditStd.className = 'cEdit';
2035             $(g.cEditStd).html('<input class="edit_box" rows="1"><div class="edit_area"></div>');
2036             $(g.cEditStd).hide();
2038             // adjust g.cEdit
2039             g.cEditTextarea.className = 'cEdit';
2040             $(g.cEditTextarea).html('<textarea class="edit_box" rows="1"></textarea><div class="edit_area"></div>');
2041             $(g.cEditTextarea).hide();
2043             // assign cell editing hint
2044             g.cellEditHint = Messages.strCellEditHint;
2045             g.saveCellWarning = Messages.strSaveCellWarning;
2046             g.alertNonUnique = Messages.strAlertNonUnique;
2047             g.gotoLinkText = Messages.strGoToLink;
2049             // initialize cell editing configuration
2050             g.saveCellsAtOnce = $(g.o).find('.save_cells_at_once').val();
2051             g.maxTruncatedLen = CommonParams.get('LimitChars');
2053             // register events
2054             $(g.t).find('td.data.click1')
2055                 .on('click', function (e) {
2056                     startGridEditing(e, this);
2057                     // prevent default action when clicking on "link" in a table
2058                     if ($(e.target).is('.grid_edit a')) {
2059                         e.preventDefault();
2060                     }
2061                 });
2063             $(g.t).find('td.data.click2')
2064                 .on('click', function (e) {
2065                     var $cell = $(this);
2066                     // In the case of relational link, We want single click on the link
2067                     // to goto the link and double click to start grid-editing.
2068                     var $link = $(e.target);
2069                     if ($link.is('.grid_edit.relation a')) {
2070                         e.preventDefault();
2071                         // get the click count and increase
2072                         var clicks = $cell.data('clicks');
2073                         clicks = (typeof clicks === 'undefined') ? 1 : clicks + 1;
2075                         if (clicks === 1) {
2076                             // if there are no previous clicks,
2077                             // start the single click timer
2078                             var timer = setTimeout(function () {
2079                                 // temporarily remove ajax class so the page loader will not handle it,
2080                                 // submit and then add it back
2081                                 $link.removeClass('ajax');
2082                                 AJAX.requestHandler.call($link[0]);
2083                                 $link.addClass('ajax');
2084                                 $cell.data('clicks', 0);
2085                             }, 700);
2086                             $cell.data('clicks', clicks);
2087                             $cell.data('timer', timer);
2088                         } else {// When double clicking a link, switch to edit mode
2089                             // this is a double click, cancel the single click timer
2090                             // and make the click count 0
2091                             clearTimeout($cell.data('timer'));
2092                             $cell.data('clicks', 0);
2093                             // start grid-editing
2094                             startGridEditing(e, this);
2095                         }
2096                     }
2097                 })
2098                 .on('dblclick', function (e) {
2099                     if ($(e.target).is('.grid_edit a')) {
2100                         e.preventDefault();
2101                     } else {
2102                         startGridEditing(e, this);
2103                     }
2104                 });
2106             $(g.cEditStd).on('keydown', 'input.edit_box, select', handleCtrlNavigation);
2108             $(g.cEditStd).find('.edit_box').on('focus', function () {
2109                 g.showEditArea();
2110             });
2111             $(g.cEditStd).on('keydown', '.edit_box, select', function (e) {
2112                 if (e.which === 13) {
2113                     // post on pressing "Enter"
2114                     e.preventDefault();
2115                     g.saveOrPostEditedCell();
2116                 }
2117             });
2118             $(g.cEditStd).on('keydown', function (e) {
2119                 if (!g.isEditCellTextEditable) {
2120                     // prevent text editing
2121                     e.preventDefault();
2122                 }
2123             });
2125             $(g.cEditTextarea).on('keydown', 'textarea.edit_box, select', handleCtrlNavigation);
2127             $(g.cEditTextarea).find('.edit_box').on('focus', function () {
2128                 g.showEditArea();
2129             });
2130             $(g.cEditTextarea).on('keydown', '.edit_box, select', function (e) {
2131                 if (e.which === 13 && !e.shiftKey) {
2132                     // post on pressing "Enter"
2133                     e.preventDefault();
2134                     g.saveOrPostEditedCell();
2135                 }
2136             });
2137             $(g.cEditTextarea).on('keydown', function (e) {
2138                 if (!g.isEditCellTextEditable) {
2139                     // prevent text editing
2140                     e.preventDefault();
2141                 }
2142             });
2143             $('html').on('click', function (e) {
2144                 // hide edit cell if the click is not fromDat edit area
2145                 if ($(e.target).parents().index($(g.cEdit)) === -1 &&
2146                     !$(e.target).parents('.ui-datepicker-header').length &&
2147                     !$('.browse_foreign_modal.ui-dialog:visible').length &&
2148                     !$(e.target).closest('.dismissable').length
2149                 ) {
2150                     g.hideEditCell();
2151                 }
2152             }).on('keydown', function (e) {
2153                 if (e.which === 27 && g.isCellEditActive) {
2154                     // cancel on pressing "Esc"
2155                     g.hideEditCell(true);
2156                 }
2157             });
2158             $(g.o).find('div.save_edited').on('click', function () {
2159                 g.hideEditCell();
2160                 g.postEditedCell();
2161             });
2162             $(window).on('beforeunload', function () {
2163                 if (g.isCellEdited) {
2164                     return g.saveCellWarning;
2165                 }
2166             });
2168             // attach to global div
2169             $(g.gDiv).append(g.cEditStd);
2170             $(g.gDiv).append(g.cEditTextarea);
2172             // add hint for grid editing feature when hovering "Edit" link in each table row
2173             if (Messages.strGridEditFeatureHint !== undefined) {
2174                 Functions.tooltip(
2175                     $(g.t).find('.edit_row_anchor a'),
2176                     'a',
2177                     Messages.strGridEditFeatureHint
2178                 );
2179             }
2180         }
2181     };
2183     /** ****************
2184      * Initialize grid
2185      ******************/
2187     // wrap all truncated data cells with span indicating the original length
2188     // todo update the original length after a grid edit
2189     $(t).find('td.data.truncated:not(:has(span))')
2190         .filter(function () {
2191             return $(this).data('originallength') !== undefined;
2192         })
2193         .wrapInner(function () {
2194             return '<span title="' + Messages.strOriginalLength + ' ' +
2195                 $(this).data('originallength') + '"></span>';
2196         });
2198     // wrap remaining cells, except actions cell, with span
2199     $(t).find('th, td:not(:has(span))')
2200         .wrapInner('<span></span>');
2202     // create grid elements
2203     g.gDiv = document.createElement('div');     // create global div
2205     // initialize the table variable
2206     g.t = t;
2208     // enclosing .sqlqueryresults div
2209     g.o = $(t).parents('.sqlqueryresults');
2211     // get data columns in the first row of the table
2212     var $firstRowCols = $(t).find('tr').first().find('th.draggable');
2214     // initialize visible headers count
2215     g.visibleHeadersCount = $firstRowCols.filter(':visible').length;
2217     // assign first column (actions) span
2218     if (! $(t).find('tr').first().find('th').first().hasClass('draggable')) {  // action header exist
2219         g.actionSpan = $(t).find('tr').first().find('th').first().prop('colspan');
2220     } else {
2221         g.actionSpan = 0;
2222     }
2224     // assign table create time
2225     // table_create_time will only available if we are in "Browse" tab
2226     g.tableCreateTime = $(g.o).find('.table_create_time').val();
2228     // assign the hints
2229     g.sortHint = Messages.strSortHint;
2230     g.strMultiSortHint = Messages.strMultiSortHint;
2231     g.markHint = Messages.strColMarkHint;
2232     g.copyHint = Messages.strColNameCopyHint;
2234     // assign common hidden inputs
2235     var $commonHiddenInputs = $(g.o).find('div.common_hidden_inputs');
2236     g.server = $commonHiddenInputs.find('input[name=server]').val();
2237     g.db = $commonHiddenInputs.find('input[name=db]').val();
2238     g.table = $commonHiddenInputs.find('input[name=table]').val();
2240     // add table class
2241     $(t).addClass('pma_table');
2243     // add relative position to global div so that resize handlers are correctly positioned
2244     $(g.gDiv).css('position', 'relative');
2246     // link the global div
2247     $(t).before(g.gDiv);
2248     $(g.gDiv).append(t);
2250     // FEATURES
2251     if (isResizeEnabled) {
2252         g.initColResize();
2253     }
2254     // disable reordering for result from EXPLAIN or SHOW syntax, which do not have a table navigation panel
2255     if (isReorderEnabled &&
2256         $(g.o).find('table.navigation').length > 0) {
2257         g.initColReorder();
2258     }
2259     if (isVisibEnabled) {
2260         g.initColVisib();
2261     }
2262     // make sure we have the ajax class
2263     if (isGridEditEnabled &&
2264         $(t).is('.ajax')) {
2265         g.initGridEdit();
2266     }
2268     // create tooltip for each <th> with draggable class
2269     Functions.tooltip(
2270         $(t).find('th.draggable'),
2271         'th',
2272         g.updateHint()
2273     );
2275     // register events for hint tooltip (anchors inside draggable th)
2276     $(t).find('th.draggable a')
2277         .on('mouseenter', function () {
2278             g.showSortHint = true;
2279             g.showMultiSortHint = true;
2280             $(t).find('th.draggable').uiTooltip('option', {
2281                 content: g.updateHint()
2282             });
2283         })
2284         .on('mouseleave', function () {
2285             g.showSortHint = false;
2286             g.showMultiSortHint = false;
2287             $(t).find('th.draggable').uiTooltip('option', {
2288                 content: g.updateHint()
2289             });
2290         });
2292     // register events for dragging-related feature
2293     if (isResizeEnabled || isReorderEnabled) {
2294         $(document).on('mousemove', function (e) {
2295             g.dragMove(e);
2296         });
2297         $(document).on('mouseup', function (e) {
2298             $(g.o).removeClass('turnOffSelect');
2299             g.dragEnd(e);
2300         });
2301     }
2303     // some adjustment
2304     $(t).removeClass('data');
2305     $(g.gDiv).addClass('data');
2309  * jQuery plugin to cancel selection in HTML code.
2310  */
2311 (function ($) {
2312     $.fn.noSelect = function (p) { // no select plugin by Paulo P.Marinas
2313         var prevent = (p === null) ? true : p;
2314         /* eslint-disable compat/compat */
2315         var isMsie = navigator.userAgent.indexOf('MSIE') > -1 || !!window.navigator.userAgent.match(/Trident.*rv:11\./);
2316         var isFirefox = navigator.userAgent.indexOf('Firefox') > -1;
2317         var isSafari = navigator.userAgent.indexOf('Safari') > -1;
2318         var isOpera = navigator.userAgent.indexOf('Presto') > -1;
2319         /* eslint-enable compat/compat */
2320         if (prevent) {
2321             return this.each(function () {
2322                 if (isMsie || isSafari) {
2323                     $(this).on('selectstart', false);
2324                 } else if (isFirefox) {
2325                     $(this).css('MozUserSelect', 'none');
2326                     $('body').trigger('focus');
2327                 } else if (isOpera) {
2328                     $(this).on('mousedown', false);
2329                 } else {
2330                     $(this).attr('unselectable', 'on');
2331                 }
2332             });
2333         } else {
2334             return this.each(function () {
2335                 if (isMsie || isSafari) {
2336                     $(this).off('selectstart');
2337                 } else if (isFirefox) {
2338                     $(this).css('MozUserSelect', 'inherit');
2339                 } else if (isOpera) {
2340                     $(this).off('mousedown');
2341                 } else {
2342                     $(this).removeAttr('unselectable');
2343                 }
2344             });
2345         }
2346     }; // end noSelect
2347 }(jQuery));