diff --git a/docs/en_US/keyboard_shortcuts.rst b/docs/en_US/keyboard_shortcuts.rst index 360efb322..57a5c2eca 100644 --- a/docs/en_US/keyboard_shortcuts.rst +++ b/docs/en_US/keyboard_shortcuts.rst @@ -64,6 +64,29 @@ Use the shortcuts below to navigate the tabsets on dialogs: | 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 *********** diff --git a/docs/en_US/release_notes_4_11.rst b/docs/en_US/release_notes_4_11.rst index 3ca075068..664cb8e13 100644 --- a/docs/en_US/release_notes_4_11.rst +++ b/docs/en_US/release_notes_4_11.rst @@ -26,6 +26,7 @@ Housekeeping Bug fixes ********* +| `Issue #3919 `_ - Allow keyboard navigation of all controls on subnode grids. | `Issue #4224 `_ - Prevent flickering of large tooltips on the Graphical EXPLAIN canvas. | `Issue #4393 `_ - Ensure parameter values are quoted when needed when editing roles. | `Issue #4395 `_ - EXPLAIN options should be Query Tool instance-specific. diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py index 009f1c106..bb5da06c1 100644 --- a/web/pgadmin/browser/register_browser_preferences.py +++ b/web/pgadmin/browser/register_browser_preferences.py @@ -387,3 +387,18 @@ def register_browser_preferences(self): category_label=gettext('Keyboard shortcuts'), 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 + ) diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js index 9ad59b6fb..3958e8cf5 100644 --- a/web/pgadmin/browser/static/js/keyboard.js +++ b/web/pgadmin/browser/static/js/keyboard.js @@ -41,6 +41,7 @@ _.extend(pgBrowser.keyboardNavigation, { '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_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 = { @@ -61,6 +62,7 @@ _.extend(pgBrowser.keyboardNavigation, { 'bindDirectDebugging': {'shortcuts': this.keyboardShortcut.direct_debugging}, // Sub menu - Direct Debugging 'bindDropMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_multiple_objects}, // Grid Menu Drop 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(); } @@ -330,6 +332,12 @@ _.extend(pgBrowser.keyboardNavigation, { $('button.delete_multiple_cascade').click(); } }, + bindAddGridRow: function() { + let subNode = $(document.activeElement).closest('.object.subnode'); + if ($(subNode).length) { + $(subNode).find('.add').click(); + } + }, isPropertyPanelVisible: function() { let isPanelVisible = false; _.each(pgAdmin.Browser.docker.findPanels(), (panel) => { diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js index b97fc3803..156430df4 100644 --- a/web/pgadmin/static/js/backform.pgadmin.js +++ b/web/pgadmin/static/js/backform.pgadmin.js @@ -10,8 +10,10 @@ define([ 'sources/gettext', 'underscore', 'underscore.string', 'jquery', 'backbone', 'backform', 'backgrid', 'codemirror', 'sources/sqleditor_utils', + 'sources/keyboard_shortcuts', '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 || {}), pgBrowser = pgAdmin.Browser; @@ -1269,6 +1271,13 @@ define([ 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 if (!(data.disabled || data.canAdd == false)) { $dialog.find('button.add').first().on('click',(e) => { @@ -1554,6 +1563,14 @@ define([ 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 $dialog.find('button.add').on('click',(e) => { e.preventDefault(); diff --git a/web/pgadmin/static/js/backgrid.pgadmin.js b/web/pgadmin/static/js/backgrid.pgadmin.js index c80460935..0d968aa9b 100644 --- a/web/pgadmin/static/js/backgrid.pgadmin.js +++ b/web/pgadmin/static/js/backgrid.pgadmin.js @@ -9,15 +9,18 @@ define([ 'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify', - 'moment', 'bignumber', 'bootstrap.datetimepicker', 'backgrid.filter', - 'bootstrap.toggle', + 'moment', 'bignumber', 'sources/utils', 'sources/keyboard_shortcuts', + 'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle', ], 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 * same column; */ + let pgAdmin = (window.pgAdmin = window.pgAdmin || {}), + pgBrowser = pgAdmin.Browser; // Add new property cellFunction in Backgrid.Column. _.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. * As we are getting numeric, integer values as string * 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, { @@ -189,7 +260,7 @@ define([ var ObjectCellEditor = Backgrid.Extension.ObjectCellEditor = Backgrid.CellEditor.extend({ modalTemplate: _.template([ - '
', + '
', '
', '
', ].join('\n')), @@ -235,6 +306,14 @@ define([ tabPanelClassName: function() { 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(); @@ -315,12 +394,18 @@ define([ editorOptions['el'] = $(this.el); 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) { if (column.get('name') == this.column.get('name')) 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() { // Notify that we are about to enter in edit mode for current cell. @@ -342,6 +427,10 @@ define([ this.$el.html( '' ); + 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( 'pg-sub-node:opened', this.model, this ); @@ -362,14 +451,16 @@ define([ return this; }, exitEditMode: function() { - var index = $(this.currentEditor.objectView.el) - .find('.nav-tabs > .active > a[data-toggle="tab"]').first() - .data('tabIndex'); - Backgrid.Cell.prototype.exitEditMode.apply(this, arguments); - this.model.trigger( - 'pg-sub-node:closed', this, index - ); - this.grabFocus = true; + if(!_.isUndefined(this.currentEditor) || !_.isEmpty(this.currentEditor)) { + var index = $(this.currentEditor.objectView.el) + .find('.nav-tabs > .active > a[data-toggle="tab"]').first() + .data('tabIndex'); + Backgrid.Cell.prototype.exitEditMode.apply(this, arguments); + this.model.trigger( + 'pg-sub-node:closed', this, index + ); + this.grabFocus = true; + } }, events: { 'click': function(e) { @@ -382,6 +473,17 @@ define([ } 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_msg, function() { + let tbody = $(that.el).parents('tbody').eq(0); 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() { return true; @@ -427,12 +538,54 @@ define([ ); } }, + exitEditMode: function() { + this.$el.removeClass('editor'); + }, initialize: function() { Backgrid.Cell.prototype.initialize.apply(this, arguments); }, render: function() { + var self = this; this.$el.empty(); + $(this.$el).attr('tabindex', 0); this.$el.html(''); + // 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(); return this; }, @@ -488,6 +641,7 @@ define([ 'change input': 'onChange', 'keyup': 'toggleSwitch', 'blur input': 'exitEditMode', + 'keydown': 'onKeyDown', }, 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() { var model = this.model, column = this.column, @@ -553,7 +714,11 @@ define([ }); setTimeout(function() { // 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.stopPropagation(); self.model.trigger('backgrid:edited', self.model, @@ -608,8 +773,7 @@ define([ }, saveOrCancel: function (e) { - var model = this.model; - var column = this.column; + var self = this; var command = new Backgrid.Command(e); var blurred = e.type === 'blur'; @@ -617,10 +781,32 @@ define([ if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() || command.save() || blurred) { - this.exitEditMode(); - e.preventDefault(); - e.stopPropagation(); - model.trigger('backgrid:edited', model, column, command); + let gotoCell; + // 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); + } } }, events: { diff --git a/web/pgadmin/static/js/dialog_tab_navigator.js b/web/pgadmin/static/js/dialog_tab_navigator.js index 44721725d..594371159 100644 --- a/web/pgadmin/static/js/dialog_tab_navigator.js +++ b/web/pgadmin/static/js/dialog_tab_navigator.js @@ -46,13 +46,13 @@ class dialogTabNavigator { if(childTabData) { var res = this.navigate(shortcut, childTabData.childTab, - childTabData.childTabPane); + childTabData.childTabPane, event); if (!res) { - this.navigate(shortcut, this.tabs, currentTabPane); + this.navigate(shortcut, this.tabs, currentTabPane, event); } } else { - this.navigate(shortcut, this.tabs, currentTabPane); + this.navigate(shortcut, this.tabs, currentTabPane, event); } } @@ -73,16 +73,16 @@ class dialogTabNavigator { return null; } - navigate(shortcut, tabs, tab_pane) { - if(shortcut == this.dialogTabBackward) { - return this.navigateBackward(tabs, tab_pane); - }else if (shortcut == this.dialogTabForward) { - return this.navigateForward(tabs, tab_pane); + navigate(shortcut, tabs, tab_pane, event) { + if (shortcut == this.dialogTabBackward) { + return this.navigateBackward(tabs, tab_pane, event); + } else if (shortcut == this.dialogTabForward) { + return this.navigateForward(tabs, tab_pane, event); } return false; } - navigateBackward(tabs, tab_pane) { + navigateBackward(tabs, tab_pane, event) { var self = this, nextTabPane, innerTabContainer, @@ -105,6 +105,7 @@ class dialogTabNavigator { self.tabSwitching = false; }, 200); + event.stopPropagation(); return true; } @@ -112,7 +113,7 @@ class dialogTabNavigator { return false; } - navigateForward(tabs, tab_pane) { + navigateForward(tabs, tab_pane, event) { var self = this, nextTabPane, innerTabContainer, @@ -135,6 +136,8 @@ class dialogTabNavigator { self.tabSwitching = false; }, 200); + event.stopPropagation(); + return true; } this.tabSwitching = false; diff --git a/web/pgadmin/static/scss/_backgrid.overrides.scss b/web/pgadmin/static/scss/_backgrid.overrides.scss index b0b7475a5..41266eb7c 100644 --- a/web/pgadmin/static/scss/_backgrid.overrides.scss +++ b/web/pgadmin/static/scss/_backgrid.overrides.scss @@ -288,6 +288,10 @@ table.backgrid { background-color: $color-bg-theme !important; } + & td.edit-cell.editor:focus { + outline: $input-focus-border-color auto 5px !important; + } + tr.editor-row { background-color: $color-gray-light !important; & > td { diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss index f5a8877c8..8f1e248d8 100644 --- a/web/pgadmin/static/scss/_pgadmin.style.scss +++ b/web/pgadmin/static/scss/_pgadmin.style.scss @@ -729,10 +729,17 @@ table tr th { padding: 0; } & 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{ font-size: 10px!important;