1 /* global firstDayOfCalendar */ // templates/javascript/variables.twig
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
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
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;
33 * Variables, assigned with default value, changed later
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,
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
82 * Start to resize column. Called when clicking on column separator.
85 * @param obj dragged div object
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');
94 objLeft: $(obj).position().left,
95 objWidth: $(g.t).find('th.draggable:visible').eq(n).find('span').outerWidth()
97 // eslint-disable-next-line compat/compat
98 $(document.body).css('cursor', 'col-resize').noSelect();
99 if (g.isCellEditActive) {
105 * Start to reorder column. Called when clicking on table header.
108 * @param obj table header object
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();
115 top: objPos.top + 20,
117 height: $(obj).height(),
118 width: $(obj).width()
124 // get the column index, zero-based
125 var n = g.getHeaderIdx(obj);
137 // eslint-disable-next-line compat/compat
138 $(document.body).css('cursor', 'move').noSelect();
139 if (g.isCellEditActive) {
145 * Handle mousemove event when dragging.
149 dragMove: function (e) {
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');
156 } else if (g.colReorder) {
157 // dragged column animation
158 dx = e.pageX - g.colReorder.x0;
160 .css('left', g.colReorder.objLeft + dx)
164 var hoveredCol = g.getHoveredCol(e);
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 ?
173 colPos.left + $(hoveredCol).outerWidth();
177 visibility: 'visible'
180 // no movement to other column, hide the column pointer
181 $(g.cPointer).css('visibility', 'hidden');
188 * Stop the dragging action.
192 dragEnd: function (e) {
194 var dx = e.pageX - g.colRsz.x0;
195 var nw = g.colRsz.objWidth + dx;
196 if (nw < g.minColWidth) {
206 $(g.cRsz).find('div').removeClass('colborder_active');
207 } else if (g.colReorder) {
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) {
220 g.refreshRestoreButton();
223 // animate new column position
224 $(g.cCpy).stop(true, true)
226 top: g.colReorder.objTop,
227 left: g.colReorder.objLeft
230 $(g.cPointer).css('visibility', 'hidden');
232 g.colReorder = false;
234 // eslint-disable-next-line compat/compat
235 $(document.body).css('cursor', 'inherit').noSelect(false);
239 * Resize column n to new width "nw"
241 * @param n zero-based column index
242 * @param nw new width of the column in pixel
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'))
253 * Reposition column resize bars.
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]);
263 if (navigator.userAgent.toLowerCase().indexOf('safari') !== -1) {
264 colWidth = $col.outerWidth();
266 colWidth = $col.outerWidth(true);
268 $($resizeHandles[n]).css('left', $col.position().left + colWidth)
270 if ($col.hasClass('condition')) {
271 $($resizeHandles[n]).addClass('condition');
273 $($resizeHandles[n - 1]).addClass('condition');
277 if ($($resizeHandles[0]).hasClass('condition')) {
278 $(g.t).find('thead th').first().addClass('before-condition');
280 $(g.cRsz).css('height', $(g.t).height());
284 * Shift column from index oldn to newn.
286 * @param oldn old zero-based column index
287 * @param newn new zero-based column index
289 shiftCol: function (oldn, newn) {
290 $(g.t).find('tr').each(function () {
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)));
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)));
303 // reposition the column resize bars
306 // adjust the column visibility list
308 $(g.cList).find('.lDiv div').eq(newn)
309 .before($(g.cList).find('.lDiv div').eq(oldn));
311 $(g.cList).find('.lDiv div').eq(newn)
312 .after($(g.cList).find('.lDiv div').eq(oldn));
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);
327 * Find currently hovered table column's header (excluding actions column).
330 * @return {object|undefined} the hovered column's th object or undefined if no hovered column found.
332 getHoveredCol: function (e) {
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) {
346 * Get a zero-based index from a <th class="draggable"> tag in a table.
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)
351 getHeaderIdx: function (obj) {
352 return $(obj).parents('tr').find('th.draggable').index(obj);
356 * Reposition the columns back to normal order.
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];
363 while (j >= 0 && x < g.colOrder[j]) {
367 g.shiftCol(i, j + 1);
370 if (g.tableCreateTime) {
371 // send request to server to remember the column order
374 g.refreshRestoreButton();
378 * Send column preferences (column order and visibility) to the server.
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
388 'ajax_request': true,
393 'table_create_time': g.tableCreateTime
395 if (g.colOrder.length > 0) {
396 $.extend(postParams, { 'col_order': g.colOrder.toString() });
398 if (g.colVisib.length > 0) {
399 $.extend(postParams, { 'col_visib': g.colVisib.toString() });
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);
413 * Refresh restore button state.
414 * Make restore button disabled if the table is similar with initial state.
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) {
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();
431 $(g.o).find('div.restore_column').show();
436 * Update current hint using the boolean values (showReorderHint, showSortHint, etc.).
441 updateHint: function () {
443 if (!g.colRsz && !g.colReorder) { // if not resizing or dragging
444 if (g.visibleHeadersCount > 1) {
445 g.showReorderHint = true;
447 if ($(t).find('th.marker').length > 0) {
448 g.showMarkHint = true;
450 if (g.showSortHint && g.sortHint) {
451 text += text.length > 0 ? '<br>' : '';
452 text += '- ' + g.sortHint;
454 if (g.showMultiSortHint && g.strMultiSortHint) {
455 text += text.length > 0 ? '<br>' : '';
456 text += '- ' + g.strMultiSortHint;
458 if (g.showMarkHint &&
460 ! g.showSortHint && // we do not show mark hint, when sort hint is shown
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;
476 * Toggle column's visibility.
477 * After calling this function and it returns true, afterToggleCol() must be called.
481 * @return {boolean} True if the column is toggled successfully.
483 toggleCol: function (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))
493 $(g.cList).find('.lDiv div').eq(n).find('input').prop('checked', false);
495 // cannot hide, force the checkbox to stay checked
496 $(g.cList).find('.lDiv div').eq(n).find('input').prop('checked', true);
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))
506 $(g.cList).find('.lDiv div').eq(n).find('input').prop('checked', true);
512 * This must be called if toggleCol() returns is true.
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().
517 afterToggleCol: function () {
518 // some adjustments after hiding column
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);
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);
541 * Show columns' visibility list.
543 * @param obj The drop down arrow of column visibility list
545 showColList: function (obj) {
546 // only show when not resizing or reordering
547 if (!g.colRsz && !g.colReorder) {
548 var pos = $(obj).position();
550 top: pos.top + $(obj).outerHeight(true)
553 $(obj).addClass('coldrop-hover');
558 * Hide columns' visibility list.
560 hideColList: function () {
562 $(g.cDrop).find('.coldrop-hover').removeClass('coldrop-hover');
566 * Reposition the column visibility drop-down arrow.
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();
574 left: pos.left + $($th[i]).width() - $cd.width(),
581 * Show all hidden columns.
583 showAllColumns: function () {
584 for (var i = 0; i < g.colVisib.length; i++) {
585 if (!g.colVisib[i]) {
593 * Show edit cell, if it can be shown
595 * @param cell <td> element to be edited
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');
604 if ($(cell).is('.grid_edit') &&
605 !g.colRsz && !g.colReorder) {
606 if (!g.isCellEditActive) {
609 if ('string' === $cell.attr('data-type') ||
610 'blob' === $cell.attr('data-type') ||
611 'json' === $cell.attr('data-type')
613 g.cEdit = g.cEditTextarea;
615 g.cEdit = g.cEditStd;
618 // remove all edit area and hide it
619 $(g.cEdit).find('.edit_area').empty().hide();
620 // reposition the cEdit element
622 top: $cell.position().top,
623 left: $cell.position().left
628 width: $cell.outerWidth(),
629 height: $cell.outerHeight()
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);
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);
645 function moveCursorToEnd (input) {
646 var originalValue = input.val();
647 var originallength = originalValue.length;
649 input.trigger('blur').trigger('focus').val(originalValue);
650 input[0].setSelectionRange(originallength, originallength);
655 * Remove edit cell and the edit area, if it is shown.
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
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);
671 g.saveOrPostEditedCell();
676 // cancel any previous request
677 if (g.lastXHR !== null) {
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;
688 $thisField.find('span').html('NULL');
689 $thisField.addClass('null');
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) + '...';
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('.'));
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));
719 var selector = 'span';
720 if ($thisField.hasClass('hex') && $thisField.find('a').length) {
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);
730 if ($thisField.is('.bit')) {
731 $thisField.find('span').text($thisField.data('value'));
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);
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);
752 // hide the cell editing area
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');
771 * Show drop-down edit area when edit cell is focused.
773 showEditArea: function () {
774 if (!g.isCellEditActive) { // make sure the edit area has not been shown
775 g.isCellEditActive = true;
776 g.isEditCellTextEditable = false;
778 * @var $td current edited cell
780 var $td = $(g.currentEditCell);
782 * @var $editArea the editing area
784 var $editArea = $(g.cEdit).find('.edit_area');
786 * @var whereClause WHERE clause for the edited cell
788 var whereClause = $td.parent('tr').find('.where_clause').val();
790 * @var fieldName String containing the name of this field.
791 * @see Sql.getFieldName()
793 var fieldName = Sql.getFieldName($(t), $td);
795 * @var relationCurrValue String current value of the field (for fields that are foreign keyed).
797 var relationCurrValue = $td.text();
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).
802 var relationKeyOrDisplayColumn = $td.find('a').attr('title');
804 * @var currValue String current value of the field (for fields that are of type enum or set).
806 var currValue = $td.find('span').text();
808 // empty all edit area, then rebuild it based on $td classes
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);
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
830 $checkbox.prop('checked', true);
831 g.wasEditedCellNull = true;
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);
839 } else if ($td.is('.relation')) {
840 $editArea.on('change', 'select', function () {
841 $checkbox.prop('checked', false);
843 $editArea.on('click', '.browse_foreign', function () {
844 $checkbox.prop('checked', false);
847 $(g.cEdit).on('keypress change paste', '.edit_box', function () {
848 if ($(this).val() !== '') {
849 $checkbox.prop('checked', false);
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);
858 $editArea.on('keydown', 'textarea', function () {
859 $checkbox.prop('checked', false);
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);
867 $checkbox.prop('checked', true);
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);
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('');
885 $editArea.find('textarea').val('');
887 $(g.cEdit).find('.edit_box').val('');
891 // reset the position of the edit_area div after closing datetime picker
892 $(g.cEdit).find('.edit_area').css({ 'top' :'0','position':'' });
895 if ($td.is('.relation')) {
897 $editArea.addClass('edit_area_loading');
899 // initialize the original data
900 $td.data('original_data', null);
903 * @var postParams Object containing parameters for the POST request
906 'ajax_request' : true,
910 'column' : fieldName,
911 'curr_value' : relationCurrValue,
912 'relation_key_or_display_column' : relationKeyOrDisplayColumn
915 g.lastXHR = $.post('index.php?route=/sql/get-relational-values', postParams, function (data) {
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);
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());
939 $editArea.on('change', 'select', function () {
940 $(g.cEdit).find('.edit_box').val($(this).val());
942 g.isEditCellTextEditable = true;
943 } else if ($td.is('.enum')) {
944 // handle enum fields
945 $editArea.addClass('edit_area_loading');
948 * @var postParams Object containing parameters for the POST request
951 'ajax_request' : true,
955 'column' : fieldName,
956 'curr_value' : currValue
958 g.lastXHR = $.post('index.php?route=/sql/get-enum-values', postParams, function (data) {
960 if (typeof data === 'object' && data.success === false) {
961 Functions.ajaxShowMessage(data.error, undefined, 'error');
964 $editArea.removeClass('edit_area_loading');
965 $editArea.append(data.dropdown);
966 $editArea.append('<div class="cell_edit_hint">' + g.cellEditHint + '</div>');
970 $editArea.on('change', 'select', function () {
971 $(g.cEdit).find('.edit_box').val($(this).val());
973 } else if ($td.is('.set')) {
975 $editArea.addClass('edit_area_loading');
977 // if the data is truncated, get the full data
978 if ($td.is('.truncated')) {
980 'ajax_request': true,
985 'curr_value': currValue,
986 'get_full_values': true,
987 'where_clause': whereClause
991 'ajax_request': true,
996 'curr_value': currValue
1000 g.lastXHR = $.post('index.php?route=/sql/get-set-values', postParams, function (data) {
1002 if (typeof data === 'object' && data.success === false) {
1003 Functions.ajaxShowMessage(data.error, undefined, 'error');
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>');
1013 $editArea.on('change', 'select', function () {
1014 $(g.cEdit).find('.edit_box').val($(this).val());
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);
1023 .on('keyup', 'textarea', function () {
1024 $(g.cEdit).find('.edit_box').val($(this).val());
1026 $(g.cEdit).on('keyup', '.edit_box', function () {
1027 $editArea.find('textarea').val($(this).val());
1029 $editArea.append('<div class="cell_edit_hint">' + g.cellEditHint + '</div>');
1031 // handle truncated/transformed values values
1032 $editArea.addClass('edit_area_loading');
1034 // initialize the original data
1035 $td.data('original_data', null);
1038 * @var sqlQuery String containing the SQL query used to retrieve value of truncated/transformed data
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,
1046 'ajax_request' : true,
1047 'sql_query' : sqlQuery,
1049 }, function (data) {
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);
1056 $td.data('original_data', data.value);
1057 $(g.cEdit).find('.edit_box').val(data.value);
1059 Functions.ajaxShowMessage(data.error, false);
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 += '.';
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);
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);
1100 // add datetime picker
1101 Functions.addDatepicker($inputField, $td.attr('data-type'), {
1102 showMillisec: showMillisec,
1103 showMicrosec: showMicrosec,
1104 timeFormat: timeFormat,
1105 firstDay: firstDayOfCalendar
1108 $inputField.on('keyup', function (e) {
1109 if (e.which === 13) {
1110 // post on pressing "Enter"
1112 e.stopPropagation();
1113 g.saveOrPostEditedCell();
1114 } else if (e.which !== 27) {
1115 Functions.toggleDatepickerIfInvalid($td, $inputField);
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();
1138 g.isEditCellTextEditable = true;
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>');
1146 if ($editArea.children().length > 0) {
1153 * Post the content of edited cell.
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
1158 postEditedCell: function (options) {
1164 * @var relationFields Array containing the name/value pairs of relational fields
1166 var relationFields = {};
1168 * @var relationalDisplay string 'K' if relational key, 'D' if relational display column
1170 var relationalDisplay = $(g.o).find('input[name=relational_display]:checked').val();
1172 * @var transformFields Array containing the name/value pairs for transformed fields
1174 var transformFields = {};
1176 * @var transformationFields Boolean, if there are any transformed fields in the edited cells
1178 var transformationFields = false;
1180 * @var fullSqlQuery String containing the complete SQL query to update this table
1182 var fullSqlQuery = '';
1184 * @var relFieldsList String, url encoded representation of {@link relations_fields}
1186 var relFieldsList = '';
1188 * @var transformFieldsList String, url encoded representation of {@link transformFields}
1190 var transformFieldsList = '';
1192 * @var fullWhereClause Array containing where clause for updated fields
1194 var fullWhereClause = [];
1196 * @var isUnique Boolean, whether the rows in this table is unique or not
1198 var isUnique = $(g.t).find('td.edit_row_anchor').is('.nonunique') ? 0 : 1;
1200 * multi edit variables
1202 var multiEditFieldsName = [];
1203 var multiEditFieldsType = [];
1204 var multiEditFields = [];
1205 var multiEditFieldsNull = [];
1207 // alert user if edited table is not unique
1209 alert(g.alertNonUnique);
1212 // loop each edited row
1213 $(g.t).find('td.to_be_saved').parents('tr').each(function () {
1215 var whereClause = $tr.find('.where_clause').val();
1216 if (typeof whereClause === 'undefined') {
1219 fullWhereClause.push(whereClause);
1220 var conditionArrayContent = $tr.find('.condition_array').val();
1221 if (typeof conditionArrayContent === 'undefined') {
1222 conditionArrayContent = '{}';
1224 var conditionArray = JSON.parse(conditionArrayContent);
1227 * multi edit variables, for current row
1228 * @TODO array indices are still not correct, they should be md5 of field's name
1230 var fieldsName = [];
1231 var fieldsType = [];
1233 var fieldsNull = [];
1235 // loop each edited cell in a row
1236 $tr.find('.to_be_saved').each(function () {
1238 * @var $thisField Object referring to the td that is being edited
1240 var $thisField = $(this);
1243 * @var fieldName String containing the name of this field.
1244 * @see Sql.getFieldName()
1246 var fieldName = Sql.getFieldName($(g.t), $thisField);
1249 * @var thisFieldParams Array temporary storage for the name/value of current field
1251 var thisFieldParams = {};
1253 if ($thisField.is('.transformed')) {
1254 transformationFields = true;
1256 thisFieldParams[fieldName] = $thisField.data('value');
1259 * @var isNull String capturing whether 'checkbox_null_<field_name>_<row_index>' is checked.
1261 var isNull = thisFieldParams[fieldName] === null;
1263 fieldsName.push(fieldName);
1266 fieldsNull.push('on');
1269 if ($thisField.is('.bit')) {
1270 fieldsType.push('bit');
1271 } else if ($thisField.hasClass('hex')) {
1272 fieldsType.push('hex');
1274 fieldsNull.push('');
1276 if ($thisField.attr('data-type') !== 'json') {
1277 fields.push($thisField.data('value'));
1279 const JSONString = Functions.stringifyJSON($thisField.data('value'));
1280 fields.push(JSONString);
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);
1289 } else if ($thisField.is('.relation')) {
1290 relationFields[cellIndex] = {};
1291 $.extend(relationFields[cellIndex], thisFieldParams);
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, '\'\'') + '\'';
1304 }); // end of loop for every edited cells in a row
1308 for (var field in conditionArray) {
1309 newClause += field + ' ' + conditionArray[field] + ' AND ';
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
1327 * @var postParams Object containing parameters for the POST request
1329 var postParams = { 'ajax_request' : true,
1330 'sql_query' : fullSqlQuery,
1331 'server' : g.server,
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'
1348 if (!g.saveCellsAtOnce) {
1349 $(g.cEdit).find('*').prop('disabled', true);
1350 $(g.cEdit).find('.edit_box').addClass('edit_box_posting');
1352 $(g.o).find('div.save_edited').addClass('saving_edited_data')
1353 .find('input').prop('disabled', true); // disable the save button
1358 url: 'index.php?route=/table/replace',
1363 if (!g.saveCellsAtOnce) {
1364 $(g.cEdit).find('*').prop('disabled', false);
1365 $(g.cEdit).find('.edit_box').removeClass('edit_box_posting');
1367 $(g.o).find('div.save_edited').removeClass('saving_edited_data')
1368 .find('input').prop('disabled', false); // enable the save button back
1370 if (typeof data !== 'undefined' && data.success === true) {
1371 if (typeof options === 'undefined' || ! options.move) {
1372 Functions.ajaxShowMessage(data.message);
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')
1391 .on('click', function () {
1392 return Functions.confirmLink(this, 'DELETE FROM `' + g.db + '`.`' + g.table + '` WHERE ' +
1393 decodedNewClause + (isUnique ? '' : ' LIMIT 1'));
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));
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);
1423 $existingQuery.append(sqlOuter + tools);
1425 Functions.highlightSql($existingQuery);
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;
1441 Functions.ajaxShowMessage(data.error, false);
1442 if (!g.saveCellsAtOnce) {
1443 $(g.t).find('.to_be_saved')
1444 .removeClass('to_be_saved');
1448 }).done(function () {
1449 if (options !== undefined && options.move) {
1450 g.showEditCell(options.cell);
1456 * Save edited cell, so it can be posted later.
1460 saveEditedCell: function () {
1462 * @var $thisField Object referring to the td that is being edited
1464 var $thisField = $(g.currentEditCell);
1465 var $testElement = ''; // to test the presence of a element
1467 var needToPost = false;
1470 * @var fieldName String containing the name of this field.
1471 * @see Sql.getFieldName()
1473 var fieldName = Sql.getFieldName($(g.t), $thisField);
1476 * @var thisFieldParams Array temporary storage for the name/value of current field
1478 var thisFieldParams = {};
1481 * @var isNull String capturing whether 'checkbox_null_<field_name>_<row_index>' is checked.
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
1488 } else if (isNull) {
1489 if (!g.wasEditedCellNull) {
1490 thisFieldParams[fieldName] = null;
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();
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();
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);
1515 thisFieldParams[fieldName] = $(g.cEdit).find('.edit_box').val();
1519 if ($thisField.attr('data-type') !== 'json') {
1520 isValueUpdated = thisFieldParams[fieldName] !== Functions.getCellValue(g.currentEditCell);
1522 const JSONString = Functions.stringifyJSON(thisFieldParams[fieldName]);
1523 isValueUpdated = JSONString !== JSON.stringify(JSON.parse(Functions.getCellValue(g.currentEditCell)));
1526 if (g.wasEditedCellNull || isValueUpdated) {
1532 $(g.currentEditCell).addClass('to_be_saved')
1533 .data('value', thisFieldParams[fieldName]);
1534 if (g.saveCellsAtOnce) {
1535 $(g.o).find('div.save_edited').show();
1537 g.isCellEdited = true;
1544 * Save or post currently edited cell, depending on the "saveCellsAtOnce" configuration.
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
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
1555 // Check if this function called from 'move' functions
1556 if (options !== undefined && options.move) {
1557 g.postEditedCell(options);
1561 // need_to_post is false
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
1569 g.hideEditCell(true);
1572 // $cfg['SaveCellsAtOnce'] is true
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
1582 g.hideEditCell(true, true);
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
1591 g.hideEditCell(true);
1598 * Initialize column resize feature.
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);
1615 $(g.cRsz).append(cb);
1619 // attach to global div
1620 $(g.gDiv).prepend(g.cRsz);
1624 * Initialize column reordering feature.
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
1631 g.cCpy.className = 'cCpy';
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
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);
1654 for (i = 0; i < $firstRowCols.length; i++) {
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);
1667 .on('mouseenter', function () {
1668 if (g.visibleHeadersCount > 1) {
1669 $(this).css('cursor', 'move');
1671 $(this).css('cursor', 'inherit');
1674 .on('mouseleave', function () {
1675 g.showReorderHint = false;
1676 $(this).uiTooltip('option', {
1677 content: g.updateHint()
1680 .on('dblclick', function (e) {
1682 var res = Functions.copyToClipboard($(this).data('column'));
1684 Functions.ajaxShowMessage(Messages.strCopyColumnSuccess, false, 'success');
1686 Functions.ajaxShowMessage(Messages.strCopyColumnFailure, false, 'error');
1689 $(g.t).find('th.draggable a')
1690 .on('dblclick', function (e) {
1691 e.stopPropagation();
1693 // restore column order when the restore button is clicked
1694 $(g.o).find('div.restore_column').on('click', function () {
1695 g.restoreColOrder();
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 () {
1707 // refresh the restore column button state
1708 g.refreshRestoreButton();
1712 * Initialize column visibility feature.
1714 initColVisib: function () {
1715 g.cDrop = document.createElement('div'); // column drop-down arrows
1716 g.cList = document.createElement('div'); // column visibility list
1719 g.cDrop.className = 'cDrop';
1722 g.cList.className = 'cList';
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');
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);
1741 for (i = 0; i < $firstRowCols.length; i++) {
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);
1752 Messages.strColVisibHint
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);
1766 $(g.cDrop).append(cd);
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())) {
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);
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 () {
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 () {
1806 // hide column visibility list if we move outside the list
1807 $(g.t).find('td, th.draggable').on('mouseenter', function () {
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);
1821 * Move currently Editing Cell to Up
1826 moveUp: function (e) {
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') {
1838 $thisField.parents('tr').first().parents('tbody').children().each(function () {
1839 if ($(this).find('.where_clause').val() === whereClause) {
1849 if (found && $prevRow) {
1850 $prevRow.children('td').each(function () {
1851 if (Sql.getFieldName($(g.t), $(this)) === fieldName) {
1858 g.hideEditCell(false, false, false, { move : true, cell : newCell });
1863 * Move currently Editing Cell to Down
1868 moveDown: function (e) {
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') {
1881 var nextRowFound = false;
1882 $thisField.parents('tr').first().parents('tbody').children().each(function () {
1883 if ($(this).find('.where_clause').val() === whereClause) {
1887 if (j >= 1 && ! nextRowFound) {
1889 nextRowFound = true;
1897 if (found && $nextRow) {
1898 $nextRow.children('td').each(function () {
1899 if (Sql.getFieldName($(g.t), $(this)) === fieldName) {
1906 g.hideEditCell(false, false, false, { move : true, cell : newCell });
1911 * Move currently Editing Cell to Left
1916 moveLeft: function (e) {
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') {
1928 $thisField.parents('tr').first().parents('tbody').children().each(function () {
1929 if ($(this).find('.where_clause').val() === whereClause) {
1931 $foundRow = $(this);
1936 var cellFound = false;
1938 $foundRow.children('td.grid_edit').each(function () {
1939 if (Sql.getFieldName($(g.t), $(this)) === fieldName) {
1949 g.hideEditCell(false, false, false, { move : true, cell : leftCell });
1954 * Move currently Editing Cell to Right
1959 moveRight: function (e) {
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') {
1972 $thisField.parents('tr').first().parents('tbody').children().each(function () {
1973 if ($(this).find('.where_clause').val() === whereClause) {
1975 $foundRow = $(this);
1980 var cellFound = false;
1981 var nextCellFound = false;
1983 $foundRow.children('td.grid_edit').each(function () {
1984 if (Sql.getFieldName($(g.t), $(this)) === fieldName) {
1988 if (j >= 1 && ! nextCellFound) {
1990 nextCellFound = true;
1999 g.hideEditCell(false, false, false, { move : true, cell : rightCell });
2004 * Initialize grid editing feature.
2006 initGridEdit: function () {
2007 function startGridEditing (e, cell) {
2008 if (g.isCellEditActive) {
2009 g.saveOrPostEditedCell();
2011 g.showEditCell(cell);
2013 e.stopPropagation();
2016 function handleCtrlNavigation (e) {
2017 if ((e.ctrlKey && e.which === 38) || (e.altKey && e.which === 38)) {
2019 } else if ((e.ctrlKey && e.which === 40) || (e.altKey && e.which === 40)) {
2021 } else if ((e.ctrlKey && e.which === 37) || (e.altKey && e.which === 37)) {
2023 } else if ((e.ctrlKey && e.which === 39) || (e.altKey && e.which === 39)) {
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();
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');
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')) {
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')) {
2071 // get the click count and increase
2072 var clicks = $cell.data('clicks');
2073 clicks = (typeof clicks === 'undefined') ? 1 : 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);
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);
2098 .on('dblclick', function (e) {
2099 if ($(e.target).is('.grid_edit a')) {
2102 startGridEditing(e, this);
2106 $(g.cEditStd).on('keydown', 'input.edit_box, select', handleCtrlNavigation);
2108 $(g.cEditStd).find('.edit_box').on('focus', function () {
2111 $(g.cEditStd).on('keydown', '.edit_box, select', function (e) {
2112 if (e.which === 13) {
2113 // post on pressing "Enter"
2115 g.saveOrPostEditedCell();
2118 $(g.cEditStd).on('keydown', function (e) {
2119 if (!g.isEditCellTextEditable) {
2120 // prevent text editing
2125 $(g.cEditTextarea).on('keydown', 'textarea.edit_box, select', handleCtrlNavigation);
2127 $(g.cEditTextarea).find('.edit_box').on('focus', function () {
2130 $(g.cEditTextarea).on('keydown', '.edit_box, select', function (e) {
2131 if (e.which === 13 && !e.shiftKey) {
2132 // post on pressing "Enter"
2134 g.saveOrPostEditedCell();
2137 $(g.cEditTextarea).on('keydown', function (e) {
2138 if (!g.isEditCellTextEditable) {
2139 // prevent text editing
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
2152 }).on('keydown', function (e) {
2153 if (e.which === 27 && g.isCellEditActive) {
2154 // cancel on pressing "Esc"
2155 g.hideEditCell(true);
2158 $(g.o).find('div.save_edited').on('click', function () {
2162 $(window).on('beforeunload', function () {
2163 if (g.isCellEdited) {
2164 return g.saveCellWarning;
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) {
2175 $(g.t).find('.edit_row_anchor a'),
2177 Messages.strGridEditFeatureHint
2183 /** ****************
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;
2193 .wrapInner(function () {
2194 return '<span title="' + Messages.strOriginalLength + ' ' +
2195 $(this).data('originallength') + '"></span>';
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
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');
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();
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();
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);
2251 if (isResizeEnabled) {
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) {
2259 if (isVisibEnabled) {
2262 // make sure we have the ajax class
2263 if (isGridEditEnabled &&
2268 // create tooltip for each <th> with draggable class
2270 $(t).find('th.draggable'),
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()
2284 .on('mouseleave', function () {
2285 g.showSortHint = false;
2286 g.showMultiSortHint = false;
2287 $(t).find('th.draggable').uiTooltip('option', {
2288 content: g.updateHint()
2292 // register events for dragging-related feature
2293 if (isResizeEnabled || isReorderEnabled) {
2294 $(document).on('mousemove', function (e) {
2297 $(document).on('mouseup', function (e) {
2298 $(g.o).removeClass('turnOffSelect');
2304 $(t).removeClass('data');
2305 $(g.gDiv).addClass('data');
2309 * jQuery plugin to cancel selection in HTML code.
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 */
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);
2330 $(this).attr('unselectable', 'on');
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');
2342 $(this).removeAttr('unselectable');