pgadmin4/web/pgadmin/static/js/keyboard_shortcuts.js
Yosry Muhammad 710d520631 Add support for editing of resultsets in the Query Tool, if the data can be identified as updatable. Fixes #1760
When a query is run in the Query Tool, check if the source of the columns
can be identified as being from a single table, and that we have all
columns that make up the primary key. If so, consider the resultset to
be editable and allow the user to edit data and add/remove rows in the
grid. Changes to data are saved using SAVEPOINTs as part of any
transaction that's in progress, and rolled back if there are integrity
violations, without otherwise affecting the ongoing transaction.

Implemented by Yosry Muhammad as a Google Summer of Code project.
2019-07-17 11:45:20 +01:00

337 lines
12 KiB
JavaScript

//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2019, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import $ from 'jquery';
import gettext from 'sources/gettext';
import { getMod } from 'sources/utils';
const PERIOD_KEY = 190,
FWD_SLASH_KEY = 191,
ESC_KEY = 27,
LEFT_KEY = 37,
UP_KEY = 38,
RIGHT_KEY = 39,
DOWN_KEY = 40;
function isMac() {
return window.navigator.platform.search('Mac') != -1;
}
function isKeyCtrlAlt(event) {
return event.ctrlKey || event.altKey;
}
function isKeyAltShift(event) {
return event.altKey || event.shiftKey;
}
function isKeyCtrlShift(event) {
return event.ctrlKey || event.shiftKey;
}
function isKeyCtrlAltShift(event) {
return event.ctrlKey || event.altKey || event.shiftKey;
}
function isAltShiftBoth(event) {
return event.altKey && event.shiftKey && !event.ctrlKey;
}
function isCtrlShiftBoth(event) {
return event.ctrlKey && event.shiftKey && !event.altKey;
}
function isCtrlAltBoth(event) {
return event.ctrlKey && event.altKey && !event.shiftKey;
}
/* Returns the key of shortcut */
function shortcut_key(shortcut) {
let key = '';
if(shortcut['key'] && shortcut['key']['char']) {
key = shortcut['key']['char'].toUpperCase();
}
return key;
}
/* Converts shortcut object to title representation
* Shortcut object is browser.get_preference().value
*/
function shortcut_title(title, shortcut) {
let text_representation = '';
if (typeof shortcut === 'undefined' || shortcut === null) {
return text_representation;
}
if(shortcut['alt']) {
text_representation = gettext('Alt') + '+';
}
if(shortcut['shift']) {
text_representation += gettext('Shift') + '+';
}
if(shortcut['control']) {
text_representation += gettext('Ctrl') + '+';
}
text_representation += shortcut_key(shortcut);
return gettext('%(title)s (%(text_representation)s)',{
'title': title,
'text_representation': text_representation,
});
}
/* Returns the key char of shortcut
* shortcut object is browser.get_preference().value
*/
function shortcut_accesskey_title(title, shortcut) {
return gettext('%(title)s (accesskey + %(key)s)',{
'title': title,
'key': shortcut_key(shortcut),
});
}
function _stopEventPropagation(event) {
event.cancelBubble = true;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
/* Function use to validate shortcut keys */
function validateShortcutKeys(user_defined_shortcut, event) {
if(!user_defined_shortcut) {
return false;
}
let keyCode = event.which || event.keyCode;
return user_defined_shortcut.alt == event.altKey &&
user_defined_shortcut.shift == event.shiftKey &&
user_defined_shortcut.control == event.ctrlKey &&
user_defined_shortcut.key.key_code == keyCode;
}
// Finds the desired panel on which user wants to navigate to
function focusDockerPanel(docker, op) {
if(!docker) {
return;
}
// If no frame in focus, focus the first one
if(!docker._focusFrame) {
if(docker._frameList.length == 0 && docker._frameList[0]._panelList.length == 0) {
return;
}
docker._frameList[0]._panelList[docker._frameList[0]._curTab].focus();
}
let focus_frame = docker._focusFrame,
focus_id = 0,
flash = false;
// Mod is used to cycle the op
if (op == 'switch') {
let i = 0, total_frames = docker._frameList.length;
for(i = 0; i < total_frames; i++) {
if(focus_frame === docker._frameList[i]) break;
}
focus_frame = docker._frameList[getMod(i+1,total_frames)];
focus_id = focus_frame._curTab;
flash = true;
} else if (op == 'left') {
focus_id = getMod(focus_frame._curTab-1, focus_frame._panelList.length);
flash = false;
} else if (op == 'right') {
focus_id = getMod(focus_frame._curTab+1, focus_frame._panelList.length);
flash = false;
}
let focus_panel = focus_frame._panelList[focus_id];
focus_panel.$container.find('*[tabindex]:not([tabindex="-1"])').trigger('focus');
focus_panel.focus(flash);
return focus_panel._type;
}
/* Debugger: Keyboard Shortcuts handling */
function keyboardShortcutsDebugger($el, event, preferences, docker) {
let panel_type = '', panel_content, $input;
if(this.validateShortcutKeys(preferences.edit_grid_values, event)) {
this._stopEventPropagation(event);
panel_content = $el.find(
'div.wcPanelTabContent:not(".wcPanelTabContentHidden")'
);
if(panel_content.length) {
$input = $(panel_content).find('td.editable:first');
if($input.length)
$input.trigger('click');
}
} else if(this.validateShortcutKeys(preferences.move_next, event)) {
this._stopEventPropagation(event);
panel_type = focusDockerPanel(docker, 'right');
} else if(this.validateShortcutKeys(preferences.move_previous, event)) {
this._stopEventPropagation(event);
panel_type = focusDockerPanel(docker, 'left');
} else if(this.validateShortcutKeys(preferences.switch_panel, event)) {
this._stopEventPropagation(event);
panel_type = focusDockerPanel(docker, 'switch');
}
return panel_type;
}
/* Query tool: Keyboard Shortcuts handling */
function keyboardShortcutsQueryTool(
sqlEditorController, queryToolActions, event, docker
) {
if (sqlEditorController.isQueryRunning()) {
return;
}
let keyCode = event.which || event.keyCode, panel_type = '';
let executeKeys = sqlEditorController.preferences.execute_query;
let explainKeys = sqlEditorController.preferences.explain_query;
let explainAnalyzeKeys = sqlEditorController.preferences.explain_analyze_query;
let downloadCsvKeys = sqlEditorController.preferences.download_csv;
let nextTabKeys = sqlEditorController.preferences.move_next;
let previousTabKeys = sqlEditorController.preferences.move_previous;
let switchPanelKeys = sqlEditorController.preferences.switch_panel;
let toggleCaseKeys = sqlEditorController.preferences.toggle_case;
let commitKeys = sqlEditorController.preferences.commit_transaction;
let rollbackKeys = sqlEditorController.preferences.rollback_transaction;
let saveDataKeys = sqlEditorController.preferences.save_data;
if (this.validateShortcutKeys(executeKeys, event)) {
this._stopEventPropagation(event);
queryToolActions.executeQuery(sqlEditorController);
} else if (this.validateShortcutKeys(explainKeys, event)) {
this._stopEventPropagation(event);
queryToolActions.explain(sqlEditorController);
} else if (this.validateShortcutKeys(explainAnalyzeKeys, event)) {
this._stopEventPropagation(event);
queryToolActions.explainAnalyze(sqlEditorController);
} else if (this.validateShortcutKeys(downloadCsvKeys, event)) {
this._stopEventPropagation(event);
queryToolActions.download(sqlEditorController);
} else if (this.validateShortcutKeys(toggleCaseKeys, event)) {
this._stopEventPropagation(event);
queryToolActions.toggleCaseOfSelectedText(sqlEditorController);
} else if (this.validateShortcutKeys(commitKeys, event)) {
// If transaction buttons are disabled then no need to execute commit.
if (!sqlEditorController.is_transaction_buttons_disabled) {
this._stopEventPropagation(event);
queryToolActions.executeCommit(sqlEditorController);
}
} else if (this.validateShortcutKeys(rollbackKeys, event)) {
// If transaction buttons are disabled then no need to execute rollback.
if (!sqlEditorController.is_transaction_buttons_disabled) {
this._stopEventPropagation(event);
queryToolActions.executeRollback(sqlEditorController);
}
} else if (this.validateShortcutKeys(saveDataKeys, event)) {
this._stopEventPropagation(event);
queryToolActions.saveDataChanges(sqlEditorController);
} else if ((
(this.isMac() && event.metaKey) ||
(!this.isMac() && event.ctrlKey)
) && !event.altKey && event.shiftKey && keyCode === FWD_SLASH_KEY) {
this._stopEventPropagation(event);
queryToolActions.commentBlockCode(sqlEditorController);
} else if ((
(this.isMac() && !this.isKeyCtrlAltShift(event) && event.metaKey) ||
(!this.isMac() && !this.isKeyAltShift(event) && event.ctrlKey)
) && keyCode === FWD_SLASH_KEY) {
this._stopEventPropagation(event);
queryToolActions.commentLineCode(sqlEditorController);
} else if ((
(this.isMac() && !this.isKeyCtrlAltShift(event) && event.metaKey) ||
(!this.isMac() && !this.isKeyAltShift(event) && event.ctrlKey)
) && keyCode === PERIOD_KEY) {
this._stopEventPropagation(event);
queryToolActions.uncommentLineCode(sqlEditorController);
} else if (keyCode == ESC_KEY) {
queryToolActions.focusOut(sqlEditorController);
/*Apply only for sub-dropdown*/
if($(event.target).hasClass('dropdown-item')
&& $(event.target).closest('.dropdown-submenu').length > 0) {
$(event.target).closest('.dropdown-submenu').find('.dropdown-menu').removeClass('show');
}
} else if(this.validateShortcutKeys(nextTabKeys, event)) {
this._stopEventPropagation(event);
panel_type = focusDockerPanel(docker, 'right');
} else if(this.validateShortcutKeys(previousTabKeys, event)) {
this._stopEventPropagation(event);
panel_type = focusDockerPanel(docker, 'left');
} else if(this.validateShortcutKeys(switchPanelKeys, event)) {
this._stopEventPropagation(event);
panel_type = focusDockerPanel(docker, 'switch');
} else if(keyCode === UP_KEY || keyCode === DOWN_KEY) {
/*Apply only for dropdown*/
if($(event.target).closest('.dropdown-menu').length > 0) {
this._stopEventPropagation(event);
let currLi = $(event.target).closest('li');
/*close all the submenus on movement*/
$(event.target).closest('.dropdown-menu').find('.show').removeClass('show');
if(keyCode === UP_KEY) {
currLi = currLi.prev();
}
else if(keyCode === DOWN_KEY){
currLi = currLi.next();
}
/*do not focus on divider, disabled and d-none */
while(currLi.hasClass('dropdown-divider')
|| currLi.hasClass('d-none')
|| currLi.find('.dropdown-item').first().hasClass('disabled')) {
if(keyCode === UP_KEY) {
currLi = currLi.prev();
}
else if(keyCode === DOWN_KEY){
currLi = currLi.next();
}
}
currLi.find('.dropdown-item').trigger('focus');
}
} else if(keyCode === LEFT_KEY || keyCode === RIGHT_KEY) {
/*Apply only for dropdown*/
if($(event.target).closest('.dropdown-menu').length > 0) {
this._stopEventPropagation(event);
let currLi = $(event.target).closest('li');
if(keyCode === RIGHT_KEY) {
/*open submenu if any*/
if(currLi.hasClass('dropdown-submenu')){
currLi.find('.dropdown-menu').addClass('show');
currLi = currLi.find('.dropdown-menu .dropdown-item').first().trigger('focus');
}
} else if(keyCode === LEFT_KEY) {
/*close submenu*/
let currMenu = currLi.closest('.dropdown-menu');
if(currMenu.closest('.dropdown-submenu').length > 0) {
currMenu.removeClass('show');
currLi = currMenu.closest('.dropdown-submenu');
currLi.find('.dropdown-item').trigger('focus');
}
}
}
}
return panel_type;
}
export {
keyboardShortcutsDebugger as processEventDebugger,
keyboardShortcutsQueryTool as processEventQueryTool,
focusDockerPanel, validateShortcutKeys,
_stopEventPropagation, isMac, isKeyCtrlAlt, isKeyAltShift, isKeyCtrlShift,
isKeyCtrlAltShift, isAltShiftBoth, isCtrlShiftBoth, isCtrlAltBoth,
shortcut_key, shortcut_title, shortcut_accesskey_title,
};