import $ from 'jquery'; import * as bootstrap from 'bootstrap'; import { AJAX } from './modules/ajax.ts'; import { addDatepicker, confirmLink, copyToClipboard, getCellValue, stringifyJSON, toggleDatepickerIfInvalid, updateCode, userAgent } from './modules/functions.ts'; import { CommonParams } from './modules/common.ts'; import highlightSql from './modules/sql-highlight.ts'; import { ajaxShowMessage } from './modules/ajax-message.ts'; import { escapeHtml } from './modules/functions/escape.ts'; /** * Create advanced table (resize, reorder, and show/hide columns; and also grid editing). * This function is designed mainly for table DOM generated from browsing a table in the database. * For using this function in other table DOM, you may need to: * - add "draggable" class in the table header
').parent().html(); const tools = $resultQuery.find('.tools').wrap('
').parent().html();
// sqlOuter and tools will not be present if 'Show SQL queries' configuration is off
if (typeof sqlOuter !== 'undefined' && typeof tools !== 'undefined') {
$(g.o).find('.result_query').not($(g.o).find('.result_query').last()).remove();
const $existingQuery = $(g.o).find('.result_query');
// If two query box exists update query in second else add a second box
if ($existingQuery.find('div.sqlOuter').length > 1) {
$existingQuery.children().eq(3).remove();
$existingQuery.children().eq(3).remove();
$existingQuery.append(sqlOuter + tools);
} else {
$existingQuery.append(sqlOuter + tools);
}
highlightSql($existingQuery);
}
}
// hide and/or update the successfully saved cells
g.hideEditCell(true, data);
// remove the "Save edited cells" button
$(g.o).find('div.save_edited').hide();
// update saved fields
$(g.t).find('.to_be_saved')
.removeClass('to_be_saved')
.data('value', null)
.data('original_data', null);
g.isCellEdited = false;
} else {
ajaxShowMessage(data.error, false);
if (! g.saveCellsAtOnce) {
$(g.t).find('.to_be_saved')
.removeClass('to_be_saved');
}
}
},
}).done(function () {
if (options !== undefined && options.move) {
g.showEditCell(options.cell);
}
}); // end $.ajax()
},
/**
* Save edited cell, so it can be posted later.
*
* @return {boolean}
*/
saveEditedCell: function () {
/**
* @var $thisField Object referring to the td that is being edited
*/
const $thisField = $(g.currentEditCell);
let $testElement = null; // to test the presence of a element
let needToPost = false;
/**
* @var fieldName String containing the name of this field.
* @see window.Sql.getFieldName()
*/
const fieldName = window.Sql.getFieldName($(g.t), $thisField);
/**
* @var thisFieldParams Array temporary storage for the name/value of current field
*/
const thisFieldParams = {};
/**
* @var isNull String capturing whether 'checkbox_null_ to which the grid_edit should move
*/
saveOrPostEditedCell: function (options = undefined) {
const saved = g.saveEditedCell();
// Check if $cfg['SaveCellsAtOnce'] is false
if (! g.saveCellsAtOnce) {
// Check if need_to_post is true
if (saved) {
// Check if this function called from 'move' functions
if (options !== undefined && options.move) {
g.postEditedCell(options);
} else {
g.postEditedCell();
}
// need_to_post is false
} else {
// Check if this function called from 'move' functions
if (options !== undefined && options.move) {
g.hideEditCell(true);
g.showEditCell(options.cell);
// NOT called from 'move' functions
} else {
g.hideEditCell(true);
}
}
// $cfg['SaveCellsAtOnce'] is true
} else {
// If need_to_post
if (saved) {
// If this function called from 'move' functions
if (options !== undefined && options.move) {
g.hideEditCell(true, true, false, options);
g.showEditCell(options.cell);
// NOT called from 'move' functions
} else {
g.hideEditCell(true, true);
}
} else {
// If this function called from 'move' functions
if (options !== undefined && options.move) {
g.hideEditCell(true, false, false, options);
g.showEditCell(options.cell);
// NOT called from 'move' functions
} else {
g.hideEditCell(true);
}
}
}
},
/**
* Initialize column resize feature.
*/
initColResize: function () {
// create column resizer div
g.cRsz = document.createElement('div');
g.cRsz.className = 'cRsz';
// get data columns in the first row of the table
const $firstRowCols = $(g.t).find('tr').first().find('th.draggable');
// create column borders
$firstRowCols.each(function () {
const cb = document.createElement('div'); // column border
$(cb).addClass('colborder')
.on('mousedown', function (e) {
g.dragStartRsz(e, this);
});
$(g.cRsz).append(cb);
});
g.reposRsz();
// attach to global div
$(g.gDiv).prepend(g.cRsz);
},
/**
* Initialize column reordering feature.
*/
initColReorder: function () {
g.cCpy = document.createElement('div'); // column copy, to store copy of dragged column header
g.cPointer = document.createElement('div'); // column pointer, used when reordering column
// adjust g.cCpy
g.cCpy.className = 'cCpy';
$(g.cCpy).hide();
// adjust g.cPointer
g.cPointer.className = 'cPointer';
$(g.cPointer).css('visibility', 'hidden'); // set visibility to hidden instead of calling hide() to force browsers to cache the image in cPointer class
// assign column reordering hint
g.reorderHint = window.Messages.strColOrderHint;
// get data columns in the first row of the table
const $firstRowCols = $(g.t).find('tr').first().find('th.draggable');
// initialize column order
const $colOrder = $(g.o).find('.col_order'); // check if column order is passed from PHP
let i;
if ($colOrder.length > 0) {
g.colOrder = ($colOrder.val() as string).split(',');
for (i = 0; i < g.colOrder.length; i++) {
g.colOrder[i] = parseInt(g.colOrder[i], 10);
}
} else {
g.colOrder = [];
for (i = 0; i < $firstRowCols.length; i++) {
g.colOrder.push(i);
}
}
// register events
$(g.t).find('th.draggable')
.on('mousedown', function (e) {
$(g.o).addClass('turnOffSelect');
if (g.visibleHeadersCount > 1) {
g.dragStartReorder(e, this);
}
})
.on('mouseenter', function () {
if (g.visibleHeadersCount > 1) {
$(this).css('cursor', 'move');
} else {
$(this).css('cursor', 'inherit');
}
})
.on('mouseleave', function () {
g.showReorderHint = false;
bootstrap.Tooltip.getOrCreateInstance(this, { title: g.updateHint(), html: true })
.setContent({ '.tooltip-inner': g.updateHint() });
})
.on('dblclick', function (e) {
e.preventDefault();
const res = copyToClipboard($(this).data('column'));
if (res) {
ajaxShowMessage(window.Messages.strCopyColumnSuccess, false, 'success');
} else {
ajaxShowMessage(window.Messages.strCopyColumnFailure, false, 'error');
}
});
$(g.t).find('th.draggable a')
.on('dblclick', function (e) {
e.stopPropagation();
});
// restore column order when the restore button is clicked
$(g.o).find('div.restore_column').on('click', function () {
g.restoreColOrder();
});
// attach to global div
$(g.gDiv).append(g.cPointer);
$(g.gDiv).append(g.cCpy);
// prevent default "dragstart" event when dragging a link
$(g.t).find('th a').on('dragstart', function () {
return false;
});
// refresh the restore column button state
g.refreshRestoreButton();
},
/**
* Initialize column visibility feature.
*/
initColVisib: function () {
g.cDrop = document.createElement('div'); // column drop-down arrows
g.cList = document.createElement('div'); // column visibility list
// adjust g.cDrop
g.cDrop.className = 'cDrop';
// adjust g.cList
g.cList.className = 'cList';
$(g.cList).hide();
// assign column visibility related hints
g.showAllColText = window.Messages.strShowAllCol;
// get data columns in the first row of the table
const $firstRowCols = $(g.t).find('tr').first().find('th.draggable');
let i;
// initialize column visibility
const $colVisib = $(g.o).find('.col_visib'); // check if column visibility is passed from PHP
if ($colVisib.length > 0) {
g.colVisib = ($colVisib.val() as string).split(',');
for (i = 0; i < g.colVisib.length; i++) {
g.colVisib[i] = parseInt(g.colVisib[i], 10);
}
} else {
g.colVisib = [];
for (i = 0; i < $firstRowCols.length; i++) {
g.colVisib.push(1);
}
}
// make sure we have more than one column
if ($firstRowCols.length > 1) {
const colVisibTh = g.t.querySelectorAll('th:not(.draggable)');
const $colVisibTh = $(colVisibTh).slice(0, 1);
colVisibTh.forEach((tableHeader: HTMLElement) => {
bootstrap.Tooltip.getOrCreateInstance(tableHeader, { title: window.Messages.strColVisibHint })
.setContent({ '.tooltip-inner': window.Messages.strColVisibHint });
});
// create column visibility drop-down arrow(s)
$colVisibTh.each(function () {
const cd = document.createElement('div'); // column drop-down arrow
$(cd).addClass('coldrop')
.on('click', function () {
if (g.cList.style.display === 'none') {
g.showColList(this);
} else {
g.hideColList();
}
});
$(g.cDrop).append(cd);
});
// add column visibility control
g.cList.innerHTML = '';
const $listDiv = $(g.cList).find('div');
const tempClick = function () {
if (g.toggleCol($(this).index())) {
g.afterToggleCol();
}
};
for (i = 0; i < $firstRowCols.length; i++) {
const currHeader = $firstRowCols[i];
const listElmt = document.createElement('div');
$(listElmt).text($(currHeader).text())
.prepend('');
$listDiv.append(listElmt);
// add event on click
$(listElmt).on('click', tempClick);
}
// add "show all column" button
const showAll = document.createElement('div');
$(showAll).addClass('showAllColBtn')
.text(g.showAllColText);
$(g.cList).append(showAll);
$(showAll).on('click', function () {
g.showAllColumns();
});
// prepend "show all column" button at top if the list is too long
if ($firstRowCols.length > 10) {
const clone = showAll.cloneNode(true);
// @ts-ignore
$(g.cList).prepend(clone);
$(clone).on('click', function () {
g.showAllColumns();
});
}
}
// hide column visibility list if we move outside the list
$(g.t).find('td, th.draggable').on('mouseenter', function () {
g.hideColList();
});
// attach to first row first col of the grid
const thFirst = $(g.t).find('th.d-print-none');
$(thFirst).append(g.cDrop);
$(thFirst).append(g.cList);
// some adjustment
g.reposDrop();
},
/**
* Move currently Editing Cell to Up
*
* @param e
*
*/
moveUp: function (e) {
e.preventDefault();
const $thisField = $(g.currentEditCell);
const fieldName = window.Sql.getFieldName($(g.t), $thisField);
let whereClause = $thisField.parents('tr').first().find('.where_clause').val();
if (typeof whereClause === 'undefined') {
whereClause = '';
}
let found = false;
let $prevRow;
$thisField.parents('tr').first().parents('tbody').children().each(function () {
if ($(this).find('.where_clause').val() === whereClause) {
found = true;
}
if (! found) {
$prevRow = $(this);
}
});
let newCell;
if (found && $prevRow) {
$prevRow.children('td').each(function () {
if (window.Sql.getFieldName($(g.t), $(this)) === fieldName) {
newCell = this;
}
});
}
if (newCell) {
g.hideEditCell(false, false, false, { move: true, cell: newCell });
}
},
/**
* Move currently Editing Cell to Down
*
* @param e
*
*/
moveDown: function (e) {
e.preventDefault();
const $thisField = $(g.currentEditCell);
const fieldName = window.Sql.getFieldName($(g.t), $thisField);
let whereClause = $thisField.parents('tr').first().find('.where_clause').val();
if (typeof whereClause === 'undefined') {
whereClause = '';
}
let found = false;
let $nextRow;
let j = 0;
let nextRowFound = false;
$thisField.parents('tr').first().parents('tbody').children().each(function () {
if ($(this).find('.where_clause').val() === whereClause) {
found = true;
}
if (found) {
if (j >= 1 && ! nextRowFound) {
$nextRow = $(this);
nextRowFound = true;
} else {
j++;
}
}
});
let newCell;
if (found && $nextRow) {
$nextRow.children('td').each(function () {
if (window.Sql.getFieldName($(g.t), $(this)) === fieldName) {
newCell = this;
}
});
}
if (newCell) {
g.hideEditCell(false, false, false, { move: true, cell: newCell });
}
},
/**
* Move currently Editing Cell to Left
*
* @param e
*
*/
moveLeft: function (e) {
e.preventDefault();
const $thisField = $(g.currentEditCell);
const fieldName = window.Sql.getFieldName($(g.t), $thisField);
let whereClause = $thisField.parents('tr').first().find('.where_clause').val();
if (typeof whereClause === 'undefined') {
whereClause = '';
}
let found = false;
let $foundRow;
$thisField.parents('tr').first().parents('tbody').children().each(function () {
if ($(this).find('.where_clause').val() === whereClause) {
found = true;
$foundRow = $(this);
}
});
let leftCell;
let cellFound = false;
if (found) {
$foundRow.children('td.grid_edit').each(function () {
if (window.Sql.getFieldName($(g.t), $(this)) === fieldName) {
cellFound = true;
}
if (! cellFound) {
leftCell = this;
}
});
}
if (leftCell) {
g.hideEditCell(false, false, false, { move: true, cell: leftCell });
}
},
/**
* Move currently Editing Cell to Right
*
* @param e
*
*/
moveRight: function (e) {
e.preventDefault();
const $thisField = $(g.currentEditCell);
const fieldName = window.Sql.getFieldName($(g.t), $thisField);
let whereClause = $thisField.parents('tr').first().find('.where_clause').val();
if (typeof whereClause === 'undefined') {
whereClause = '';
}
let found = false;
let $foundRow;
let j = 0;
$thisField.parents('tr').first().parents('tbody').children().each(function () {
if ($(this).find('.where_clause').val() === whereClause) {
found = true;
$foundRow = $(this);
}
});
let rightCell;
let cellFound = false;
let nextCellFound = false;
if (found) {
$foundRow.children('td.grid_edit').each(function () {
if (window.Sql.getFieldName($(g.t), $(this)) === fieldName) {
cellFound = true;
}
if (cellFound) {
if (j >= 1 && ! nextCellFound) {
rightCell = this;
nextCellFound = true;
} else {
j++;
}
}
});
}
if (rightCell) {
g.hideEditCell(false, false, false, { move: true, cell: rightCell });
}
},
/**
* Initialize grid editing feature.
*/
initGridEdit: function () {
function startGridEditing (e, cell) {
if (g.isCellEditActive) {
g.saveOrPostEditedCell();
} else {
g.showEditCell(cell);
}
e.stopPropagation();
}
function handleCtrlNavigation (e) {
if ((e.ctrlKey && e.which === 38) || (e.altKey && e.which === 38)) {
g.moveUp(e);
} else if ((e.ctrlKey && e.which === 40) || (e.altKey && e.which === 40)) {
g.moveDown(e);
} else if ((e.ctrlKey && e.which === 37) || (e.altKey && e.which === 37)) {
g.moveLeft(e);
} else if ((e.ctrlKey && e.which === 39) || (e.altKey && e.which === 39)) {
g.moveRight(e);
}
}
// create cell edit wrapper element
g.cEditStd = document.createElement('div');
g.cEdit = g.cEditStd;
g.cEditTextarea = document.createElement('div');
// adjust g.cEditStd
g.cEditStd.className = 'cEdit';
$(g.cEditStd).html('');
$(g.cEditStd).hide();
// adjust g.cEdit
g.cEditTextarea.className = 'cEdit';
$(g.cEditTextarea).html('');
$(g.cEditTextarea).hide();
// assign cell editing hint
g.cellEditHint = window.Messages.strCellEditHint;
g.saveCellWarning = window.Messages.strSaveCellWarning;
g.alertNonUnique = window.Messages.strAlertNonUnique;
g.gotoLinkText = window.Messages.strGoToLink;
// initialize cell editing configuration
g.saveCellsAtOnce = $(g.o).find('.save_cells_at_once').val();
g.maxTruncatedLen = CommonParams.get('LimitChars');
// register events
$(g.t).find('td.data.click1')
.on('click', function (e) {
startGridEditing(e, this);
// prevent default action when clicking on "link" in a table
if ($(e.target).is('.grid_edit a')) {
e.preventDefault();
}
});
$(g.t)
.on('click', 'td.data.click2', function (e) {
const $cell = $(this);
// In the case of relational link, We want single click on the link
// to goto the link and double click to start grid-editing.
const $link = $(e.target);
if ($link.is('.grid_edit.relation a')) {
e.preventDefault();
// get the click count and increase
let clicks = $cell.data('clicks');
clicks = (typeof clicks === 'undefined') ? 1 : clicks + 1;
if (clicks === 1) {
// if there are no previous clicks,
// start the single click timer
const timer = setTimeout(function () {
// temporarily remove ajax class so the page loader will not handle it,
// submit and then add it back
$link.removeClass('ajax');
AJAX.requestHandler.call($link[0]);
$link.addClass('ajax');
$cell.data('clicks', 0);
}, 700);
$cell.data('clicks', clicks);
$cell.data('timer', timer);
} else {// When double clicking a link, switch to edit mode
// this is a double click, cancel the single click timer
// and make the click count 0
clearTimeout($cell.data('timer'));
$cell.data('clicks', 0);
// start grid-editing
startGridEditing(e, this);
}
}
})
.on('dblclick', 'td.data.click2', function (e) {
if ($(e.target).is('.grid_edit a')) {
e.preventDefault();
} else {
startGridEditing(e, this);
}
});
$(g.cEditStd).on('keydown', 'input.edit_box, select', handleCtrlNavigation);
$(g.cEditStd).find('.edit_box').on('focus', function () {
g.showEditArea();
});
$(g.cEditStd).on('keydown', '.edit_box, select', function (e) {
if (e.which === 13) {
// post on pressing "Enter"
e.preventDefault();
g.saveOrPostEditedCell();
}
});
$(g.cEditStd).on('keydown', function (e) {
if (! g.isEditCellTextEditable) {
// prevent text editing
e.preventDefault();
}
});
$(g.cEditTextarea).on('keydown', 'textarea.edit_box, select', handleCtrlNavigation);
$(g.cEditTextarea).find('.edit_box').on('focus', function () {
g.showEditArea();
});
$(g.cEditTextarea).on('keydown', '.edit_box, select', function (e) {
if (e.which === 13 && ! e.shiftKey) {
// post on pressing "Enter"
e.preventDefault();
g.saveOrPostEditedCell();
}
});
$(g.cEditTextarea).on('keydown', function (e) {
if (! g.isEditCellTextEditable) {
// prevent text editing
e.preventDefault();
}
});
$('html').on('click', function (e) {
// hide edit cell if the click is not fromDat edit area
if ($(e.target).parents().index($(g.cEdit)) === -1 &&
! $(e.target).parents('.ui-datepicker-header').length &&
! $('#browseForeignModal').length &&
! $(e.target).closest('.dismissable').length
) {
g.hideEditCell();
}
}).on('keydown', function (e) {
if (e.which === 27 && g.isCellEditActive) {
// cancel on pressing "Esc"
g.hideEditCell(true);
}
});
$(g.o).find('div.save_edited').on('click', function () {
g.hideEditCell();
g.postEditedCell();
});
$(window).on('beforeunload', function () {
if (g.isCellEdited) {
return g.saveCellWarning;
}
});
// attach to global div
$(g.gDiv).append(g.cEditStd);
$(g.gDiv).append(g.cEditTextarea);
// add hint for grid editing feature when hovering "Edit" link in each table row
g.t.querySelectorAll('.edit_row_anchor').forEach((editRowAnchor: HTMLElement) => {
if (editRowAnchor.dataset.gridEditConfig === 'disabled') {
return;
}
bootstrap.Tooltip.getOrCreateInstance(editRowAnchor.querySelector('a'));
});
},
/**
* Initialize cell selection feature (square selection).
*/
initCellSelection: function () {
g.isSelectingCells = false;
g.startSelectCell = null;
g.preEndSelectCell = null;
g.endSelectCell = null;
g.selectedColumns = new Set();
g.selectedRows = new Set();
g.renderedSelectedCells = new Set();
const colspan = Number(
$(g.t).find('thead th').first().attr('colspan'),
) - 1 || 0;
const selectingClass = 'cell-selected';
let keyboardEventTimestamp = 0;
// Check if an element is visible for user
function isPartiallyHidden (el: HTMLElement) {
const rect = el.getBoundingClientRect();
const vW = window.innerWidth;
const vH = window.innerHeight;
if (rect.top < 0 || rect.left < 0 || rect.bottom > vH || rect.right > vW) {
return true;
}
const points = [
{ x: rect.left + 1, y: rect.top + 1 },
{ x: rect.right - 1, y: rect.top + 1 },
{ x: rect.right - 1, y: rect.bottom - 1 },
{ x: rect.left + 1, y: rect.bottom - 1 },
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
];
for (let i = 0; i < 5; i++) {
const p = points[i];
const topEl = document.elementFromPoint(p.x, p.y);
if (topEl && !$(topEl).hasClass('tooltip-inner') && !el.contains(topEl) && topEl !== el) {
return true;
}
}
return false;
}
function scrollDuringSelection () {
const { endSelectCell, preEndSelectCell } = g;
if (!endSelectCell || !preEndSelectCell || !isPartiallyHidden(endSelectCell)) {
return;
}
const $end = $(endSelectCell);
const $preEnd = $(preEndSelectCell);
const isHeader = $end.is('th');
const endIdx = $end.index() + (isHeader ? colspan : 0);
const endRowIdx = $end.parent().index();
const preIdx = $preEnd.index() + (isHeader ? colspan : 0);
const preRowIdx = $preEnd.parent().index();
let direction = null;
if (endRowIdx < preRowIdx) {
direction = 'up';
} else if (endRowIdx > preRowIdx) {
direction = 'down';
} else if (endIdx < preIdx) {
direction = 'left';
} else if (endIdx > preIdx) {
direction = 'right';
}
if (direction) {
endSelectCell.scrollIntoView({
block: 'nearest',
inline: 'nearest',
behavior: 'auto',
});
const extra = 50;
const dw = endSelectCell.offsetWidth + extra;
const dh = endSelectCell.offsetHeight + extra;
const offsets = {
up: [0, -dh],
down: [0, dh],
left: [-dw, 0],
right: [dw, 0],
};
const [scrollX, scrollY] = offsets[direction];
window.scrollBy(scrollX, scrollY);
}
}
function renderCellSelection () {
g.renderedSelectedCells.clear();
$(g.t).find(`.${selectingClass}`).removeClass(selectingClass).removeClass('with-bg-color');
if (g.selectedColumns.size === 0) {
return;
}
g.selectedRows.forEach((rowIndex: number) => {
g.selectedColumns.forEach((cellIndex: number) => {
g.renderedSelectedCells.add(`${rowIndex}-${cellIndex}`);
const columns = rowIndex === -1 ?
$(g.t).find('thead tr').eq(0).find('th').eq(cellIndex - colspan) :
$(g.t).find('tbody tr').eq(rowIndex).find('td').eq(cellIndex);
columns.addClass((g.selectedColumns.size === 1 && g.selectedRows.size === 1) ? selectingClass : `${selectingClass} with-bg-color`);
});
});
}
function updateCellSelection (cell: HTMLElement) {
const $cell = $(cell);
if (!$cell) {
return;
}
const cellIndex = $cell.index();
const rowIndex = $cell.parent().index();
const key = `${rowIndex}-${cellIndex}`;
if (g.renderedSelectedCells.has(key)) {
g.selectedRows.delete(rowIndex);
g.selectedColumns.delete(cellIndex);
g.renderedSelectedCells.delete(key);
} else {
g.selectedRows.add(rowIndex);
g.selectedColumns.add(cellIndex);
}
renderCellSelection();
}
function selectCell (cell: HTMLElement) {
g.preEndSelectCell = g.endSelectCell;
g.endSelectCell = cell;
updateCellSelection(cell);
scrollDuringSelection();
}
function resetCellSelection () {
g.isSelectingCells = false;
g.startSelectCell = null;
g.endSelectCell = null;
g.selectedColumns.clear();
g.selectedRows.clear();
g.renderedSelectedCells.clear();
renderCellSelection();
}
// Event to reset selection on Escape key press
$(document).on('keydown', function (e) {
if (e.key === 'Escape') {
resetCellSelection();
}
});
// Keyboard events for cell selection
$(document).on('keydown', function (e) {
if (document.activeElement && $(document.activeElement).is('input, textarea, select')) {
return; // do not interfere with input fields
}
// ctrl + A to select all cells
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyA') {
e.preventDefault();
window.getSelection().removeAllRanges();
// Select all cells with header
$(g.t).find('tbody > tr, thead > tr').each(function () {
$(this).find('td.data, th:not(.column_action)').addClass(`${selectingClass} with-bg-color`);
});
}
// add throttle to avoid multiple events firing too quickly
if (keyboardEventTimestamp && (Date.now() - keyboardEventTimestamp) < 100) {
return;
}
keyboardEventTimestamp = Date.now();
// arrow + shift to select cells
if (e.shiftKey && g.endSelectCell) {
const allowedSelectorToSelect = '.column_heading, .data';
const lookupNextCell = {
ArrowUp: () => {
if ($(g.endSelectCell).is('th')) {
return null;
}
const rowElement = $(g.endSelectCell).closest('tr');
if (rowElement.index() === 0) {
g.selectedRows.add(-1);
return $(g.t).find('thead > tr').eq(0).find('th.bg-body').eq($(g.endSelectCell).index() - colspan).get(0);
}
const nextElement = rowElement.prev().find('td, th').eq($(g.endSelectCell).index());
if (nextElement.hasClass(selectingClass)) {
g.selectedRows.delete(rowElement.index());
} else {
g.selectedRows.add(rowElement.prev().index());
}
return nextElement.get(0);
},
ArrowDown: () => {
const rowElement = $(g.endSelectCell).closest('tr');
if ($(g.endSelectCell).is('th')) {
g.selectedRows.delete(-1);
return $(g.t).find('tbody > tr').eq(0).find('td').eq($(g.endSelectCell).index() + colspan).get(0);
}
const nextElement = rowElement.next().find('td, th').eq($(g.endSelectCell).index());
if (rowElement.next().index() < 0) {
return null;
}
if (nextElement.hasClass(selectingClass)) {
g.selectedRows.delete(rowElement.index());
} else {
g.selectedRows.add(rowElement.next().index());
}
return nextElement.get(0);
},
ArrowLeft: () => {
const nextElement = $(g.endSelectCell).prev(allowedSelectorToSelect);
if (!nextElement || nextElement.index() < 0) {
return null;
}
const isHeader = nextElement.is('th');
if (nextElement.hasClass(selectingClass)) {
g.selectedColumns.delete($(g.endSelectCell).index() + (isHeader ? colspan : 0));
} else {
g.selectedColumns.add(nextElement.index() + (isHeader ? colspan : 0));
}
return nextElement.get(0);
},
ArrowRight: () => {
const nextElement = $(g.endSelectCell).next(allowedSelectorToSelect);
if (!nextElement || nextElement.index() < 0) {
return null;
}
const isHeader = nextElement.is('th');
if (nextElement.hasClass(selectingClass)) {
g.selectedColumns.delete($(g.endSelectCell).index() + (isHeader ? colspan : 0));
} else {
g.selectedColumns.add(nextElement.index() + (isHeader ? colspan : 0));
}
return nextElement.get(0);
},
};
if (Object.keys(lookupNextCell).includes(e.key)) {
e.preventDefault();
const nextCell = lookupNextCell[e.key]();
if (nextCell) {
g.preEndSelectCell = g.endSelectCell;
g.endSelectCell = nextCell;
renderCellSelection();
scrollDuringSelection();
}
}
}
});
// Reset selection when clicking outside the table
$(document).on('mousedown', function (e) {
if (!$(e.target).closest(g.t).length) {
resetCellSelection();
}
});
$(g.t).on('mousedown', 'td.data', function (e) {
let isMultiselect = null;
// Ignore if clicking on link/input/etc or right click
if (e.which !== 1 || $(e.target).is('a, input, select, textarea, .edit_box')) {
return;
}
if (!e.ctrlKey && !e.metaKey) {
$(g.t).find(`.${selectingClass}`).removeClass(`${selectingClass} with-bg-color`);
resetCellSelection();
isMultiselect = false;
} else {
isMultiselect = true;
}
g.startSelectCell = this;
g.isSelectingCells = true;
selectCell(this);
$(g.t).on('mouseleave.cellSelect', 'td.data', function () {
window.getSelection().removeAllRanges();
});
// Dynamic mouseover for drag
$(g.t).on('mouseover.cellSelect', 'td.data, thead th:not(.column_action)', function (e) {
if (!g.isSelectingCells || g.endSelectCell === this) {
return;
}
const isHeader = $(this).is('th');
const colIndex = $(this).index() + (isHeader ? colspan : 0);
const rowIndex = $(this).parent().index() + (isHeader ? -1 : 0);
const startColIndex = $(g.startSelectCell).index();
const startRowIndex = $(g.startSelectCell).parent().index();
const minCol = Math.min(colIndex, startColIndex);
const maxCol = Math.max(colIndex, startColIndex);
const minRow = Math.min(rowIndex, startRowIndex);
const maxRow = Math.max(rowIndex, startRowIndex);
if (!isMultiselect) {
g.selectedRows.clear();
g.selectedColumns.clear();
}
for (let r = minRow; r <= maxRow; r++) {
g.selectedRows.add(r);
}
for (let c = minCol; c <= maxCol; c++) {
g.selectedColumns.add(c);
}
g.preEndSelectCell = g.endSelectCell;
g.endSelectCell = this;
scrollDuringSelection();
renderCellSelection();
});
// One-time mouseup on document to stop selection
$(document).on('mouseup.cellSelect', function () {
g.isSelectingCells = false;
$(g.t).off('mouseover.cellSelect');
$(g.t).off('mouseleave.cellSelect');
$(document).off('mouseup.cellSelect');
});
});
// Copy handler
$(document).on('copy', function (e) {
const selection = window.getSelection();
if (!document.body.contains(g.t) || g.isCellEditActive || (selection.rangeCount > 0 && !selection.isCollapsed)) {
return;
}
if ($(g.t).find(`.${selectingClass}`).length > 0) {
let selectionText = '';
const headers: string[] = [];
$(g.t).find('thead th.cell-selected').each(function () {
headers.push(
$(this).find('a')[0].childNodes[0].nodeValue.trim(),
);
});
if (headers.length > 0) {
selectionText += headers.join('\t') + '\n';
}
const rows: string[] = [];
$(g.t).find('tbody tr').each(function () {
const rowCells: string[] = [];
$(this).find('td.cell-selected').each(function () {
rowCells.push($(this).text().trim());
});
if (rowCells.length > 0) {
rows.push(rowCells.join('\t'));
}
});
selectionText += rows.join('\n');
const ev = e.originalEvent as ClipboardEvent;
if (ev?.clipboardData) {
ev.clipboardData.setData('text/plain', selectionText);
e.preventDefault();
}
}
});
},
};
/** ****************
* Initialize grid
******************/
// wrap all truncated data cells with span indicating the original length
// todo update the original length after a grid edit
$(t).find('td.data.truncated:not(:has(>span))')
.filter(function () {
return $(this).data('originallength') !== undefined;
})
.wrapInner(function () {
return '';
});
// wrap remaining cells, except actions cell, with span
$(t).find('th, td:not(:has(>span))')
.wrapInner('');
// create grid elements
g.gDiv = document.createElement('div'); // create global div
// initialize the table variable
g.t = t;
// enclosing .sqlqueryresults div
g.o = $(t).parents('.sqlqueryresults');
// get data columns in the first row of the table
const $firstRowCols = $(t).find('tr').first().find('th.draggable');
// initialize visible headers count
g.visibleHeadersCount = $firstRowCols.filter(':visible').length;
// assign first column (actions) span
if (! $(t).find('tr').first().find('th').first().hasClass('draggable')) { // action header exist
g.actionSpan = $(t).find('tr').first().find('th').first().prop('colspan');
} else {
g.actionSpan = 0;
}
// assign table create time
// table_create_time will only available if we are in "Browse" tab
g.tableCreateTime = $(g.o).find('.table_create_time').val();
// assign the hints
g.sortHint = window.Messages.strSortHint;
g.strMultiSortHint = window.Messages.strMultiSortHint;
g.markHint = window.Messages.strColMarkHint;
g.copyHint = window.Messages.strColNameCopyHint;
// assign common hidden inputs
const $commonHiddenInputs = $(g.o).find('div.common_hidden_inputs');
g.server = $commonHiddenInputs.find('input[name=server]').val();
g.db = $commonHiddenInputs.find('input[name=db]').val();
g.table = $commonHiddenInputs.find('input[name=table]').val();
// add table class
$(t).addClass('pma_table');
// add relative position to global div so that resize handlers are correctly positioned
$(g.gDiv).css('position', 'relative');
// link the global div
$(t).before(g.gDiv);
$(g.gDiv).append(t);
// FEATURES
if (isResizeEnabled) {
g.initColResize();
}
// disable reordering for result from EXPLAIN or SHOW syntax, which do not have a table navigation panel
if (isReorderEnabled &&
$(g.o).find('table.navigation').length > 0) {
g.initColReorder();
}
if (isVisibEnabled) {
g.initColVisib();
}
// make sure we have the ajax class
if (isGridEditEnabled &&
$(t).is('.ajax')) {
g.initGridEdit();
}
// create tooltip for each with draggable class
t.querySelectorAll('th.draggable').forEach((tableHeader: HTMLElement) => {
bootstrap.Tooltip.getOrCreateInstance(tableHeader, { title: g.updateHint(), html: true })
.setContent({ '.tooltip-inner': g.updateHint() });
});
// register events for hint tooltip (anchors inside draggable th)
$(t).find('th.draggable a')
.on('mouseenter', function () {
g.showSortHint = true;
g.showMultiSortHint = true;
t.querySelectorAll('th.draggable').forEach((tableHeader: HTMLElement) => {
bootstrap.Tooltip.getOrCreateInstance(tableHeader, { title: g.updateHint(), html: true })
.setContent({ '.tooltip-inner': g.updateHint() });
});
})
.on('mouseleave', function () {
g.showSortHint = false;
g.showMultiSortHint = false;
t.querySelectorAll('th.draggable').forEach((tableHeader: HTMLElement) => {
bootstrap.Tooltip.getOrCreateInstance(tableHeader, { title: g.updateHint(), html: true })
.setContent({ '.tooltip-inner': g.updateHint() });
});
});
// register events for dragging-related feature
if (isResizeEnabled || isReorderEnabled) {
$(document).on('mousemove', function (e) {
g.dragMove(e);
});
$(document).on('mouseup', function (e) {
$(g.o).removeClass('turnOffSelect');
g.dragEnd(e);
});
}
// some adjustment
$(t).removeClass('data');
$(g.gDiv).addClass('data');
/* Store the grid controller instance on the table element so it can be accessed later by other modules
(e.g. during AJAX teardown) without exposing the grid object as a global variable.*/
$(t).data('pmaGrid', g);
g.initCellSelection();
};
declare global {
interface Window {
makeGrid: typeof makeGrid;
}
}
window.makeGrid = makeGrid;