Allow keyboard navigation of all controls on subnode grids. Partially fixes #3919

This commit is contained in:
Ganesh Jaybhay
2019-07-11 10:14:01 +01:00
committed by Dave Page
parent 28585110dd
commit 4cbc1f2f59
9 changed files with 296 additions and 32 deletions

View File

@@ -64,6 +64,29 @@ Use the shortcuts below to navigate the tabsets on dialogs:
| Control+Shift+] | Dialog tab forward | | Control+Shift+] | Dialog tab forward |
+----------------------------+-------------------------------------------------------+ +----------------------------+-------------------------------------------------------+
Property Grid Controls
**********************
Use the shortcuts below when working with property grid controls:
.. table::
:class: longtable
:widths: 2 3
+----------------------------+-------------------------------------------------------+
| Shortcut for all platforms | Function |
+============================+=======================================================+
| Control+Shift+A | Add row in Grid |
+----------------------------+-------------------------------------------------------+
| Tab | Move focus to the next control |
+----------------------------+-------------------------------------------------------+
| Shift+Tab | Move focus to the previous control |
+----------------------------+-------------------------------------------------------+
| Return | Pick the selected an item in a combo box |
+----------------------------+-------------------------------------------------------+
| Control+Shift+A | Add row in Grid |
+----------------------------+-------------------------------------------------------+
SQL Editors SQL Editors
*********** ***********

View File

@@ -26,6 +26,7 @@ Housekeeping
Bug fixes Bug fixes
********* *********
| `Issue #3919 <https://redmine.postgresql.org/issues/3919>`_ - Allow keyboard navigation of all controls on subnode grids.
| `Issue #4224 <https://redmine.postgresql.org/issues/4224>`_ - Prevent flickering of large tooltips on the Graphical EXPLAIN canvas. | `Issue #4224 <https://redmine.postgresql.org/issues/4224>`_ - Prevent flickering of large tooltips on the Graphical EXPLAIN canvas.
| `Issue #4393 <https://redmine.postgresql.org/issues/4393>`_ - Ensure parameter values are quoted when needed when editing roles. | `Issue #4393 <https://redmine.postgresql.org/issues/4393>`_ - Ensure parameter values are quoted when needed when editing roles.
| `Issue #4395 <https://redmine.postgresql.org/issues/4395>`_ - EXPLAIN options should be Query Tool instance-specific. | `Issue #4395 <https://redmine.postgresql.org/issues/4395>`_ - EXPLAIN options should be Query Tool instance-specific.

View File

@@ -387,3 +387,18 @@ def register_browser_preferences(self):
category_label=gettext('Keyboard shortcuts'), category_label=gettext('Keyboard shortcuts'),
fields=fields fields=fields
) )
self.preference.register(
'keyboard_shortcuts',
'add_grid_row',
gettext('Add grid row'),
'keyboardshortcut',
{
'alt': False,
'shift': True,
'control': True,
'key': {'key_code': 65, 'char': 'a'}
},
category_label=gettext('Keyboard shortcuts'),
fields=fields
)

View File

