diff --git a/core/keyboard_nav/ast_node.js b/core/keyboard_nav/ast_node.js
new file mode 100644
index 000000000..53d77c053
--- /dev/null
+++ b/core/keyboard_nav/ast_node.js
@@ -0,0 +1,705 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2019 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * 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 an ast node.
+ * Used to traverse the blockly ast.
+ */
+'use strict';
+
+goog.provide('Blockly.ASTNode');
+
+/**
+ * Class for an ast node.
+ * It is recommended that you use one of the createNode methods instead of
+ * creating a node directly.
+ * @constructor
+ * @param {!string} type The type of the location.
+ * Must be in Bockly.ASTNode.types.
+ * @param {Blockly.Block|Blockly.Connection|Blockly.Field|Blockly.Workspace}
+ * location The position in the ast.
+ * @param {Object=} params Optional dictionary of options.
+ */
+Blockly.ASTNode = function(type, location, params) {
+ if (!location) {
+ throw Error('Cannot create a node without a location.');
+ }
+
+ /**
+ * The type of the location.
+ * One of Blockly.ASTNode.types
+ * @type {string}
+ * @private
+ */
+ this.type_ = type;
+
+ /**
+ * Whether the location points to a connection.
+ * @type {boolean}
+ * @private
+ */
+ this.isConnection_ = Blockly.ASTNode.isConnectionType(type);
+
+ /**
+ * The location of the ast node.
+ * @type {!(Blockly.Block|Blockly.Connection|Blockly.Field|Blockly.Workspace)}
+ * @private
+ */
+ this.location_ = location;
+
+ this.processParams_(params || null);
+
+};
+
+/**
+ * Object holding different types for an ast node.
+ */
+Blockly.ASTNode.types = {
+ FIELD: 'field',
+ BLOCK: 'block',
+ INPUT: 'input',
+ OUTPUT: 'output',
+ NEXT: 'next',
+ PREVIOUS: 'previous',
+ STACK: 'stack',
+ WORKSPACE: 'workspace'
+};
+
+/**
+ * The amount to move the workspace coordinate to the left or right.
+ * This occurs when we get the next or previous node from a workspace node.
+ * @type {number}
+ * @private
+ */
+Blockly.ASTNode.wsMove_ = 10;
+
+/**
+ * The default y offset to use when moving the cursor from a stack to the
+ * workspace.
+ * @type {number}
+ * @private
+ */
+Blockly.ASTNode.DEFAULT_OFFSET_Y = -20;
+
+/**
+ * Whether an ast node of the given type points to a connection.
+ * @param {string} type The type to check. One of Blockly.ASTNode.types.
+ * @return {boolean} True if a node of the given type points to a connection.
+ * @package
+ */
+Blockly.ASTNode.isConnectionType = function(type) {
+ switch (type) {
+ case Blockly.ASTNode.types.PREVIOUS:
+ case Blockly.ASTNode.types.NEXT:
+ case Blockly.ASTNode.types.INPUT:
+ case Blockly.ASTNode.types.OUTPUT:
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Create an ast node pointing to a field.
+ * @param {Blockly.Field} field The location of the ast node.
+ * @return {Blockly.ASTNode} An ast node pointing to a field.
+ */
+Blockly.ASTNode.createFieldNode = function(field) {
+ return new Blockly.ASTNode(Blockly.ASTNode.types.FIELD, field);
+};
+
+/**
+ * Creates an ast node pointing to a connection. If the connection has a parent
+ * input then create an ast node of type input that will hold the connection.
+ * @param {Blockly.Connection} connection This is the connection the node will
+ * point to.
+ * @return {Blockly.ASTNode} An ast node pointing to a connection.
+ */
+Blockly.ASTNode.createConnectionNode = function(connection) {
+ if (!connection){
+ return null;
+ }
+ if (connection.type === Blockly.INPUT_VALUE) {
+ return Blockly.ASTNode.createInputNode(connection.getParentInput());
+ } else if (connection.type === Blockly.NEXT_STATEMENT
+ && connection.getParentInput()) {
+ return Blockly.ASTNode.createInputNode(connection.getParentInput());
+ } else if (connection.type === Blockly.NEXT_STATEMENT) {
+ return new Blockly.ASTNode(Blockly.ASTNode.types.NEXT, connection);
+ } else if (connection.type === Blockly.OUTPUT_VALUE) {
+ return new Blockly.ASTNode(Blockly.ASTNode.types.OUTPUT, connection);
+ } else if (connection.type === Blockly.PREVIOUS_STATEMENT) {
+ return new Blockly.ASTNode(Blockly.ASTNode.types.PREVIOUS, connection);
+ }
+ return null;
+};
+
+/**
+ * Creates an ast node pointing to an input. Stores the input connection as the
+ * location.
+ * @param {Blockly.Input} input The input used to create an ast node.
+ * @return {Blockly.ASTNode} An ast node pointing to a input.
+ */
+Blockly.ASTNode.createInputNode = function(input) {
+ if (!input) {
+ return null;
+ }
+ var params = {
+ "input": input
+ };
+ return new Blockly.ASTNode(Blockly.ASTNode.types.INPUT, input.connection, params);
+};
+
+/**
+ * Creates an ast node pointing to a block.
+ * @param {Blockly.Block} block The block used to create an ast node.
+ * @return {Blockly.ASTNode} An ast node pointing to a block.
+ */
+Blockly.ASTNode.createBlockNode = function(block) {
+ return new Blockly.ASTNode(Blockly.ASTNode.types.BLOCK, block);
+};
+
+/**
+ * Create an ast node of type stack. A stack, represented by its top block, is
+ * the set of all blocks connected to a top block, including the top block.
+ * @param {Blockly.Block} topBlock A top block has no parent and can be found
+ * in the list returned by workspace.getTopBlocks().
+ * @return {Blockly.ASTNode} An ast node of type stack that points to the top
+ * block on the stack.
+ */
+Blockly.ASTNode.createStackNode = function(topBlock) {
+ return new Blockly.ASTNode(Blockly.ASTNode.types.STACK, topBlock);
+};
+
+/**
+ * Creates an ast node pointing to a workpsace.
+ * @param {Blockly.Workspace} workspace The workspace that we are on.
+ * @param {Blockly.utils.Coordinate} wsCoordinate The position on the workspace for
+ * this node.
+ * @return {Blockly.ASTNode} An ast node pointing to a workspace and a position
+ * on the workspace.
+ */
+Blockly.ASTNode.createWorkspaceNode = function(workspace, wsCoordinate) {
+ var params = {
+ "wsCoordinate": wsCoordinate
+ };
+ return new Blockly.ASTNode(
+ Blockly.ASTNode.types.WORKSPACE, workspace, params);
+};
+
+/**
+ * Parse the optional parameters.
+ * @param {Object} params The user specified parameters.
+ * @private
+ */
+Blockly.ASTNode.prototype.processParams_ = function(params){
+ if (!params) {
+ return;
+ }
+ if (params['wsCoordinate']) {
+ this.wsCoordinate_ = params['wsCoordinate'];
+ } else if (params['input']) {
+ this.parentInput_ = params['input'];
+ }
+};
+
+/**
+ * Gets the value pointed to by this node.
+ * It is the callers responsibility to check the node type to figure out what
+ * type of object they get back from this.
+ * @return {!(Blockly.Field|Blockly.Connection|Blockly.Block|Blockly.Workspace)}
+ * The current field, connection, workspace, or block the cursor is on.
+ */
+Blockly.ASTNode.prototype.getLocation = function() {
+ return this.location_;
+};
+
+/**
+ * The type of the current location.
+ * One of Blockly.ASTNode.types
+ * @return {string} The type of the location.
+ */
+Blockly.ASTNode.prototype.getType = function() {
+ return this.type_;
+};
+
+/**
+ * The coordinate on the workspace.
+ * @return {Blockly.utils.Coordinate} The workspace coordinate or null if the
+ * location is not a workspace.
+ */
+Blockly.ASTNode.prototype.getWsCoordinate = function() {
+ return this.wsCoordinate_;
+};
+
+/**
+ * Get the parent input of the location.
+ * @return {Blockly.Input} The parent input of the location or null if the node
+ * is not input type.
+ * @package
+ */
+Blockly.ASTNode.prototype.getParentInput = function() {
+ return this.parentInput_;
+};
+
+/**
+ * Whether the node points to a connection.
+ * @return {boolean} [description]
+ * @package
+ */
+Blockly.ASTNode.prototype.isConnection = function() {
+ return this.isConnection_;
+};
+
+/**
+ * Get either the previous editable field, or get the first editable field for
+ * the given input.
+ * @param {!(Blockly.Field|Blockly.Connection)} location The current location of
+ * the cursor, which must be a field or connection.
+ * @param {!Blockly.Input} parentInput The parentInput of the field.
+ * @param {boolean=} opt_last If true find the last editable field otherwise get
+ * the previous field.
+ * @return {Blockly.ASTNode} The ast node holding the previous or last field or
+ * null if no previous field exists.
+ * @private
+ */
+Blockly.ASTNode.prototype.findPreviousEditableField_ = function(location,
+ parentInput, opt_last) {
+ var fieldRow = parentInput.fieldRow;
+ var fieldIdx = fieldRow.indexOf(location);
+ var previousField = null;
+ var startIdx = opt_last ? fieldRow.length - 1 : fieldIdx - 1;
+ for (var i = startIdx, field; field = fieldRow[i]; i--) {
+ if (field.isCurrentlyEditable()) {
+ previousField = field;
+ return Blockly.ASTNode.createFieldNode(previousField);
+ }
+ }
+ return null;
+};
+
+/**
+ * Given an input find the next editable field or an input with a non null
+ * connection in the same block. The current location must be an input
+ * connection.
+ * @return {Blockly.ASTNode} The ast node holding the next field or connection
+ * or null if there is no editable field or input connection after the given
+ * input.
+ * @private
+ */
+Blockly.ASTNode.prototype.findNextForInput_ = function() {
+ var parentInput = this.location_.getParentInput();
+ var block = parentInput.getSourceBlock();
+ var curIdx = block.inputList.indexOf(parentInput);
+ for (var i = curIdx + 1, input; input = block.inputList[i]; i++) {
+ var fieldRow = input.fieldRow;
+ for (var j = 0, field; field = fieldRow[j]; j++) {
+ if (field.isCurrentlyEditable()) {
+ return Blockly.ASTNode.createFieldNode(field);
+ }
+ }
+ if (input.connection) {
+ return Blockly.ASTNode.createInputNode(input);
+ }
+ }
+ return null;
+};
+
+/**
+ * Given a field find the next editable field or an input with a non null
+ * connection in the same block. The current location must be a field.
+ * @return {Blockly.ASTNode} The ast node pointing to the next field or
+ * connection or null if there is no editable field or input connection
+ * after the given input.
+ * @private
+ */
+Blockly.ASTNode.prototype.findNextForField_ = function() {
+ var location = this.location_;
+ var input = location.getParentInput();
+ var block = location.getSourceBlock();
+ var curIdx = block.inputList.indexOf(input);
+ var fieldIdx = input.fieldRow.indexOf(location) + 1;
+ for (var i = curIdx, input; input = block.inputList[i]; i++) {
+ var fieldRow = input.fieldRow;
+ while (fieldIdx < fieldRow.length) {
+ if (fieldRow[fieldIdx].isCurrentlyEditable()) {
+ return Blockly.ASTNode.createFieldNode(fieldRow[fieldIdx]);
+ }
+ fieldIdx++;
+ }
+ fieldIdx = 0;
+ if (input.connection) {
+ return Blockly.ASTNode.createInputNode(input);
+ }
+ }
+ return null;
+};
+
+/**
+ * Given an input find the previous editable field or an input with a non null
+ * connection in the same block. The current location must be an input
+ * connection.
+ * @return {Blockly.ASTNode} The ast node holding the previous field or
+ * connection.
+ * @private
+ */
+Blockly.ASTNode.prototype.findPrevForInput_ = function(){
+ var location = this.location_.getParentInput();
+ var block = location.getSourceBlock();
+ var curIdx = block.inputList.indexOf(location);
+ for (var i = curIdx, input; input = block.inputList[i]; i--) {
+ if (input.connection && input !== location) {
+ return Blockly.ASTNode.createInputNode(input);
+ }
+ var fieldRow = input.fieldRow;
+ for (var j = fieldRow.length - 1, field; field = fieldRow[j]; j--) {
+ if (field.isCurrentlyEditable()) {
+ return Blockly.ASTNode.createFieldNode(field);
+ }
+ }
+ }
+ return null;
+};
+
+/**
+ * Given a field find the previous editable field or an input with a non null
+ * connection in the same block. The current location must be a field.
+ * @return {Blockly.ASTNode} The ast node holding the previous input or field.
+ * @private
+ */
+Blockly.ASTNode.prototype.findPrevForField_ = function() {
+ var location = this.location_;
+ var parentInput = location.getParentInput();
+ var block = location.getSourceBlock();
+ var curIdx = block.inputList.indexOf(parentInput);
+ var fieldIdx = parentInput.fieldRow.indexOf(location) - 1;
+ for (var i = curIdx, input; input = block.inputList[i]; i--) {
+ if (input.connection && input !== parentInput) {
+ return Blockly.ASTNode.createInputNode(input);
+ }
+ var fieldRow = input.fieldRow;
+ while (fieldIdx > -1) {
+ if (fieldRow[fieldIdx].isCurrentlyEditable()) {
+ return Blockly.ASTNode.createFieldNode(fieldRow[fieldIdx]);
+ }
+ fieldIdx--;
+ }
+ //Reset the fieldIdx to the length of the field row of the previous input
+ if (i - 1 >= 0) {
+ fieldIdx = block.inputList[i - 1].fieldRow.length - 1;
+ }
+ }
+ return null;
+};
+
+/**
+ * Navigate between stacks of blocks on the workspace.
+ * @param {boolean} forward True to go forward. False to go backwards.
+ * @return {Blockly.ASTNode} The first block of the next stack or null if there
+ * are no blocks on the workspace.
+ * @private
+ */
+Blockly.ASTNode.prototype.navigateBetweenStacks_ = function(forward) {
+ var curLocation = this.getLocation();
+ if (!(curLocation instanceof Blockly.Block)) {
+ curLocation = curLocation.getSourceBlock();
+ }
+ if (!curLocation) {
+ return null;
+ }
+ var curRoot = curLocation.getRootBlock();
+ var topBlocks = curRoot.workspace.getTopBlocks(true);
+ for (var i = 0, topBlock; topBlock = topBlocks[i]; i++) {
+ if (curRoot.id == topBlock.id) {
+ var offset = forward ? 1 : -1;
+ var resultIndex = i + offset;
+ if (resultIndex == -1 || resultIndex == topBlocks.length) {
+ return null;
+ }
+ return Blockly.ASTNode.createStackNode(topBlocks[resultIndex]);
+ }
+ }
+ throw Error('Couldn\'t find ' + (forward ? 'next' : 'previous') +
+ ' stack?!?!?!');
+};
+
+/**
+ * Finds the top most ast node for a given block.
+ * This is either the previous connection, output connection or block depending
+ * on what kind of connections the block has.
+ * @param {Blockly.Block} block The block that we want to find the top
+ * connection on.
+ * @return {!Blockly.ASTNode} The ast node containing the top connection.
+ * @private
+ */
+Blockly.ASTNode.prototype.findTopASTNodeForBlock_ = function(block) {
+ var topConnection = block.previousConnection || block.outputConnection;
+ if (topConnection) {
+ return Blockly.ASTNode.createConnectionNode(topConnection);
+ } else {
+ return Blockly.ASTNode.createBlockNode(block);
+ }
+};
+
+/**
+ * Get the ast node pointing to the input that the block is nested under or if
+ * the block is not nested then get the stack ast node.
+ * @param {Blockly.Block} block The source block of the current location.
+ * @return {Blockly.ASTNode} The ast node pointing to the input connection or
+ * the top block of the stack this block is in.
+ * @private
+ */
+Blockly.ASTNode.prototype.getOutAstNodeForBlock_ = function(block) {
+ var topBlock = null;
+ //If the block doesn't have a previous connection then it is the top of the
+ //substack
+ if (!block.previousConnection) {
+ topBlock = block;
+ } else {
+ topBlock = this.findTopOfSubStack_(block);
+ }
+ var topConnection = topBlock.previousConnection || topBlock.outputConnection;
+ //If the top connection has a parentInput, create an ast node pointing to that input
+ if (topConnection && topConnection.targetConnection &&
+ topConnection.targetConnection.getParentInput()) {
+ return Blockly.ASTNode.createInputNode(
+ topConnection.targetConnection.getParentInput());
+ } else {
+ //Go to stack level if you are not underneath an input
+ return Blockly.ASTNode.createStackNode(topBlock);
+ }
+};
+
+/**
+ * Find the first editable field or input with a connection on a given block.
+ * @param {!Blockly.BlockSvg} block The source block of the current location.
+ * @return {Blockly.ASTNode} An ast node pointing to the first field or input.
+ * Null if there are no editable fields or inputs with connections on the block.
+ * @private
+ */
+Blockly.ASTNode.prototype.findFirstFieldOrInput_ = function(block) {
+ var inputs = block.inputList;
+ for (var i = 0, input; input = inputs[i]; i++) {
+ var fieldRow = input.fieldRow;
+ for (var j = 0, field; field = fieldRow[j]; j++) {
+ if (field.isCurrentlyEditable()) {
+ return Blockly.ASTNode.createFieldNode(field);
+ }
+ }
+ if (input.connection) {
+ return Blockly.ASTNode.createInputNode(input);
+ }
+ }
+ return null;
+};
+
+/**
+ * Walk backwards from the given block up through the stack of blocks to find
+ * the top block of the sub stack. If we are nested in a statement input only
+ * find the top most nested block. Do not go all the way to the top of the
+ * stack.
+ * @param {!Blockly.Block} sourceBlock A block in the stack.
+ * @return {!Blockly.Block} The top block in a stack.
+ * @private
+ */
+Blockly.ASTNode.prototype.findTopOfSubStack_ = function(sourceBlock) {
+ var topBlock = sourceBlock;
+ while (topBlock && topBlock.previousConnection
+ && topBlock.previousConnection.targetConnection
+ && topBlock.previousConnection.targetBlock().nextConnection
+ == topBlock.previousConnection.targetConnection) {
+ topBlock = topBlock.previousConnection.targetBlock();
+ }
+ return topBlock;
+};
+
+/**
+ * Find the element to the right of the current element in the ast.
+ * @return {Blockly.ASTNode} An ast node that wraps the next field, connection,
+ * block, or workspace. Or null if there is no node to the right.
+ */
+Blockly.ASTNode.prototype.next = function() {
+ switch (this.type_) {
+ case Blockly.ASTNode.types.WORKSPACE:
+ //TODO: Need to limit this. The view is bounded to half a screen beyond
+ //the furthest block.
+ var newX = this.wsCoordinate_.x + Blockly.ASTNode.wsMove_;
+ var newWsCoordinate = new Blockly.utils.Coordinate(newX, this.wsCoordinate_.y);
+ var workspace = /** @type {Blockly.Workspace} */ (this.location_);
+ return Blockly.ASTNode.createWorkspaceNode(workspace,
+ newWsCoordinate);
+
+ case Blockly.ASTNode.types.STACK:
+ return this.navigateBetweenStacks_(true);
+
+ case Blockly.ASTNode.types.OUTPUT:
+ return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock());
+
+ case Blockly.ASTNode.types.FIELD:
+ return this.findNextForField_();
+
+ case Blockly.ASTNode.types.INPUT:
+ return this.findNextForInput_();
+
+ case Blockly.ASTNode.types.BLOCK:
+ var nextConnection = this.location_.nextConnection;
+ if (nextConnection) {
+ return Blockly.ASTNode.createConnectionNode(nextConnection);
+ }
+ break;
+
+ case Blockly.ASTNode.types.PREVIOUS:
+ return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock());
+
+ case Blockly.ASTNode.types.NEXT:
+ var targetConnection = this.location_.targetConnection;
+ if (targetConnection) {
+ return Blockly.ASTNode.createConnectionNode(targetConnection);
+ }
+ break;
+ }
+
+ return null;
+};
+
+/**
+ * Find the element one level below and all the way to the left of the current
+ * location.
+ * @return {Blockly.ASTNode} An ast node that wraps the next field, connection,
+ * workspace, or block. Or null if there is nothing below this node.
+ */
+Blockly.ASTNode.prototype.in = function() {
+ switch (this.type_) {
+ case Blockly.ASTNode.types.WORKSPACE:
+ var topBlocks = this.location_.getTopBlocks(true);
+ if (topBlocks.length > 0) {
+ return Blockly.ASTNode.createStackNode(topBlocks[0]);
+ }
+ break;
+
+ case Blockly.ASTNode.types.STACK:
+ var block = /** @type {!Blockly.Block} */ (this.location_);
+ return this.findTopASTNodeForBlock_(block);
+
+ case Blockly.ASTNode.types.BLOCK:
+ return this.findFirstFieldOrInput_(this.location_);
+
+ case Blockly.ASTNode.types.INPUT:
+ var targetConnection = this.location_.targetConnection;
+ if (targetConnection) {
+ return Blockly.ASTNode.createConnectionNode(targetConnection);
+ }
+ break;
+
+ }
+ return null;
+};
+
+/**
+ * Find the element to the left of the current element in the ast.
+ * @return {Blockly.ASTNode} An ast node that wraps the previous field,
+ * connection, workspace or block. Or null if no node exists to the left.
+ * null.
+ */
+Blockly.ASTNode.prototype.prev = function() {
+ switch (this.type_) {
+ case Blockly.ASTNode.types.WORKSPACE:
+ var newX = this.wsCoordinate_.x - Blockly.ASTNode.wsMove_;
+ var newCoord = new Blockly.utils.Coordinate(newX, this.wsCoordinate_.y);
+ var ws = /** @type {Blockly.Workspace} */ (this.location_);
+ return Blockly.ASTNode.createWorkspaceNode(ws, newCoord);
+
+ case Blockly.ASTNode.types.STACK:
+ return this.navigateBetweenStacks_(false);
+
+ case Blockly.ASTNode.types.OUTPUT:
+ return null;
+
+ case Blockly.ASTNode.types.FIELD:
+ return this.findPrevForField_();
+
+ case Blockly.ASTNode.types.INPUT:
+ return this.findPrevForInput_();
+
+ case Blockly.ASTNode.types.BLOCK:
+ var prevConnection = this.location_.previousConnection;
+ var outputConnection = this.location_.outputConnection;
+ var topConnection = prevConnection || outputConnection;
+ if (topConnection) {
+ return Blockly.ASTNode.createConnectionNode(topConnection);
+ }
+ break;
+
+ case Blockly.ASTNode.types.PREVIOUS:
+ var targetConnection = this.location_.targetConnection;
+ if (targetConnection) {
+ return Blockly.ASTNode.createConnectionNode(targetConnection);
+ }
+ break;
+
+ case Blockly.ASTNode.types.NEXT:
+ return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock());
+ }
+
+ return null;
+};
+
+/**
+ * Find the next element that is one position above and all the way to the left
+ * of the current location.
+ * @return {Blockly.ASTNode} An ast node that wraps the next field, connection,
+ * workspace or block. Or null if we are at the workspace level.
+ */
+Blockly.ASTNode.prototype.out = function() {
+ switch (this.type_) {
+ case Blockly.ASTNode.types.STACK:
+ var blockPos = this.location_.getRelativeToSurfaceXY();
+ //TODO: Make sure this is in the bounds of the workspace
+ var wsCoordinate = new Blockly.utils.Coordinate(
+ blockPos.x, blockPos.y + Blockly.ASTNode.DEFAULT_OFFSET_Y);
+ return Blockly.ASTNode.createWorkspaceNode(
+ this.location_.workspace, wsCoordinate);
+
+ case Blockly.ASTNode.types.OUTPUT:
+ var target = this.location_.targetConnection;
+ if (target) {
+ return Blockly.ASTNode.createConnectionNode(target);
+ } else {
+ return Blockly.ASTNode.createStackNode(this.location_.getSourceBlock());
+ }
+
+ case Blockly.ASTNode.types.FIELD:
+ return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock());
+
+ case Blockly.ASTNode.types.INPUT:
+ return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock());
+
+ case Blockly.ASTNode.types.BLOCK:
+ var block = /** @type {!Blockly.Block} */ (this.location_);
+ return this.getOutAstNodeForBlock_(block);
+
+ case Blockly.ASTNode.types.PREVIOUS:
+ return this.getOutAstNodeForBlock_(this.location_.getSourceBlock());
+
+ case Blockly.ASTNode.types.NEXT:
+ return this.getOutAstNodeForBlock_(this.location_.getSourceBlock());
+ }
+
+ return null;
+};
diff --git a/core/keyboard_nav/cursor.js b/core/keyboard_nav/cursor.js
new file mode 100644
index 000000000..df494fb09
--- /dev/null
+++ b/core/keyboard_nav/cursor.js
@@ -0,0 +1,163 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2019 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * 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.
+ * Used primarily for keyboard navigation.
+ */
+'use strict';
+
+goog.provide('Blockly.Cursor');
+
+/**
+ * Class for a cursor.
+ * @constructor
+ */
+Blockly.Cursor = function() {
+ /*
+ * The current location of the cursor.
+ * @type {Blockly.Field|Blockly.Connection|Blockly.Block}
+ * @private
+ */
+ this.curNode_ = null;
+};
+
+/**
+ * Object holding different types for a cursor.
+ */
+Blockly.Cursor.prototype.types = {
+ FIELD: 'field',
+ BLOCK: 'block',
+ INPUT: 'input',
+ OUTPUT: 'output',
+ NEXT: 'next',
+ PREVIOUS: 'previous',
+ STACK: 'stack',
+ WORKSPACE: 'workspace'
+};
+
+/**
+ * Gets the current location of the cursor.
+ * @return {Blockly.ASTNode} The current field, connection, or block the cursor
+ * is on.
+ */
+Blockly.Cursor.prototype.getCurNode = function() {
+ return this.curNode_;
+};
+
+/**
+ * Set the location of the cursor and call the update method.
+ * Setting isStack to true will only work if the newLocation is the top most
+ * output or previous connection on a stack.
+ * @param {Blockly.ASTNode} newNode The new location of the cursor.
+ */
+Blockly.Cursor.prototype.setLocation = function(newNode) {
+ this.curNode_ = newNode;
+ this.update_();
+};
+
+/**
+ * Update method to be overwritten in cursor_svg.
+ * @protected
+ */
+Blockly.Cursor.prototype.update_ = function() {};
+
+/**
+ * Find the next connection, field, or block.
+ * @return {Blockly.ASTNode} The next element, or null if the current node is
+ * not set or there is no next value.
+ */
+Blockly.Cursor.prototype.next = function() {
+ var curNode = this.getCurNode();
+ if (!curNode) {
+ return null;
+ }
+ var newNode = curNode.next();
+
+ if (newNode && newNode.getType() === Blockly.ASTNode.types.NEXT) {
+ newNode = newNode.next() || newNode;
+ }
+
+ if (newNode) {
+ this.setLocation(newNode);
+ }
+ return newNode;
+};
+
+/**
+ * Find the in connection or field.
+ * @return {Blockly.ASTNode} The in element, or null if the current node is
+ * not set or there is no in value.
+ */
+Blockly.Cursor.prototype.in = function() {
+ var curNode = this.getCurNode();
+ if (!curNode) {
+ return null;
+ }
+ var newNode = curNode.in();
+
+ if (newNode && newNode.getType() === Blockly.ASTNode.types.OUTPUT) {
+ newNode = newNode.next() || newNode;
+ }
+
+ if (newNode) {
+ this.setLocation(newNode);
+ }
+ return newNode;
+};
+
+/**
+ * Find the previous connection, field, or block.
+ * @return {Blockly.ASTNode} The previous element, or null if the current node
+ * is not set or there is no previous value.
+ */
+Blockly.Cursor.prototype.prev = function() {
+ var curNode = this.getCurNode();
+ if (!curNode) {
+ return null;
+ }
+ var newNode = curNode.prev();
+
+ if (newNode && newNode.getType() === Blockly.ASTNode.types.NEXT) {
+ newNode = newNode.prev() || newNode;
+ }
+
+ if (newNode) {
+ this.setLocation(newNode);
+ }
+ return newNode;
+};
+
+/**
+ * Find the out connection, field, or block.
+ * @return {Blockly.ASTNode} The out element, or null if the current node is
+ * not set or there is no out value.
+ */
+Blockly.Cursor.prototype.out = function() {
+ var curNode = this.getCurNode();
+ if (!curNode) {
+ return null;
+ }
+ var newNode = curNode.out();
+ if (newNode) {
+ this.setLocation(newNode);
+ }
+ return newNode;
+};
diff --git a/core/keyboard_nav/cursor_svg.js b/core/keyboard_nav/cursor_svg.js
new file mode 100644
index 000000000..d7101fb18
--- /dev/null
+++ b/core/keyboard_nav/cursor_svg.js
@@ -0,0 +1,499 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2019 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * 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 Methods for graphically rendering a cursor as SVG.
+ * @author samelh@microsoft.com (Sam El-Husseini)
+ */
+'use strict';
+
+goog.provide('Blockly.CursorSvg');
+goog.require('Blockly.Cursor');
+/**
+ * Class for a cursor.
+ * @param {!Blockly.Workspace} workspace The workspace to sit in.
+ * @param {?boolean} opt_isImmovable True if the cursor cannot be moved with
+ * calls to prev/next/in/out. This is called a marker.
+ * @extends {Blockly.Cursor}
+ * @constructor
+ */
+Blockly.CursorSvg = function(workspace, opt_isImmovable) {
+ Blockly.CursorSvg.superClass_.constructor.call(this);
+ this.workspace_ = workspace;
+ this.isMarker_ = opt_isImmovable || false;
+};
+goog.inherits(Blockly.CursorSvg, Blockly.Cursor);
+
+/**
+ * Height of the horizontal cursor.
+ * @type {number}
+ * @const
+ */
+Blockly.CursorSvg.CURSOR_HEIGHT = 5;
+
+/**
+ * Width of the horizontal cursor.
+ * @type {number}
+ * @const
+ */
+Blockly.CursorSvg.CURSOR_WIDTH = 100;
+
+/**
+ * The start length of the notch.
+ * @type {number}
+ * @const
+ */
+Blockly.CursorSvg.NOTCH_START_LENGTH = 24;
+
+/**
+ * Padding around the input.
+ * @type {number}
+ * @const
+ */
+Blockly.CursorSvg.VERTICAL_PADDING = 5;
+
+/**
+ * Padding around a stack.
+ * @type {number}
+ * @const
+ */
+Blockly.CursorSvg.STACK_PADDING = 10;
+/**
+ * Cursor color.
+ * @type {string}
+ * @const
+ */
+Blockly.CursorSvg.CURSOR_COLOR = '#cc0a0a';
+
+/**
+ * Immovable marker color.
+ * @type {string}
+ * @const
+ */
+Blockly.CursorSvg.MARKER_COLOR = '#4286f4';
+
+/**
+ * Parent svg element.
+ * This is generally a block's svg root, unless the cursor is on the workspace.
+ * @type {Element}
+ */
+Blockly.CursorSvg.prototype.parent_ = null;
+
+/**
+ * The current svg element for the cursor.
+ * @type {Element}
+ */
+Blockly.CursorSvg.prototype.currentCursorSvg = null;
+
+/**
+ * Return the root node of the SVG or null if none exists.
+ * @return {Element} The root SVG node.
+ */
+Blockly.CursorSvg.prototype.getSvgRoot = function() {
+ return this.svgGroup_;
+};
+
+/**
+ * Create the dom element for the cursor.
+ * @return {!Element} The cursor controls SVG group.
+ */
+Blockly.CursorSvg.prototype.createDom = function() {
+ this.svgGroup_ =
+ Blockly.utils.dom.createSvgElement('g', {
+ 'class': 'blocklyCursor'
+ }, null);
+
+ this.createCursorSvg_();
+ return this.svgGroup_;
+};
+
+/**
+ * Set parent of the cursor. This is so that the cursor will be on the correct
+ * svg group.
+ * @param {Element} newParent New parent of the cursor.
+ * @private
+ */
+Blockly.CursorSvg.prototype.setParent_ = function(newParent) {
+ var oldParent = this.parent_;
+ if (newParent == oldParent) {
+ return;
+ }
+
+ var svgRoot = this.getSvgRoot();
+
+ if (newParent) {
+ newParent.appendChild(svgRoot);
+ }
+ // If we are losing a parent, we want to move our DOM element to the
+ // root of the workspace.
+ else if (oldParent) {
+ this.workspace_.getCanvas().appendChild(svgRoot);
+ }
+ this.parent_ = newParent;
+};
+
+/**************************/
+/**** Display ****/
+/**************************/
+
+/**
+ * Show the cursor using coordinates.
+ * @private
+ */
+Blockly.CursorSvg.prototype.showWithCoordinates_ = function() {
+ var workspaceNode = this.getCurNode();
+ var wsCoordinate = workspaceNode.getWsCoordinate();
+ this.currentCursorSvg = this.cursorSvgLine_;
+ this.setParent_(this.workspace_.svgBlockCanvas_);
+ this.positionLine_(wsCoordinate.x, wsCoordinate.y, Blockly.CursorSvg.CURSOR_WIDTH);
+ this.showCurrent_();
+};
+
+/**
+ * Show the cursor using a block
+ * @private
+ */
+Blockly.CursorSvg.prototype.showWithBlock_ = function() {
+ //TODO: Change this from getLocation to something else
+ var block = this.getCurNode().getLocation();
+
+ this.currentCursorSvg = this.cursorSvgRect_;
+ this.setParent_(block.getSvgRoot());
+ this.positionRect_(0, 0, block.width , block.height);
+ this.showCurrent_();
+};
+
+/**
+ * Show the cursor using a connection with input or output type
+ * @private
+ */
+Blockly.CursorSvg.prototype.showWithInputOutput_ = function() {
+ var connection = /** @type {Blockly.Connection} */
+ (this.getCurNode().getLocation());
+ this.currentCursorSvg = this.cursorInputOutput_;
+ this.setParent_(connection.getSourceBlock().getSvgRoot());
+ this.positionInputOutput_(connection);
+ this.showCurrent_();
+};
+
+/**
+ * Show the cursor using a next connection
+ * @private
+ */
+Blockly.CursorSvg.prototype.showWithNext_ = function() {
+ var connection = this.getCurNode().getLocation();
+ var targetBlock = connection.getSourceBlock();
+ var x = 0;
+ var y = connection.getOffsetInBlock().y;
+ var width = targetBlock.getHeightWidth().width;
+
+ this.currentCursorSvg = this.cursorSvgLine_;
+ this.setParent_(connection.getSourceBlock().getSvgRoot());
+ this.positionLine_(x, y, width);
+ this.showCurrent_();
+};
+
+/**
+ * Show the cursor using a previous connection.
+ * @private
+ */
+Blockly.CursorSvg.prototype.showWithPrev_ = function() {
+ var connection = this.getCurNode().getLocation();
+ var targetBlock = connection.getSourceBlock();
+ var width = targetBlock.getHeightWidth().width;
+
+ this.currentCursorSvg = this.cursorSvgLine_;
+ this.setParent_(connection.getSourceBlock().getSvgRoot());
+ this.positionLine_(0, 0, width);
+ this.showCurrent_();
+};
+
+/**
+ * Show the cursor using a field.
+ * @private
+ */
+Blockly.CursorSvg.prototype.showWithField_ = function() {
+ var field = this.getCurNode().getLocation();
+ var width = field.borderRect_.width.baseVal.value;
+ var height = field.borderRect_.height.baseVal.value;
+
+ this.currentCursorSvg = this.cursorSvgRect_;
+ this.setParent_(field.getSvgRoot());
+ this.positionRect_(0, 0, width, height);
+ this.showCurrent_();
+};
+
+/**
+ * Show the cursor using a stack.
+ * @private
+ */
+Blockly.CursorSvg.prototype.showWithStack_ = function() {
+ var block = this.getCurNode().getLocation();
+
+ // Gets the height and width of entire stack.
+ var heightWidth = block.getHeightWidth();
+
+ // Add padding so that being on a stack looks different than being on a block.
+ var width = heightWidth.width + Blockly.CursorSvg.STACK_PADDING;
+ var height = heightWidth.height + Blockly.CursorSvg.STACK_PADDING;
+
+ // Shift the rectangle slightly to upper left so padding is equal on all sides.
+ var x = -1 * Blockly.CursorSvg.STACK_PADDING / 2;
+ var y = -1 * Blockly.CursorSvg.STACK_PADDING / 2;
+
+ // If the block has an output connection it needs more padding.
+ if (block.outputConnection) {
+ x -= Blockly.BlockSvg.TAB_WIDTH;
+ }
+
+ this.currentCursorSvg = this.cursorSvgRect_;
+ this.setParent_(block.getSvgRoot());
+
+ this.positionRect_(x, y, width, height);
+ this.showCurrent_();
+};
+
+
+/**************************/
+/**** Position ****/
+/**************************/
+
+/**
+ * Move and show the cursor at the specified coordinate in workspace units.
+ * @param {number} x The new x, in workspace units.
+ * @param {number} y The new y, in workspace units.
+ * @param {number} width The new width, in workspace units.
+ * @private
+ */
+Blockly.CursorSvg.prototype.positionLine_ = function(x, y, width) {
+ this.cursorSvgLine_.setAttribute('x', x);
+ this.cursorSvgLine_.setAttribute('y', y);
+ this.cursorSvgLine_.setAttribute('width', width);
+};
+
+/**
+ * Move and show the cursor at the specified coordinate in workspace units.
+ * @param {number} x The new x, in workspace units.
+ * @param {number} y The new y, in workspace units.
+ * @param {number} width The new width, in workspace units.
+ * @param {number} height The new height, in workspace units.
+ * @private
+ */
+Blockly.CursorSvg.prototype.positionRect_ = function(x, y, width, height) {
+ this.cursorSvgRect_.setAttribute('x', x);
+ this.cursorSvgRect_.setAttribute('y', y);
+ this.cursorSvgRect_.setAttribute('width', width);
+ this.cursorSvgRect_.setAttribute('height', height);
+};
+
+/**
+ * Position the cursor for an output connection.
+ * @param {Blockly.Connection} connection The connection to position cursor around.
+ * @private
+ */
+Blockly.CursorSvg.prototype.positionInputOutput_ = function(connection) {
+ var x = connection.getOffsetInBlock().x;
+ var y = connection.getOffsetInBlock().y;
+
+ this.cursorInputOutput_.setAttribute('transform', 'translate(' + x + ',' + y + ')' +
+ (connection.getSourceBlock().RTL ? ' scale(-1 1)' : ''));
+};
+
+/**
+ * Show the current cursor.
+ * @private
+ */
+Blockly.CursorSvg.prototype.showCurrent_ = function() {
+ this.hide();
+ this.currentCursorSvg.style.display = '';
+};
+
+/**
+ * Hide the cursor.
+ */
+Blockly.CursorSvg.prototype.hide = function() {
+ this.cursorSvgLine_.style.display = 'none';
+ this.cursorSvgRect_.style.display = 'none';
+ this.cursorInputOutput_.style.display = 'none';
+};
+
+/**
+ * Update the cursor.
+ * @package
+ */
+Blockly.CursorSvg.prototype.update_ = function() {
+ if (!this.getCurNode()) {
+ return;
+ }
+ var curNode = this.getCurNode();
+ if (curNode.getType() === Blockly.ASTNode.types.BLOCK) {
+ this.showWithBlock_();
+ //This needs to be the location type because next connections can be input
+ //type but they need to draw like they are a next statement
+ } else if (curNode.getLocation().type === Blockly.INPUT_VALUE
+ || curNode.getType() === Blockly.ASTNode.types.OUTPUT) {
+ this.showWithInputOutput_();
+ } else if (curNode.getLocation().type === Blockly.NEXT_STATEMENT) {
+ this.showWithNext_();
+ } else if (curNode.getType() === Blockly.ASTNode.types.PREVIOUS) {
+ this.showWithPrev_();
+ } else if (curNode.getType() === Blockly.ASTNode.types.FIELD) {
+ this.showWithField_();
+ } else if (curNode.getType() === Blockly.ASTNode.types.WORKSPACE) {
+ this.showWithCoordinates_();
+ } else if (curNode.getType() === Blockly.ASTNode.types.STACK) {
+ this.showWithStack_();
+ }
+};
+
+/**
+ * Create the cursor svg.
+ * @return {Element} The SVG node created.
+ * @private
+ */
+Blockly.CursorSvg.prototype.createCursorSvg_ = function() {
+ /* This markup will be generated and added to the .svgGroup_:
+
+
+
+
+
+ */
+
+ var colour = this.isMarker_ ? Blockly.CursorSvg.MARKER_COLOR :
+ Blockly.CursorSvg.CURSOR_COLOR;
+ this.cursorSvg_ = Blockly.utils.dom.createSvgElement('g',
+ {
+ 'width': Blockly.CursorSvg.CURSOR_WIDTH,
+ 'height': Blockly.CursorSvg.CURSOR_HEIGHT
+ }, this.svgGroup_);
+
+ this.cursorSvgLine_ = Blockly.utils.dom.createSvgElement('rect',
+ {
+ 'x': '0',
+ 'y': '0',
+ 'fill': colour,
+ 'width': Blockly.CursorSvg.CURSOR_WIDTH,
+ 'height': Blockly.CursorSvg.CURSOR_HEIGHT,
+ 'style': 'display: none;'
+ },
+ this.cursorSvg_);
+
+ this.cursorSvgRect_ = Blockly.utils.dom.createSvgElement('rect',
+ {
+ 'class': 'blocklyVerticalCursor',
+ 'x': '0',
+ 'y': '0',
+ 'rx': '10', 'ry': '10',
+ 'style': 'display: none;',
+ 'stroke': colour
+ },
+ this.cursorSvg_);
+
+ this.cursorInputOutput_ = Blockly.utils.dom.createSvgElement(
+ 'path',
+ {
+ 'width': Blockly.CursorSvg.CURSOR_WIDTH,
+ 'height': Blockly.CursorSvg.CURSOR_HEIGHT,
+ 'd': 'm 0,0 ' + Blockly.BlockSvg.TAB_PATH_DOWN + ' v 5',
+ 'transform':'',
+ 'style':'display: none;',
+ 'fill': colour
+ },
+ this.cursorSvg_);
+
+ // Markers don't blink.
+ if (!this.isMarker_) {
+ Blockly.utils.dom.createSvgElement('animate',
+ {
+ 'attributeType': 'XML',
+ 'attributeName': 'fill',
+ 'dur': '1s',
+ 'values': Blockly.CursorSvg.CURSOR_COLOR + ';transparent;transparent;',
+ 'repeatCount': 'indefinite'
+ },
+ this.cursorSvgLine_);
+
+ Blockly.utils.dom.createSvgElement('animate',
+ {
+ 'attributeType': 'XML',
+ 'attributeName': 'fill',
+ 'dur': '1s',
+ 'values': Blockly.CursorSvg.CURSOR_COLOR + ';transparent;transparent;',
+ 'repeatCount': 'indefinite'
+ },
+ this.cursorInputOutput_);
+ }
+
+ return this.cursorSvg_;
+};
+
+/**
+ * Find the next connection, field, or block.
+ * Does nothing if this cursor is an immovable marker.
+ * @return {Blockly.ASTNode} The next element, or null if the current node is
+ * not set or there is no next value.
+ */
+Blockly.CursorSvg.prototype.next = function() {
+ if (this.isMarker_) {
+ return null;
+ }
+ return Blockly.CursorSvg.superClass_.next.call(this);
+};
+
+/**
+ * Find the in connection or field.
+ * Does nothing if this cursor is an immovable marker.
+ * @return {Blockly.ASTNode} The in element, or null if the current node is
+ * not set or there is no in value.
+ */
+Blockly.CursorSvg.prototype.in = function() {
+ if (this.isMarker_) {
+ return null;
+ }
+ return Blockly.CursorSvg.superClass_.in.call(this);
+};
+
+/**
+ * Find the previous connection, field, or block.
+ * Does nothing if this cursor is an immovable marker.
+ * @return {Blockly.ASTNode} The previous element, or null if the current node
+ * is not set or there is no previous value.
+ */
+Blockly.CursorSvg.prototype.prev = function() {
+ if (this.isMarker_) {
+ return null;
+ }
+ return Blockly.CursorSvg.superClass_.prev.call(this);
+};
+
+/**
+ * Find the out connection, field, or block.
+ * Does nothing if this cursor is an immovable marker.
+ * @return {Blockly.ASTNode} The out element, or null if the current node is
+ * not set or there is no out value.
+ */
+Blockly.CursorSvg.prototype.out = function() {
+ if (this.isMarker_) {
+ return null;
+ }
+ return Blockly.CursorSvg.superClass_.out.call(this);
+};
diff --git a/core/keyboard_nav/navigation.js b/core/keyboard_nav/navigation.js
new file mode 100644
index 000000000..8cf1bc631
--- /dev/null
+++ b/core/keyboard_nav/navigation.js
@@ -0,0 +1,832 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2019 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * 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.
+ */
+
+goog.provide('Blockly.Navigation');
+
+goog.require('Blockly.ASTNode');
+
+/**
+ * The cursor for keyboard navigation.
+ * @type {Blockly.Cursor}
+ * @private
+ */
+Blockly.Navigation.cursor_ = null;
+
+/**
+ * The marker that shows where a user has marked while navigating blocks.
+ * @type {!Blockly.CursorSvg}
+ */
+Blockly.Navigation.marker_ = null;
+
+/**
+ * The current selected category if the toolbox is open or
+ * last selected category if focus is on a different element.
+ * @type {goog.ui.tree.BaseNode}
+ * @private
+ */
+Blockly.Navigation.currentCategory_ = null;
+
+/**
+ * The current selected block in the flyout.
+ * @type {Blockly.BlockSvg}
+ * @private
+ */
+Blockly.Navigation.flyoutBlock_ = null;
+
+/**
+ * A function to call to give feedback to the user about logs, warnings, and
+ * errors. You can override this to customize feedback (e.g. warning sounds,
+ * reading out the warning text, etc).
+ * Null by default.
+ * The first argument is one of 'log', 'warn', and 'error'.
+ * The second argument is the message.
+ * @type {function(string, string)}
+ * @public
+ */
+Blockly.Navigation.loggingCallback = null;
+
+/**
+ * State indicating focus is currently on the flyout.
+ * @type {number}
+ */
+Blockly.Navigation.STATE_FLYOUT = 1;
+
+/**
+ * State indicating focus is currently on the workspace.
+ * @type {number}
+ */
+Blockly.Navigation.STATE_WS = 2;
+
+/**
+ * State indicating focus is currently on the toolbox.
+ * @type {number}
+ */
+Blockly.Navigation.STATE_TOOLBOX = 3;
+
+/**
+ * The current state the user is in.
+ * Initialized to workspace state since a user enters navigation mode by shift
+ * clicking on a block or workspace.
+ * @type {number}
+ * @private
+ */
+Blockly.Navigation.currentState_ = Blockly.Navigation.STATE_WS;
+
+/**
+ * Set the navigation cursor.
+ * @param {Blockly.Cursor} cursor The cursor to navigate through blocks on a
+ * workspace.
+ * @package
+ */
+Blockly.Navigation.setCursor = function(cursor) {
+ Blockly.Navigation.cursor_ = cursor;
+};
+
+/**
+ * Set the navigation marker.
+ * @param {Blockly.CursorSvg} marker The marker that shows where a user has
+ * marked while navigating blocks.
+ * @package
+ */
+Blockly.Navigation.setMarker = function(marker) {
+ Blockly.Navigation.marker_ = marker;
+};
+
+/**
+ * Move the marker to the cursor's current location.
+ * @package
+ */
+Blockly.Navigation.markAtCursor = function() {
+ // TODO: bring the cursor (blinking) in front of the marker (solid)
+ Blockly.Navigation.marker_.setLocation(
+ Blockly.Navigation.cursor_.getCurNode());
+};
+
+/**
+ * Remove the marker from its current location and hide it.
+ * @package
+ */
+Blockly.Navigation.removeMark = function() {
+ Blockly.Navigation.marker_.setLocation(null);
+ Blockly.Navigation.marker_.hide();
+};
+
+/************************/
+/** Toolbox Navigation **/
+/************************/
+
+/**
+ * Set the state to the toolbox state and the current category as the first
+ * category.
+ */
+Blockly.Navigation.focusToolbox = function() {
+ Blockly.Navigation.resetFlyout(false /* shouldHide */);
+ Blockly.Navigation.currentState_ = Blockly.Navigation.STATE_TOOLBOX;
+ var workspace = Blockly.getMainWorkspace();
+ var toolbox = workspace.getToolbox();
+
+ if (!Blockly.Navigation.marker_.getCurNode()) {
+ Blockly.Navigation.markAtCursor();
+ }
+ if (workspace && !Blockly.Navigation.currentCategory_) {
+ Blockly.Navigation.currentCategory_ = toolbox.tree_.firstChild_;
+ }
+ toolbox.tree_.setSelectedItem(Blockly.Navigation.currentCategory_);
+};
+
+/**
+ * Select the next category.
+ * Taken from closure/goog/ui/tree/basenode.js
+ */
+Blockly.Navigation.nextCategory = function() {
+ if (!Blockly.Navigation.currentCategory_) {
+ return;
+ }
+ var curCategory = Blockly.Navigation.currentCategory_;
+ var nextNode = curCategory.getNextShownNode();
+
+ if (nextNode) {
+ nextNode.select();
+ Blockly.Navigation.currentCategory_ = nextNode;
+ }
+};
+
+/**
+ * Select the previous category.
+ * Taken from closure/goog/ui/tree/basenode.js
+ */
+Blockly.Navigation.previousCategory = function() {
+ if (!Blockly.Navigation.currentCategory_) {
+ return;
+ }
+ var curCategory = Blockly.Navigation.currentCategory_;
+ var previousNode = curCategory.getPreviousShownNode();
+
+ if (previousNode) {
+ previousNode.select();
+ Blockly.Navigation.currentCategory_ = previousNode;
+ }
+};
+
+/**
+ * Go to child category if there is a nested category.
+ * Taken from closure/goog/ui/tree/basenode.js
+ */
+Blockly.Navigation.inCategory = function() {
+ if (!Blockly.Navigation.currentCategory_) {
+ return;
+ }
+ var curCategory = Blockly.Navigation.currentCategory_;
+
+ if (curCategory.hasChildren()) {
+ if (!curCategory.getExpanded()) {
+ curCategory.setExpanded(true);
+ } else {
+ curCategory.getFirstChild().select();
+ Blockly.Navigation.currentCategory_ = curCategory.getFirstChild();
+ }
+ } else {
+ Blockly.Navigation.focusFlyout();
+ }
+};
+
+/**
+ * Go to parent category if we are in a child category.
+ * Taken from closure/goog/ui/tree/basenode.js
+ */
+Blockly.Navigation.outCategory = function() {
+ if (!Blockly.Navigation.currentCategory_) {
+ return;
+ }
+ var curCategory = Blockly.Navigation.currentCategory_;
+
+ if (curCategory.hasChildren() && curCategory.getExpanded() && curCategory.isUserCollapsible()) {
+ curCategory.setExpanded(false);
+ } else {
+ var parent = curCategory.getParent();
+ var tree = curCategory.getTree();
+ if (parent && (tree.getShowRootNode() || parent != tree)) {
+ parent.select();
+
+ Blockly.Navigation.currentCategory_ = /** @type {goog.ui.tree.BaseNode} */
+ (parent);
+ }
+ }
+};
+
+/***********************/
+/** Flyout Navigation **/
+/***********************/
+
+/**
+ * Change focus to the flyout.
+ */
+Blockly.Navigation.focusFlyout = function() {
+ var topBlock = null;
+ Blockly.Navigation.currentState_ = Blockly.Navigation.STATE_FLYOUT;
+ var workspace = Blockly.getMainWorkspace();
+ var toolbox = workspace.getToolbox();
+ var cursor = Blockly.Navigation.cursor_;
+ var flyout = toolbox ? toolbox.flyout_ : workspace.getFlyout();
+
+ if (!Blockly.Navigation.marker_.getCurNode()) {
+ Blockly.Navigation.markAtCursor();
+ }
+
+ if (flyout && flyout.getWorkspace()) {
+ var topBlocks = flyout.getWorkspace().getTopBlocks();
+ if (topBlocks.length > 0) {
+ topBlock = topBlocks[0];
+ Blockly.Navigation.flyoutBlock_ = topBlock;
+ var astNode = Blockly.ASTNode.createBlockNode(Blockly.Navigation.flyoutBlock_);
+ cursor.setLocation(astNode);
+ }
+ }
+};
+
+/**
+ * Select the next block in the flyout.
+ */
+Blockly.Navigation.selectNextBlockInFlyout = function() {
+ if (!Blockly.Navigation.flyoutBlock_) {
+ return;
+ }
+ var blocks = Blockly.Navigation.getFlyoutBlocks_();
+ var curBlock = Blockly.Navigation.flyoutBlock_;
+ var curIdx = blocks.indexOf(curBlock);
+ var cursor = Blockly.Navigation.cursor_;
+ var nextBlock;
+
+ if (curIdx > -1 && blocks[++curIdx]) {
+ nextBlock = blocks[curIdx];
+ }
+
+ if (nextBlock) {
+ Blockly.Navigation.flyoutBlock_ = nextBlock;
+ var astNode = Blockly.ASTNode.createBlockNode(nextBlock);
+ cursor.setLocation(astNode);
+ }
+};
+
+/**
+ * Select the previous block in the flyout.
+ */
+Blockly.Navigation.selectPreviousBlockInFlyout = function() {
+ if (!Blockly.Navigation.flyoutBlock_) {
+ return;
+ }
+ var blocks = Blockly.Navigation.getFlyoutBlocks_();
+ var curBlock = Blockly.Navigation.flyoutBlock_;
+ var curIdx = blocks.indexOf(curBlock);
+ var cursor = Blockly.Navigation.cursor_;
+ var prevBlock;
+
+ if (curIdx > -1 && blocks[--curIdx]) {
+ prevBlock = blocks[curIdx];
+ }
+
+ if (prevBlock) {
+ Blockly.Navigation.flyoutBlock_ = prevBlock;
+ var astNode = Blockly.ASTNode.createBlockNode(prevBlock);
+ cursor.setLocation(astNode);
+ }
+};
+
+/**
+ * Get a list of all blocks in the flyout.
+ * @return {!Array} List of blocks in the flyout.
+ */
+Blockly.Navigation.getFlyoutBlocks_ = function() {
+ var workspace = Blockly.getMainWorkspace();
+ var toolbox = workspace.getToolbox();
+ var topBlocks = [];
+ var flyout = toolbox ? toolbox.flyout_ : workspace.getFlyout();
+ if (flyout && flyout.getWorkspace()) {
+ topBlocks = flyout.getWorkspace().getTopBlocks();
+ }
+ return topBlocks;
+};
+
+/**
+ * If there is a marked connection try connecting the block from the flyout to
+ * that connection. If no connection has been marked then inserting will place
+ * it on the workspace.
+ */
+Blockly.Navigation.insertFromFlyout = function() {
+
+ var flyout = Blockly.getMainWorkspace().getFlyout();
+ if (!flyout || !flyout.isVisible()) {
+ Blockly.Navigation.warn('Trying to insert from the flyout when the flyout does not ' +
+ ' exist or is not visible');
+ return;
+ }
+
+ var newBlock = flyout.createBlock(Blockly.Navigation.flyoutBlock_);
+ // Render to get the sizing right.
+ newBlock.render();
+ // Connections are hidden when the block is first created. Normally there's
+ // enough time for them to become unhidden in the user's mouse movements,
+ // but not here.
+ newBlock.setConnectionsHidden(false);
+ Blockly.Navigation.cursor_.setLocation(
+ Blockly.ASTNode.createBlockNode(newBlock));
+ if (!Blockly.Navigation.modify()) {
+ Blockly.Navigation.warn('Something went wrong while inserting a block from the flyout.');
+ }
+
+ // Move the cursor to the right place on the inserted block.
+ Blockly.Navigation.focusWorkspace();
+ var prevConnection = newBlock.previousConnection;
+ var outConnection = newBlock.outputConnection;
+ var topConnection = prevConnection ? prevConnection : outConnection;
+ //TODO: This will have to be fixed when we add in a block that does not have
+ //a previous or output connection
+ var astNode = Blockly.ASTNode.createConnectionNode(topConnection);
+ Blockly.Navigation.cursor_.setLocation(astNode);
+ Blockly.Navigation.removeMark();
+};
+
+/**
+ * Reset flyout information, and optionally close the flyout.
+ * @param {boolean} shouldHide True if the flyout should be hidden.
+ */
+Blockly.Navigation.resetFlyout = function(shouldHide) {
+ var cursor = Blockly.Navigation.cursor_;
+ Blockly.Navigation.flyoutBlock_ = null;
+ cursor.hide();
+ if (shouldHide) {
+ cursor.workspace_.getFlyout().hide();
+ }
+};
+
+/************/
+/** Modify **/
+/************/
+
+/**
+ * Handle the modifier key (currently I for Insert).
+ * @return {boolean} True if the key was handled; false if something went wrong.
+ * @package
+ */
+Blockly.Navigation.modify = function() {
+ var markerNode = Blockly.Navigation.marker_.getCurNode();
+ var cursorNode = Blockly.Navigation.cursor_.getCurNode();
+
+ if (!markerNode) {
+ Blockly.Navigation.warn('Cannot insert with no marked node.');
+ return false;
+ }
+
+ if (!cursorNode) {
+ Blockly.Navigation.warn('Cannot insert with no cursor node.');
+ return false;
+ }
+ var markerType = markerNode.getType();
+ var cursorType = cursorNode.getType();
+
+ if (markerType == Blockly.ASTNode.types.FIELD) {
+ Blockly.Navigation.warn('Should not have been able to mark a field.');
+ return false;
+ }
+ if (markerType == Blockly.ASTNode.types.BLOCK) {
+ Blockly.Navigation.warn('Should not have been able to mark a block.');
+ return false;
+ }
+ if (markerType == Blockly.ASTNode.types.STACK) {
+ Blockly.Navigation.warn('Should not have been able to mark a stack.');
+ return false;
+ }
+
+ if (cursorType == Blockly.ASTNode.types.FIELD) {
+ Blockly.Navigation.warn('Cannot attach a field to anything else.');
+ return false;
+ }
+
+ if (cursorType == Blockly.ASTNode.types.WORKSPACE) {
+ Blockly.Navigation.warn('Cannot attach a workspace to anything else.');
+ return false;
+ }
+
+ var cursorLoc = cursorNode.getLocation();
+ var markerLoc = markerNode.getLocation();
+
+ if (markerNode.isConnection()) {
+ // TODO: Handle the case when one or both are already connected.
+ if (cursorNode.isConnection()) {
+ return Blockly.Navigation.connect(cursorLoc, markerLoc);
+ } else if (cursorType == Blockly.ASTNode.types.BLOCK ||
+ cursorType == Blockly.ASTNode.types.STACK) {
+ return Blockly.Navigation.insertBlock(cursorLoc, markerLoc);
+ }
+ } else if (markerType == Blockly.ASTNode.types.WORKSPACE) {
+ if (cursorNode.isConnection()) {
+ if (cursorType == Blockly.ASTNode.types.INPUT ||
+ cursorType == Blockly.ASTNode.types.NEXT) {
+ Blockly.Navigation.warn(
+ 'Cannot move a next or input connection to the workspace.');
+ return false;
+ }
+ var block = cursorLoc.getSourceBlock();
+ } else if (cursorType == Blockly.ASTNode.types.BLOCK ||
+ cursorType == Blockly.ASTNode.types.STACK) {
+ var block = cursorLoc;
+ } else {
+ return false;
+ }
+ if (block.isShadow()) {
+ Blockly.Navigation.warn('Cannot move a shadow block to the workspace.');
+ return false;
+ }
+ if (block.getParent()) {
+ block.unplug(false);
+ }
+ block.moveTo(markerNode.getWsCoordinate());
+ return true;
+ }
+ Blockly.Navigation.warn('Unexpected state in Blockly.Navigation.modify.');
+ return false;
+ // TODO: Make sure the cursor and marker end up in the right places.
+};
+
+/**
+ * Connect the moving connection to the targetConnection. Disconnect the moving
+ * connection if necessary, and and position the blocks so that the target
+ * connection does not move.
+ * @param {Blockly.RenderedConnection} movingConnection The connection to move.
+ * @param {Blockly.RenderedConnection} targetConnection The connection that
+ * stays stationary as the movingConnection attaches to it.
+ * @return {boolean} Whether the connection was successful.
+ * @package
+ */
+Blockly.Navigation.connect = function(movingConnection, targetConnection) {
+ if (movingConnection) {
+ var movingBlock = movingConnection.getSourceBlock();
+ if (targetConnection.type == Blockly.PREVIOUS_STATEMENT
+ || targetConnection.type == Blockly.OUTPUT_VALUE) {
+ movingBlock.positionNearConnection(movingConnection, targetConnection);
+ }
+ try {
+ targetConnection.connect(movingConnection);
+ return true;
+ }
+ catch (e) {
+ // TODO: Is there anything else useful to do at this catch?
+ // Perhaps position the block near the target connection?
+ Blockly.Navigation.warn('Connection failed with error: ' + e);
+ return false;
+ }
+ }
+ return false;
+};
+
+/**
+ * Finds our best guess of what connection point on the given block the user is
+ * trying to connect to given a target connection.
+ * @param {Blockly.Block} block The block to be connected.
+ * @param {Blockly.Connection} connection The connection to connect to.
+ * @return {Blockly.Connection} blockConnection The best connection we can
+ * determine for the block, or null if the block doesn't have a matching
+ * connection for the given target connection.
+ */
+Blockly.Navigation.findBestConnection = function(block, connection) {
+ if (!block || !connection) {
+ return null;
+ }
+
+ // TODO: Possibly check types and return null if the types don't match.
+ if (connection.type === Blockly.PREVIOUS_STATEMENT) {
+ return block.nextConnection;
+ } else if (connection.type === Blockly.NEXT_STATEMENT) {
+ return block.previousConnection;
+ } else if (connection.type === Blockly.INPUT_VALUE) {
+ return block.outputConnection;
+ } else if (connection.type === Blockly.OUTPUT_VALUE) {
+ // Select the first input that has a connection.
+ for (var i = 0; i < block.inputList.length; i++) {
+ var inputConnection = block.inputList[i].connection;
+ if (inputConnection.type === Blockly.INPUT_VALUE) {
+ return inputConnection;
+ }
+ }
+ }
+ return null;
+};
+
+/**
+ * Tries to connect the given block to the target connection, making an
+ * intelligent guess about which connection to use to on the moving block.
+ * @param {!Blockly.Block} block The block to move.
+ * @param {Blockly.Connection} targetConnection The connection to connect to.
+ * @return {boolean} Whether the connection was successful.
+ */
+Blockly.Navigation.insertBlock = function(block, targetConnection) {
+ var bestConnection =
+ Blockly.Navigation.findBestConnection(block, targetConnection);
+ if (bestConnection && bestConnection.isConnected() &&
+ !bestConnection.targetBlock().isShadow()) {
+ bestConnection.disconnect();
+ }
+ return Blockly.Navigation.connect(bestConnection, targetConnection);
+};
+
+/**
+ * Disconnect the connection that the cursor is pointing to, and bump blocks.
+ * This is a no-op if the connection cannot be broken or if the cursor is not
+ * pointing to a connection.
+ * @package
+ */
+Blockly.Navigation.disconnectBlocks = function() {
+ var curNode = Blockly.Navigation.cursor_.getCurNode();
+ if (!curNode.isConnection()) {
+ Blockly.Navigation.log('Cannot disconnect blocks when the cursor is not on a connection');
+ return;
+ }
+ var curConnection = curNode.getLocation();
+ if (!curConnection.isConnected()) {
+ Blockly.Navigation.log('Cannot disconnect unconnected connection');
+ return;
+ }
+ var superiorConnection =
+ curConnection.isSuperior() ? curConnection : curConnection.targetConnection;
+
+ var inferiorConnection =
+ curConnection.isSuperior() ? curConnection.targetConnection : curConnection;
+
+ if (inferiorConnection.getSourceBlock().isShadow()) {
+ Blockly.Navigation.log('Cannot disconnect a shadow block');
+ return;
+ }
+ superiorConnection.disconnect();
+ inferiorConnection.bumpAwayFrom_(superiorConnection);
+
+ var rootBlock = superiorConnection.getSourceBlock().getRootBlock();
+ rootBlock.bringToFront();
+
+ var connectionNode = Blockly.ASTNode.createConnectionNode(superiorConnection);
+ Blockly.Navigation.cursor_.setLocation(connectionNode);
+};
+
+/*************************/
+/** Keyboard Navigation **/
+/*************************/
+
+/**
+ * Sets the cursor to the previous or output connection of the selected block
+ * on the workspace.
+ * If no block is selected, places the cursor at a fixed point on the workspace.
+ */
+Blockly.Navigation.focusWorkspace = function() {
+ var cursor = Blockly.Navigation.cursor_;
+ var reset = Blockly.getMainWorkspace().getToolbox() ? true : false;
+
+ Blockly.Navigation.resetFlyout(reset);
+ Blockly.Navigation.currentState_ = Blockly.Navigation.STATE_WS;
+ Blockly.Navigation.enableKeyboardAccessibility();
+ if (Blockly.selected) {
+ var previousConnection = Blockly.selected.previousConnection;
+ var outputConnection = Blockly.selected.outputConnection;
+ //TODO: This still needs to work with blocks that have neither previous
+ //or output connection.
+ var connection = previousConnection ? previousConnection : outputConnection;
+ var newAstNode = Blockly.ASTNode.createConnectionNode(connection);
+ cursor.setLocation(newAstNode);
+ Blockly.selected.unselect();
+ } else {
+ var ws = cursor.workspace_;
+ // TODO: Find the center of the visible workspace.
+ var wsCoord = new Blockly.utils.Coordinate(100, 100);
+ var wsNode = Blockly.ASTNode.createWorkspaceNode(ws, wsCoord);
+ cursor.setLocation(wsNode);
+ }
+};
+
+/**
+ * Handles hitting the enter key on the workspace.
+ */
+Blockly.Navigation.handleEnterForWS = function() {
+ var cursor = Blockly.Navigation.cursor_;
+ var curNode = cursor.getCurNode();
+ var nodeType = curNode.getType();
+ if (nodeType === Blockly.ASTNode.types.FIELD) {
+ var location = curNode.getLocation();
+ location.showEditor_();
+ } else if (curNode.isConnection() ||
+ nodeType == Blockly.ASTNode.types.WORKSPACE) {
+ Blockly.Navigation.markAtCursor();
+ } else if (nodeType == Blockly.ASTNode.types.BLOCK) {
+ Blockly.Navigation.warn('Cannot mark a block.');
+ } else if (nodeType == Blockly.ASTNode.types.STACK) {
+ Blockly.Navigation.warn('Cannot mark a stack.');
+ }
+};
+
+/**********************/
+/** Helper Functions **/
+/**********************/
+
+
+/**
+ * TODO: Revisit keycodes before releasing
+ * Handler for all the keyboard navigation events.
+ * @param {Event} e The keyboard event.
+ * @return {!boolean} True if the key was handled false otherwise.
+ */
+Blockly.Navigation.navigate = function(e) {
+ var curState = Blockly.Navigation.currentState_;
+ if (e.keyCode === goog.events.KeyCodes.T) {
+ var workspace = Blockly.getMainWorkspace();
+ if (!workspace.getToolbox()) {
+ Blockly.Navigation.focusFlyout();
+ Blockly.Navigation.log('T: Focus Flyout');
+ } else {
+ Blockly.Navigation.focusToolbox();
+ Blockly.Navigation.log('T: Focus Toolbox');
+ }
+ return true;
+ } else if (curState === Blockly.Navigation.STATE_FLYOUT) {
+ return Blockly.Navigation.flyoutKeyHandler(e);
+ } else if (curState === Blockly.Navigation.STATE_WS) {
+ return Blockly.Navigation.workspaceKeyHandler(e);
+ } else if (curState === Blockly.Navigation.STATE_TOOLBOX) {
+ return Blockly.Navigation.toolboxKeyHandler(e);
+ } else {
+ Blockly.Navigation.log('Not a valid key ');
+ }
+ return false;
+};
+
+/**
+ * Handles all keyboard events when the user is focused on the flyout.
+ * @param {Event} e The keyboard event.
+ * @return {!boolean} True if the key was handled false otherwise.
+ */
+Blockly.Navigation.flyoutKeyHandler = function(e) {
+ if (e.keyCode === goog.events.KeyCodes.W) {
+ Blockly.Navigation.selectPreviousBlockInFlyout();
+ Blockly.Navigation.log('W: Flyout : Previous');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.A) {
+ Blockly.Navigation.focusToolbox();
+ Blockly.Navigation.log('A: Flyout : Go To Toolbox');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.S) {
+ Blockly.Navigation.selectNextBlockInFlyout();
+ Blockly.Navigation.log('S: Flyout : Next');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.ENTER) {
+ Blockly.Navigation.insertFromFlyout();
+ Blockly.Navigation.log('Enter: Flyout : Select');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.E ||
+ e.keyCode === goog.events.KeyCodes.ESC) {
+ Blockly.Navigation.focusWorkspace();
+ Blockly.Navigation.log('E or ESC: Flyout: Exit');
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Handles all keyboard events when the user is focused on the toolbox.
+ * @param {Event} e The keyboard event.
+ * @return {!boolean} True if the key was handled false otherwise.
+ */
+Blockly.Navigation.toolboxKeyHandler = function(e) {
+ if (e.keyCode === goog.events.KeyCodes.W) {
+ Blockly.Navigation.previousCategory();
+ Blockly.Navigation.log('W: Toolbox : Previous');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.A) {
+ Blockly.Navigation.outCategory();
+ Blockly.Navigation.log('A: Toolbox : Out');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.S) {
+ Blockly.Navigation.nextCategory();
+ Blockly.Navigation.log('S: Toolbox : Next');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.D) {
+ Blockly.Navigation.inCategory();
+ Blockly.Navigation.log('D: Toolbox : Go to flyout');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.ENTER) {
+ //TODO: focus on flyout OR open if the category is nested
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.E ||
+ e.keyCode === goog.events.KeyCodes.ESC) {
+ Blockly.Navigation.log('E or ESC: Toolbox: Exit');
+ Blockly.Navigation.focusWorkspace();
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Handles all keyboard events when the user is focused on the workspace.
+ * @param {Event} e The keyboard event.
+ * @return {!boolean} True if the key was handled false otherwise.
+ */
+Blockly.Navigation.workspaceKeyHandler = function(e) {
+ if (e.keyCode === goog.events.KeyCodes.W) {
+ Blockly.Navigation.cursor_.prev();
+ Blockly.Navigation.log('W: Workspace : Out');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.A) {
+ Blockly.Navigation.cursor_.out();
+ Blockly.Navigation.log('S: Workspace : Previous');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.S) {
+ Blockly.Navigation.cursor_.next();
+ Blockly.Navigation.log('S: Workspace : In');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.D) {
+ Blockly.Navigation.cursor_.in();
+ Blockly.Navigation.log('S: Workspace : Next');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.I) {
+ Blockly.Navigation.modify();
+ Blockly.Navigation.log('I: Workspace : Insert/Connect Blocks');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.ENTER) {
+ Blockly.Navigation.handleEnterForWS();
+ Blockly.Navigation.log('Enter: Workspace : Mark');
+ return true;
+ } else if (e.keyCode === goog.events.KeyCodes.X) {
+ Blockly.Navigation.log('X: Workspace: Disconnect Blocks');
+ Blockly.Navigation.disconnectBlocks();
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Enable accessibility mode.
+ */
+Blockly.Navigation.enableKeyboardAccessibility = function() {
+ Blockly.keyboardAccessibilityMode = true;
+};
+
+/**
+ * Disable accessibility mode.
+ */
+Blockly.Navigation.disableKeyboardAccessibility = function() {
+ Blockly.keyboardAccessibilityMode = false;
+};
+
+/**
+ * Navigation log handler. If loggingCallback is defined, use it.
+ * Otherwise just log to the console.
+ * @param {string} msg The message to log.
+ * @package
+ */
+Blockly.Navigation.log = function(msg) {
+ if (Blockly.Navigation.loggingCallback) {
+ Blockly.Navigation.loggingCallback('log', msg);
+ } else {
+ console.log(msg);
+ }
+};
+
+/**
+ * Navigation warning handler. If loggingCallback is defined, use it.
+ * Otherwise call Blockly.Navigation.warn.
+ * @param {string} msg The warning message.
+ * @package
+ */
+Blockly.Navigation.warn = function(msg) {
+ if (Blockly.Navigation.loggingCallback) {
+ Blockly.Navigation.loggingCallback('warn', msg);
+ } else {
+ console.warn(msg);
+ }
+};
+
+/**
+ * Navigation error handler. If loggingCallback is defined, use it.
+ * Otherwise call console.error.
+ * @param {string} msg The error message.
+ * @package
+ */
+Blockly.Navigation.error = function(msg) {
+ if (Blockly.Navigation.loggingCallback) {
+ Blockly.Navigation.loggingCallback('error', msg);
+ } else {
+ console.error(msg);
+ }
+};