From eb50bb33e74a6f5c0b9e1fb33465a6fc67f21b96 Mon Sep 17 00:00:00 2001 From: Sam El-Husseini Date: Wed, 30 Oct 2019 12:41:04 -0700 Subject: [PATCH] Use a cursor to tab between tab navigable fields. (#3365) * Use a cursor to tab between tab navigable fields. --- blockly_uncompressed.js | 5 +- core/block_svg.js | 49 +----- core/keyboard_nav/tab_navigate_cursor.js | 183 +++++++++++++++++++++++ 3 files changed, 194 insertions(+), 43 deletions(-) create mode 100644 core/keyboard_nav/tab_navigate_cursor.js diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index 43ec6e9f2..c3b5961e8 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -26,7 +26,7 @@ goog.addDependency("../../core/block_animations.js", ['Blockly.blockAnimations'] goog.addDependency("../../core/block_drag_surface.js", ['Blockly.BlockDragSurfaceSvg'], ['Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom']); goog.addDependency("../../core/block_dragger.js", ['Blockly.BlockDragger'], ['Blockly.blockAnimations', 'Blockly.Events', 'Blockly.Events.BlockMove', 'Blockly.Events.Ui', 'Blockly.InsertionMarkerManager', 'Blockly.utils.Coordinate', 'Blockly.utils.dom']); goog.addDependency("../../core/block_events.js", ['Blockly.Events.BlockBase', 'Blockly.Events.BlockChange', 'Blockly.Events.BlockCreate', 'Blockly.Events.BlockDelete', 'Blockly.Events.BlockMove', 'Blockly.Events.Change', 'Blockly.Events.Create', 'Blockly.Events.Delete', 'Blockly.Events.Move'], ['Blockly.Events', 'Blockly.Events.Abstract', 'Blockly.utils.Coordinate', 'Blockly.utils.object', 'Blockly.utils.xml']); -goog.addDependency("../../core/block_svg.js", ['Blockly.BlockSvg'], ['Blockly.Block', 'Blockly.blockAnimations', 'Blockly.blockRendering.IPathObject', 'Blockly.ContextMenu', 'Blockly.Events', 'Blockly.Events.Ui', 'Blockly.Events.BlockMove', 'Blockly.Msg', 'Blockly.RenderedConnection', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.Rect', 'Blockly.Warning']); +goog.addDependency("../../core/block_svg.js", ['Blockly.BlockSvg'], ['Blockly.Block', 'Blockly.blockAnimations', 'Blockly.blockRendering.IPathObject', 'Blockly.ContextMenu', 'Blockly.Events', 'Blockly.Events.Ui', 'Blockly.Events.BlockMove', 'Blockly.Msg', 'Blockly.RenderedConnection', 'Blockly.TabNavigateCursor', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.Rect', 'Blockly.Warning']); goog.addDependency("../../core/blockly.js", ['Blockly'], ['Blockly.constants', 'Blockly.Events', 'Blockly.Events.Ui', 'Blockly.inject', 'Blockly.navigation', 'Blockly.Procedures', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.utils', 'Blockly.utils.colour', 'Blockly.Variables', 'Blockly.WidgetDiv', 'Blockly.WorkspaceSvg', 'Blockly.Xml']); goog.addDependency("../../core/blocks.js", ['Blockly.Blocks'], []); goog.addDependency("../../core/bubble.js", ['Blockly.Bubble'], ['Blockly.Scrollbar', 'Blockly.Touch', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.userAgent', 'Blockly.Workspace']); @@ -81,6 +81,7 @@ goog.addDependency("../../core/keyboard_nav/flyout_cursor.js", ['Blockly.FlyoutC goog.addDependency("../../core/keyboard_nav/key_map.js", ['Blockly.user.keyMap'], ['Blockly.utils.KeyCodes', 'Blockly.utils.object']); goog.addDependency("../../core/keyboard_nav/marker_cursor.js", ['Blockly.MarkerCursor'], ['Blockly.Cursor', 'Blockly.utils.object']); goog.addDependency("../../core/keyboard_nav/navigation.js", ['Blockly.navigation'], ['Blockly.Action', 'Blockly.ASTNode', 'Blockly.utils.Coordinate', 'Blockly.user.keyMap']); +goog.addDependency("../../core/keyboard_nav/tab_navigate_cursor.js", ['Blockly.TabNavigateCursor'], ['Blockly.ASTNode', 'Blockly.Cursor', 'Blockly.utils.object']); goog.addDependency("../../core/msg.js", ['Blockly.Msg'], ['Blockly.utils.global']); goog.addDependency("../../core/mutator.js", ['Blockly.Mutator'], ['Blockly.Bubble', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Events.Ui', 'Blockly.Icon', 'Blockly.utils', 'Blockly.utils.dom', 'Blockly.utils.global', 'Blockly.utils.object', 'Blockly.utils.xml', 'Blockly.WorkspaceSvg', 'Blockly.Xml']); goog.addDependency("../../core/names.js", ['Blockly.Names'], ['Blockly.Msg']); @@ -99,7 +100,7 @@ goog.addDependency("../../core/renderers/geras/drawer.js", ['Blockly.geras.Drawe goog.addDependency("../../core/renderers/geras/highlight_constants.js", ['Blockly.geras.HighlightConstantProvider'], ['Blockly.blockRendering.ConstantProvider', 'Blockly.utils.svgPaths']); goog.addDependency("../../core/renderers/geras/highlighter.js", ['Blockly.geras.Highlighter'], ['Blockly.blockRendering.BottomRow', 'Blockly.blockRendering.InputRow', 'Blockly.blockRendering.Measurable', 'Blockly.blockRendering.RenderInfo', 'Blockly.blockRendering.Row', 'Blockly.blockRendering.SpacerRow', 'Blockly.blockRendering.TopRow', 'Blockly.blockRendering.Types', 'Blockly.utils.svgPaths']); goog.addDependency("../../core/renderers/geras/info.js", ['Blockly.geras', 'Blockly.geras.RenderInfo'], ['Blockly.blockRendering.BottomRow', 'Blockly.blockRendering.InputRow', 'Blockly.blockRendering.Measurable', 'Blockly.blockRendering.NextConnection', 'Blockly.blockRendering.OutputConnection', 'Blockly.blockRendering.PreviousConnection', 'Blockly.blockRendering.RenderInfo', 'Blockly.blockRendering.BottomRow', 'Blockly.blockRendering.InputRow', 'Blockly.blockRendering.Measurable', 'Blockly.blockRendering.NextConnection', 'Blockly.blockRendering.OutputConnection', 'Blockly.blockRendering.PreviousConnection', 'Blockly.blockRendering.Types', 'Blockly.blockRendering.ExternalValueInput', 'Blockly.geras.InlineInput', 'Blockly.geras.StatementInput', 'Blockly.utils.object']); -goog.addDependency("../../core/renderers/geras/measurables/inputs.js", ['Blockly.geras.InlineInput', 'Blockly.geras.StatementInput'], ['Blockly.blockRendering.Connection', 'Blockly.utils.object']); +goog.addDependency("../../core/renderers/geras/measurables/inputs.js", ['Blockly.geras.InlineInput', 'Blockly.geras.StatementInput'], ['Blockly.utils.object']); goog.addDependency("../../core/renderers/geras/path_object.js", ['Blockly.geras.PathObject'], ['Blockly.blockRendering.IPathObject', 'Blockly.utils.dom']); goog.addDependency("../../core/renderers/geras/renderer.js", ['Blockly.geras.Renderer'], ['Blockly.blockRendering', 'Blockly.blockRendering.Renderer', 'Blockly.geras.ConstantProvider', 'Blockly.geras.Drawer', 'Blockly.geras.HighlightConstantProvider', 'Blockly.geras.PathObject', 'Blockly.geras.RenderInfo', 'Blockly.utils.object']); goog.addDependency("../../core/renderers/measurables/base.js", ['Blockly.blockRendering.Measurable'], ['Blockly.blockRendering.Types']); diff --git a/core/block_svg.js b/core/block_svg.js index ebbba039c..b404e27fb 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -32,6 +32,7 @@ goog.require('Blockly.Events.Ui'); goog.require('Blockly.Events.BlockMove'); goog.require('Blockly.Msg'); goog.require('Blockly.RenderedConnection'); +goog.require('Blockly.TabNavigateCursor'); goog.require('Blockly.Tooltip'); goog.require('Blockly.Touch'); goog.require('Blockly.utils'); @@ -693,52 +694,18 @@ Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { /** * Open the next (or previous) FieldTextInput. - * @param {Blockly.Field|Blockly.Block} start Current location. + * @param {!Blockly.Field} start Current field. * @param {boolean} forward If true go forward, otherwise backward. */ Blockly.BlockSvg.prototype.tab = function(start, forward) { - var list = this.createTabList_(); - var i = start != null ? list.indexOf(start) : -1; - if (i == -1) { - // No start location, start at the beginning or end. - i = forward ? -1 : list.length; - } - var target = list[forward ? i + 1 : i - 1]; - if (!target) { - // Ran off of list. - var parent = this.getParent(); - if (parent) { - parent.tab(this, forward); - } - } else if (target instanceof Blockly.Field) { - target.showEditor(); - } else { - target.tab(null, forward); - } -}; + var tabCursor = new Blockly.TabNavigateCursor(); + tabCursor.setCurNode(Blockly.ASTNode.createFieldNode(start)); -/** - * Create an ordered list of all text fields and connected inputs. - * @return {!Array.} The ordered list. - * @private - */ -Blockly.BlockSvg.prototype.createTabList_ = function() { - // This function need not be efficient since it runs once on a keypress. - var list = []; - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - if (field.isTabNavigable() && field.isVisible()) { - list.push(field); - } - } - if (input.connection) { - var block = input.connection.targetBlock(); - if (block) { - list.push(block); - } - } + var nextNode = forward ? tabCursor.next() : tabCursor.prev(); + if (nextNode) { + var nextField = /** @type {!Blockly.Field} */ (nextNode.getLocation()); + nextField.showEditor(); } - return list; }; /** diff --git a/core/keyboard_nav/tab_navigate_cursor.js b/core/keyboard_nav/tab_navigate_cursor.js new file mode 100644 index 000000000..f6cf9435e --- /dev/null +++ b/core/keyboard_nav/tab_navigate_cursor.js @@ -0,0 +1,183 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview The class representing a cursor that is used to navigate + * between tab navigable fields. + * @author samelh@google.com (Sam El-Husseini) + */ +'use strict'; + +goog.provide('Blockly.TabNavigateCursor'); + +goog.require('Blockly.ASTNode'); +goog.require('Blockly.Cursor'); +goog.require('Blockly.utils.object'); + + +/** + * A cursor for navigating between tab navigable fields. + * @constructor + * @extends {Blockly.Cursor} + */ +Blockly.TabNavigateCursor = function() { + Blockly.TabNavigateCursor.superClass_.constructor.call(this); +}; +Blockly.utils.object.inherits(Blockly.TabNavigateCursor, Blockly.Cursor); + + +/** + * Find the next node in the pre order traversal. + * @override + */ +Blockly.TabNavigateCursor.prototype.next = function() { + var curNode = this.getCurNode(); + if (!curNode) { + return null; + } + var newNode = this.getNextNode_(curNode); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; +}; + +/** + * Find the previous node in the pre order traversal. + * @override + */ +Blockly.TabNavigateCursor.prototype.prev = function() { + var curNode = this.getCurNode(); + if (!curNode) { + return null; + } + var newNode = this.getPreviousNode_(curNode); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; +}; + +/** + * Skip all nodes except for tab navigable fields. + * @param {Blockly.ASTNode} node The AST node to check whether it is valid. + * @return {boolean} True if the node should be visited, false otherwise. + * @private + */ +Blockly.TabNavigateCursor.prototype.validNode_ = function(node) { + var isValid = false; + var type = node && node.getType(); + if (node) { + var location = node.getLocation(); + if (type == Blockly.ASTNode.types.FIELD && + location && location.isTabNavigable() && + (/** @type {!Blockly.Field} */ (location)).isClickable()) { + isValid = true; + } + } + return isValid; +}; + +/** + * From a given node find either the next valid sibling or parent. + * @param {Blockly.ASTNode} node The current position in the AST. + * @return {Blockly.ASTNode} The parent AST node or null if there are no + * valid parents. + * @private + */ +Blockly.TabNavigateCursor.prototype.findSiblingOrParent_ = function(node) { + if (!node) { + return null; + } + var nextNode = node.next(); + if (nextNode) { + return nextNode; + } + return this.findSiblingOrParent_(node.out()); +}; + +/** + * Navigate the Blockly AST using pre-order traversal. + * @param {Blockly.ASTNode} node The current position in the AST. + * @return {Blockly.ASTNode} The next node in the traversal. + * @private + */ +Blockly.TabNavigateCursor.prototype.getNextNode_ = function(node) { + if (!node) { + return null; + } + var newNode = node.in() || node.next(); + if (this.validNode_(newNode)) { + return newNode; + } else if (newNode) { + return this.getNextNode_(newNode); + } + var siblingOrParent = this.findSiblingOrParent_(node.out()); + if (this.validNode_(siblingOrParent)) { + return siblingOrParent; + } else if (siblingOrParent) { + return this.getNextNode_(siblingOrParent); + } + return null; +}; + +/** + * Get the right most child of a node. + * @param {Blockly.ASTNode} node The node to find the right most child of. + * @return {Blockly.ASTNode} The right most child of the given node, or the node + * if no child exists. + * @private + */ +Blockly.TabNavigateCursor.prototype.getRightMostChild_ = function(node) { + if (!node.in()) { + return node; + } + var newNode = node.in(); + while (newNode.next()) { + newNode = newNode.next(); + } + return this.getRightMostChild_(newNode); + +}; + +/** + * Use reverse pre-order traversal in order to find the previous node. + * @param {Blockly.ASTNode} node The current position in the AST. + * @return {Blockly.ASTNode} The previous node in the traversal or null if no + * previous node exists. + * @private + */ +Blockly.TabNavigateCursor.prototype.getPreviousNode_ = function(node) { + if (!node) { + return null; + } + var newNode = node.prev(); + + if (newNode) { + newNode = this.getRightMostChild_(newNode); + } else { + newNode = node.out(); + } + if (this.validNode_(newNode)) { + return newNode; + } else if (newNode) { + return this.getPreviousNode_(newNode); + } + return null; +};