@@ -41,6 +41,7 @@ _.extend(pgBrowser.keyboardNavigation, {
'direct_debugging': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'direct_debugging').value), 'direct_debugging': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'direct_debugging').value),
'drop_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_multiple').value), 'drop_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_multiple').value),
'drop_cascade_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_cascade_multiple').value), 'drop_cascade_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_cascade_multiple').value),
'add_grid_row': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'add_grid_row').value),
}; };
this.shortcutMethods = { this.shortcutMethods = {
@@ -61,6 +62,7 @@ _.extend(pgBrowser.keyboardNavigation, {
'bindDirectDebugging': {'shortcuts': this.keyboardShortcut.direct_debugging}, // Sub menu - Direct Debugging 'bindDirectDebugging': {'shortcuts': this.keyboardShortcut.direct_debugging}, // Sub menu - Direct Debugging
'bindDropMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_multiple_objects}, // Grid Menu Drop Multiple 'bindDropMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_multiple_objects}, // Grid Menu Drop Multiple
'bindDropCascadeMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_cascade_multiple_objects}, // Grid Menu Drop Cascade Multiple 'bindDropCascadeMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_cascade_multiple_objects}, // Grid Menu Drop Cascade Multiple
'bindAddGridRow': {'shortcuts': this.keyboardShortcut.add_grid_row}, // Subnode Grid Add Row
}; };
this.bindShortcuts(); this.bindShortcuts();
} }
@@ -330,6 +332,12 @@ _.extend(pgBrowser.keyboardNavigation, {
$('button.delete_multiple_cascade').click(); $('button.delete_multiple_cascade').click();
} }
}, },
bindAddGridRow: function() {
let subNode = $(document.activeElement).closest('.object.subnode');
if ($(subNode).length) {
$(subNode).find('.add').click();
}
},
isPropertyPanelVisible: function() { isPropertyPanelVisible: function() {
let isPanelVisible = false; let isPanelVisible = false;
_.each(pgAdmin.Browser.docker.findPanels(), (panel) => { _.each(pgAdmin.Browser.docker.findPanels(), (panel) => {

View File

@@ -10,8 +10,10 @@
define([ define([
'sources/gettext', 'underscore', 'underscore.string', 'jquery', 'sources/gettext', 'underscore', 'underscore.string', 'jquery',
'backbone', 'backform', 'backgrid', 'codemirror', 'sources/sqleditor_utils', 'backbone', 'backform', 'backgrid', 'codemirror', 'sources/sqleditor_utils',
'sources/keyboard_shortcuts',
'spectrum', 'pgadmin.backgrid', 'select2', 'bootstrap.toggle', 'spectrum', 'pgadmin.backgrid', 'select2', 'bootstrap.toggle',
], function(gettext, _, S, $, Backbone, Backform, Backgrid, CodeMirror, SqlEditorUtils) { ], function(gettext, _, S, $, Backbone, Backform, Backgrid, CodeMirror,
SqlEditorUtils, keyboardShortcuts) {
var pgAdmin = (window.pgAdmin = window.pgAdmin || {}), var pgAdmin = (window.pgAdmin = window.pgAdmin || {}),
pgBrowser = pgAdmin.Browser; pgBrowser = pgAdmin.Browser;
@@ -1269,6 +1271,13 @@ define([
var $dialog = gridBody.append(subNodeGrid); var $dialog = gridBody.append(subNodeGrid);
let preferences = pgBrowser.get_preferences_for_module('browser');
let addBtn = $dialog.find('.add');
// Add title to the buttons
$(addBtn)
.attr('title',
keyboardShortcuts.shortcut_title(gettext('Add new row'),preferences.add_grid_row));
// Add button callback // Add button callback
if (!(data.disabled || data.canAdd == false)) { if (!(data.disabled || data.canAdd == false)) {
$dialog.find('button.add').first().on('click',(e) => { $dialog.find('button.add').first().on('click',(e) => {
@@ -1554,6 +1563,14 @@ define([
var $dialog = gridBody.append(subNodeGrid); var $dialog = gridBody.append(subNodeGrid);
let preferences = pgBrowser.get_preferences_for_module('browser');
let addBtn = $dialog.find('.add');
// Add title to the buttons
$(addBtn)
.attr('title',
keyboardShortcuts.shortcut_title(gettext('Add new row'),preferences.add_grid_row));
// Add button callback // Add button callback
$dialog.find('button.add').on('click',(e) => { $dialog.find('button.add').on('click',(e) => {
e.preventDefault(); e.preventDefault();

View File

@@ -9,15 +9,18 @@
define([ define([
'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify', 'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify',
'moment', 'bignumber', 'bootstrap.datetimepicker', 'backgrid.filter', 'moment', 'bignumber', 'sources/utils', 'sources/keyboard_shortcuts',
'bootstrap.toggle', 'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle',
], function( ], function(
gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber,
commonUtils, keyboardShortcuts
) { ) {
/* /*
* Add mechanism in backgrid to render different types of cells in * Add mechanism in backgrid to render different types of cells in
* same column; * same column;
*/ */
let pgAdmin = (window.pgAdmin = window.pgAdmin || {}),
pgBrowser = pgAdmin.Browser;
// Add new property cellFunction in Backgrid.Column. // Add new property cellFunction in Backgrid.Column.
_.extend(Backgrid.Column.prototype.defaults, { _.extend(Backgrid.Column.prototype.defaults, {
@@ -37,6 +40,18 @@ define([
}, },
}); });
// bind shortcut in cell edit mode
_.extend(Backgrid.InputCellEditor.prototype.events, {
'keydown': function(e) {
let preferences = pgBrowser.get_preferences_for_module('browser');
if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
pgBrowser.keyboardNavigation.bindAddGridRow();
} else {
Backgrid.InputCellEditor.prototype.saveOrCancel.apply(this, arguments);
}
},
});
/* Overriding backgrid sort method. /* Overriding backgrid sort method.
* As we are getting numeric, integer values as string * As we are getting numeric, integer values as string
* from server side, but on client side javascript truncates * from server side, but on client side javascript truncates
@@ -151,6 +166,62 @@ define([
} }
}; };
}, },
moveToNextCell: function (model, column, command) {
var i = this.collection.indexOf(model);
var j = this.columns.indexOf(column);
var cell, renderable, editable, m, n;
// return if model being edited in a different grid
if (j === -1) return this;
this.rows[i].cells[j].exitEditMode();
if (command.moveUp() || command.moveDown() || command.moveLeft() ||
command.moveRight() || command.save()) {
var l = this.columns.length;
var maxOffset = l * this.collection.length;
if (command.moveUp() || command.moveDown()) {
m = i + (command.moveUp() ? -1 : 1);
var row = this.rows[m];
if (row) {
cell = row.cells[j];
if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) {
cell.enterEditMode();
model.trigger('backgrid:next', m, j, false);
}
}
else model.trigger('backgrid:next', m, j, true);
}
else if (command.moveLeft() || command.moveRight()) {
var right = command.moveRight();
for (var offset = i * l + j + (right ? 1 : -1);
offset >= 0 && offset < maxOffset;
right ? offset++ : offset--) {
m = ~~(offset / l);
n = offset - m * l;
cell = this.rows[m].cells[n];
renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model);
editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model);
if(cell && cell.$el.hasClass('edit-cell') &&
!cell.$el.hasClass('privileges') || cell.$el.hasClass('delete-cell')) {
model.trigger('backgrid:next', m, n, false);
break;
} else if (renderable && editable) {
cell.enterEditMode();
model.trigger('backgrid:next', m, n, false);
break;
}
}
if (offset == maxOffset) {
model.trigger('backgrid:next', ~~(offset / l), offset - m * l, true);
}
}
}
return this;
},
}); });
_.extend(Backgrid.Row.prototype, { _.extend(Backgrid.Row.prototype, {
@@ -189,7 +260,7 @@ define([
var ObjectCellEditor = Backgrid.Extension.ObjectCellEditor = Backgrid.CellEditor.extend({ var ObjectCellEditor = Backgrid.Extension.ObjectCellEditor = Backgrid.CellEditor.extend({
modalTemplate: _.template([ modalTemplate: _.template([
'<div class="subnode-dialog" tabindex="1">', '<div class="subnode-dialog" tabindex="0">',
' <div class="subnode-body"></div>', ' <div class="subnode-body"></div>',
'</div>', '</div>',
].join('\n')), ].join('\n')),
@@ -235,6 +306,14 @@ define([
tabPanelClassName: function() { tabPanelClassName: function() {
return 'sub-node-form col-sm-12'; return 'sub-node-form col-sm-12';
}, },
events: {
'keydown': function (event) {
let preferences = pgBrowser.get_preferences_for_module('browser');
if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,event)) {
pgBrowser.keyboardNavigation.bindAddGridRow();
}
},
},
}); });
this.objectView.render(); this.objectView.render();
@@ -315,12 +394,18 @@ define([
editorOptions['el'] = $(this.el); editorOptions['el'] = $(this.el);
editorOptions['columns_length'] = this.column.collection.length; editorOptions['columns_length'] = this.column.collection.length;
editorOptions['el'].attr('tabindex', 1); editorOptions['el'].attr('tabindex', 0);
this.listenTo(this.model, 'backgrid:edit', function(model, column, cell, editor) { this.listenTo(this.model, 'backgrid:edit', function(model, column, cell, editor) {
if (column.get('name') == this.column.get('name')) if (column.get('name') == this.column.get('name'))
editor.extendWithOptions(editorOptions); editor.extendWithOptions(editorOptions);
}); });
// Listen for Tab key, open subnode dialog on space key
this.$el.on('keydown', function(e) {
if (e.keyCode == 32) {
$(this).click();
}
});
}, },
enterEditMode: function() { enterEditMode: function() {
// Notify that we are about to enter in edit mode for current cell. // Notify that we are about to enter in edit mode for current cell.
@@ -342,6 +427,10 @@ define([
this.$el.html( this.$el.html(
'<i class=\'fa fa-pencil-square subnode-edit-in-process\' title=\'' + _('Edit row') + '\'></i>' '<i class=\'fa fa-pencil-square subnode-edit-in-process\' title=\'' + _('Edit row') + '\'></i>'
); );
let body = $(this.$el).parents()[1],
container = $(body).find('.tab-content:first > .tab-pane.active:first');
commonUtils.findAndSetFocus(container);
pgBrowser.keyboardNavigation.getDialogTabNavigator($(body).find('.subnode-dialog'));
this.model.trigger( this.model.trigger(
'pg-sub-node:opened', this.model, this 'pg-sub-node:opened', this.model, this
); );
@@ -362,14 +451,16 @@ define([
return this; return this;
}, },
exitEditMode: function() { exitEditMode: function() {
var index = $(this.currentEditor.objectView.el) if(!_.isUndefined(this.currentEditor) || !_.isEmpty(this.currentEditor)) {
.find('.nav-tabs > .active > a[data-toggle="tab"]').first() var index = $(this.currentEditor.objectView.el)
.data('tabIndex'); .find('.nav-tabs > .active > a[data-toggle="tab"]').first()
Backgrid.Cell.prototype.exitEditMode.apply(this, arguments); .data('tabIndex');
this.model.trigger( Backgrid.Cell.prototype.exitEditMode.apply(this, arguments);
'pg-sub-node:closed', this, index this.model.trigger(
); 'pg-sub-node:closed', this, index
this.grabFocus = true; );
this.grabFocus = true;
}
}, },
events: { events: {
'click': function(e) { 'click': function(e) {
@@ -382,6 +473,17 @@ define([
} }
e.preventDefault(); e.preventDefault();
}, },
'keydown': function(e) {
var model = this.model;
var column = this.column;
var command = new Backgrid.Command(e);
if (command.moveLeft()) {
setTimeout(function() {
model.trigger('backgrid:edited', model, column, command);
}, 20);
}
},
}, },
}); });
@@ -413,7 +515,16 @@ define([
delete_title, delete_title,
delete_msg, delete_msg,
function() { function() {
let tbody = $(that.el).parents('tbody').eq(0);
that.model.collection.remove(that.model); that.model.collection.remove(that.model);
let row = $(tbody).find('tr');
if(row.length > 0) {
// set focus to first tr
row.first().children()[0].focus();
} else {
// set focus to add button
$(tbody).parents('.subnode').eq(0).find('.add').focus();
}
}, },
function() { function() {
return true; return true;
@@ -427,12 +538,54 @@ define([
); );
} }
}, },
exitEditMode: function() {
this.$el.removeClass('editor');
},
initialize: function() { initialize: function() {
Backgrid.Cell.prototype.initialize.apply(this, arguments); Backgrid.Cell.prototype.initialize.apply(this, arguments);
}, },
render: function() { render: function() {
var self = this;
this.$el.empty(); this.$el.empty();
$(this.$el).attr('tabindex', 0);
this.$el.html('<i class=\'fa fa-trash\' title=\'' + _('Delete row') + '\'></i>'); this.$el.html('<i class=\'fa fa-trash\' title=\'' + _('Delete row') + '\'></i>');
// Listen for Tab/Shift-Tab key
this.$el.on('keydown', function(e) {
// with keyboard navigation on space key, mark row for deletion
if (e.keyCode == 32) {
self.$el.click();
}
var gotoCell;
if (e.keyCode == 9 || e.keyCode == 16) {
// go to Next Cell & if Shift is also pressed go to Previous Cell
gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next();
}
if (gotoCell) {
let command = new Backgrid.Command({
key: 'Tab',
keyCode: 9,
which: 9,
shiftKey: e.shiftKey,
});
setTimeout(function() {
// When we have Editable Cell
if (gotoCell.hasClass('editable')) {
e.preventDefault();
e.stopPropagation();
self.model.trigger('backgrid:edited', self.model,
self.column, command);
}
else {
// When we have Non-Editable Cell
self.model.trigger('backgrid:edited', self.model,
self.column, command);
}
}, 20);
}
});
this.delegateEvents(); this.delegateEvents();
return this; return this;
}, },
@@ -488,6 +641,7 @@ define([
'change input': 'onChange', 'change input': 'onChange',
'keyup': 'toggleSwitch', 'keyup': 'toggleSwitch',
'blur input': 'exitEditMode', 'blur input': 'exitEditMode',
'keydown': 'onKeyDown',
}, },
toggleSwitch: function(e) { toggleSwitch: function(e) {
@@ -497,6 +651,13 @@ define([
} }
}, },
onKeyDown: function(e) {
let preferences = pgBrowser.get_preferences_for_module('browser');
if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
pgBrowser.keyboardNavigation.bindAddGridRow();
}
},
onChange: function() { onChange: function() {
var model = this.model, var model = this.model,
column = this.column, column = this.column,
@@ -553,7 +714,11 @@ define([
}); });
setTimeout(function() { setTimeout(function() {
// When we have Editable Cell // When we have Editable Cell
if (gotoCell.hasClass('editable')) { if (gotoCell.hasClass('editable') && gotoCell.hasClass('edit-cell')) {
e.preventDefault();
e.stopPropagation();
gotoCell.trigger('focus');
} else if (gotoCell.hasClass('editable')) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
self.model.trigger('backgrid:edited', self.model, self.model.trigger('backgrid:edited', self.model,
@@ -608,8 +773,7 @@ define([
}, },
saveOrCancel: function (e) { saveOrCancel: function (e) {
var model = this.model; var self = this;
var column = this.column;
var command = new Backgrid.Command(e); var command = new Backgrid.Command(e);
var blurred = e.type === 'blur'; var blurred = e.type === 'blur';
@@ -617,10 +781,32 @@ define([
if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() || if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() ||
command.save() || blurred) { command.save() || blurred) {
this.exitEditMode(); let gotoCell;
e.preventDefault(); // go to Next Cell & if Shift is also pressed go to Previous Cell
e.stopPropagation(); gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next();
model.trigger('backgrid:edited', model, column, command);
if (gotoCell) {
let command = new Backgrid.Command({
key: 'Tab',
keyCode: 9,
which: 9,
shiftKey: e.shiftKey,
});
setTimeout(function() {
// When we have Editable Cell
if (gotoCell.hasClass('editable')) {
e.preventDefault();
e.stopPropagation();
self.model.trigger('backgrid:edited', self.model,
self.column, command);
}
else {
// When we have Non-Editable Cell
self.model.trigger('backgrid:edited', self.model,
self.column, command);
}
}, 20);
}
} }
}, },
events: { events: {

View File

@@ -46,13 +46,13 @@ class dialogTabNavigator {
if(childTabData) { if(childTabData) {
var res = this.navigate(shortcut, childTabData.childTab, var res = this.navigate(shortcut, childTabData.childTab,
childTabData.childTabPane); childTabData.childTabPane, event);
if (!res) { if (!res) {
this.navigate(shortcut, this.tabs, currentTabPane); this.navigate(shortcut, this.tabs, currentTabPane, event);
} }
} else { } else {
this.navigate(shortcut, this.tabs, currentTabPane); this.navigate(shortcut, this.tabs, currentTabPane, event);
} }
} }
@@ -73,16 +73,16 @@ class dialogTabNavigator {
return null; return null;
} }
navigate(shortcut, tabs, tab_pane) { navigate(shortcut, tabs, tab_pane, event) {
if(shortcut == this.dialogTabBackward) { if (shortcut == this.dialogTabBackward) {
return this.navigateBackward(tabs, tab_pane); return this.navigateBackward(tabs, tab_pane, event);
}else if (shortcut == this.dialogTabForward) { } else if (shortcut == this.dialogTabForward) {
return this.navigateForward(tabs, tab_pane); return this.navigateForward(tabs, tab_pane, event);
} }
return false; return false;
} }
navigateBackward(tabs, tab_pane) { navigateBackward(tabs, tab_pane, event) {
var self = this, var self = this,
nextTabPane, nextTabPane,
innerTabContainer, innerTabContainer,
@@ -105,6 +105,7 @@ class dialogTabNavigator {
self.tabSwitching = false; self.tabSwitching = false;
}, 200); }, 200);
event.stopPropagation();
return true; return true;
} }
@@ -112,7 +113,7 @@ class dialogTabNavigator {
return false; return false;
} }
navigateForward(tabs, tab_pane) { navigateForward(tabs, tab_pane, event) {
var self = this, var self = this,
nextTabPane, nextTabPane,
innerTabContainer, innerTabContainer,
@@ -135,6 +136,8 @@ class dialogTabNavigator {
self.tabSwitching = false; self.tabSwitching = false;
}, 200); }, 200);
event.stopPropagation();
return true; return true;
} }
this.tabSwitching = false; this.tabSwitching = false;

View File

@@ -288,6 +288,10 @@ table.backgrid {
background-color: $color-bg-theme !important; background-color: $color-bg-theme !important;
} }
& td.edit-cell.editor:focus {
outline: $input-focus-border-color auto 5px !important;
}
tr.editor-row { tr.editor-row {
background-color: $color-gray-light !important; background-color: $color-gray-light !important;
& > td { & > td {

View File

@@ -729,10 +729,17 @@ table tr th {
padding: 0; padding: 0;
} }
& button:focus { & button:focus {
outline: none; outline: $input-focus-border-color auto 5px !important;
} }
} }
table tr td {
td.edit-cell:focus,
td.delete-cell:focus,
td.string-cell:focus {
outline: $input-focus-border-color auto 5px !important;
}
}
.privilege_label{ .privilege_label{
font-size: 10px!important; font-size: 10px!important;