diff --git a/core/block.js b/core/block.js index a165476d062..97a33f21ba4 100644 --- a/core/block.js +++ b/core/block.js @@ -29,6 +29,8 @@ const parsing = goog.require('Blockly.utils.parsing'); const {Abstract} = goog.requireType('Blockly.Events.Abstract'); const {Align, Input} = goog.require('Blockly.Input'); const {ASTNode} = goog.require('Blockly.ASTNode'); +/* eslint-disable-next-line no-unused-vars */ +const {BlockMove} = goog.requireType('Blockly.Events.BlockMove'); const {Blocks} = goog.require('Blockly.blocks'); /* eslint-disable-next-line no-unused-vars */ const {Comment} = goog.requireType('Blockly.Comment'); @@ -2098,7 +2100,8 @@ Block.prototype.moveBy = function(dx, dy) { if (this.parentBlock_) { throw Error('Block has parent.'); } - const event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(this); + const event = /** @type {!BlockMove} */ ( + new (eventUtils.get(eventUtils.BLOCK_MOVE))(this)); this.xy_.translate(dx, dy); event.recordNew(); eventUtils.fire(event); diff --git a/core/block_dragger.js b/core/block_dragger.js index d0ee612d356..88c1a177d62 100644 --- a/core/block_dragger.js +++ b/core/block_dragger.js @@ -22,6 +22,8 @@ const dom = goog.require('Blockly.utils.dom'); const eventUtils = goog.require('Blockly.Events.utils'); const registry = goog.require('Blockly.registry'); /* eslint-disable-next-line no-unused-vars */ +const {BlockMove} = goog.requireType('Blockly.Events.BlockMove'); +/* eslint-disable-next-line no-unused-vars */ const {BlockSvg} = goog.requireType('Blockly.BlockSvg'); const {Coordinate} = goog.require('Blockly.utils.Coordinate'); /* eslint-disable-next-line no-unused-vars */ @@ -376,8 +378,8 @@ const BlockDragger = class { * @protected */ fireMoveEvent_() { - const event = - new (eventUtils.get(eventUtils.BLOCK_MOVE))(this.draggingBlock_); + const event = /** @type {!BlockMove} */ + (new (eventUtils.get(eventUtils.BLOCK_MOVE))(this.draggingBlock_)); event.oldCoordinate = this.startXY_; event.recordNew(); eventUtils.fire(event); diff --git a/core/block_svg.js b/core/block_svg.js index 8326591364e..0f81aa04e06 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -31,6 +31,8 @@ const userAgent = goog.require('Blockly.utils.userAgent'); const {ASTNode} = goog.require('Blockly.ASTNode'); const {Block} = goog.require('Blockly.Block'); /* eslint-disable-next-line no-unused-vars */ +const {BlockMove} = goog.requireType('Blockly.Events.BlockMove'); +/* eslint-disable-next-line no-unused-vars */ const {Comment} = goog.requireType('Blockly.Comment'); const {ConnectionType} = goog.require('Blockly.ConnectionType'); /* eslint-disable-next-line no-unused-vars */ @@ -468,7 +470,8 @@ BlockSvg.prototype.moveBy = function(dx, dy) { const eventsEnabled = eventUtils.isEnabled(); let event; if (eventsEnabled) { - event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(this); + event = /** @type {!BlockMove} */ + (new (eventUtils.get(eventUtils.BLOCK_MOVE))(this)); } const xy = this.getRelativeToSurfaceXY(); this.translate(xy.x + dx, xy.y + dy); diff --git a/core/bubble_dragger.js b/core/bubble_dragger.js index 7a716d3dbeb..54e957cbbd3 100644 --- a/core/bubble_dragger.js +++ b/core/bubble_dragger.js @@ -19,6 +19,8 @@ const eventUtils = goog.require('Blockly.Events.utils'); const svgMath = goog.require('Blockly.utils.svgMath'); /* eslint-disable-next-line no-unused-vars */ const {BlockDragSurfaceSvg} = goog.requireType('Blockly.BlockDragSurfaceSvg'); +/* eslint-disable-next-line no-unused-vars */ +const {CommentMove} = goog.requireType('Blockly.Events.CommentMove'); const {ComponentManager} = goog.require('Blockly.ComponentManager'); const {Coordinate} = goog.require('Blockly.utils.Coordinate'); /* eslint-disable-next-line no-unused-vars */ @@ -244,8 +246,9 @@ const BubbleDragger = class { if (this.draggingBubble_.isComment) { // TODO (adodson): Resolve build errors when requiring // WorkspaceCommentSvg. - const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))( - /** @type {!WorkspaceCommentSvg} */ (this.draggingBubble_)); + const event = /** @type {!CommentMove} */ + (new (eventUtils.get(eventUtils.COMMENT_MOVE))( + /** @type {!WorkspaceCommentSvg} */ (this.draggingBubble_))); event.setOldCoordinate(this.startXY_); event.recordNew(); eventUtils.fire(event); diff --git a/core/component_manager.js b/core/component_manager.js index 5010f538ef2..fb59b9ef4fe 100644 --- a/core/component_manager.js +++ b/core/component_manager.js @@ -31,210 +31,215 @@ const {IPositionable} = goog.requireType('Blockly.IPositionable'); /** * Manager for all items registered with the workspace. - * @constructor - * @alias Blockly.ComponentManager */ -const ComponentManager = function() { +class ComponentManager { /** - * A map of the components registered with the workspace, mapped to id. - * @type {!Object} - * @private + * @alias Blockly.ComponentManager */ - this.componentData_ = Object.create(null); + constructor() { + /** + * A map of the components registered with the workspace, mapped to id. + * @type {!Object} + * @private + */ + this.componentData_ = Object.create(null); + + /** + * A map of capabilities to component IDs. + * @type {!Object>} + * @private + */ + this.capabilityToComponentIds_ = Object.create(null); + } /** - * A map of capabilities to component IDs. - * @type {!Object>} - * @private + * Adds a component. + * @param {!ComponentManager.ComponentDatum} componentInfo The data for + * the component to register. + * @param {boolean=} opt_allowOverrides True to prevent an error when + * overriding an already registered item. */ - this.capabilityToComponentIds_ = Object.create(null); -}; - -/** - * An object storing component information. - * @typedef {{ - * component: !IComponent, - * capabilities: ( - * !Array> - * ), - * weight: number - * }} - */ -ComponentManager.ComponentDatum; - -/** - * Adds a component. - * @param {!ComponentManager.ComponentDatum} componentInfo The data for - * the component to register. - * @param {boolean=} opt_allowOverrides True to prevent an error when overriding - * an already registered item. - */ -ComponentManager.prototype.addComponent = function( - componentInfo, opt_allowOverrides) { - // Don't throw an error if opt_allowOverrides is true. - const id = componentInfo.component.id; - if (!opt_allowOverrides && this.componentData_[id]) { - throw Error( - 'Plugin "' + id + '" with capabilities "' + - this.componentData_[id].capabilities + '" already added.'); + addComponent(componentInfo, opt_allowOverrides) { + // Don't throw an error if opt_allowOverrides is true. + const id = componentInfo.component.id; + if (!opt_allowOverrides && this.componentData_[id]) { + throw Error( + 'Plugin "' + id + '" with capabilities "' + + this.componentData_[id].capabilities + '" already added.'); + } + this.componentData_[id] = componentInfo; + const stringCapabilities = []; + for (let i = 0; i < componentInfo.capabilities.length; i++) { + const capability = String(componentInfo.capabilities[i]).toLowerCase(); + stringCapabilities.push(capability); + if (this.capabilityToComponentIds_[capability] === undefined) { + this.capabilityToComponentIds_[capability] = [id]; + } else { + this.capabilityToComponentIds_[capability].push(id); + } + } + this.componentData_[id].capabilities = stringCapabilities; } - this.componentData_[id] = componentInfo; - const stringCapabilities = []; - for (let i = 0; i < componentInfo.capabilities.length; i++) { - const capability = String(componentInfo.capabilities[i]).toLowerCase(); - stringCapabilities.push(capability); - if (this.capabilityToComponentIds_[capability] === undefined) { - this.capabilityToComponentIds_[capability] = [id]; - } else { - this.capabilityToComponentIds_[capability].push(id); + + /** + * Removes a component. + * @param {string} id The ID of the component to remove. + */ + removeComponent(id) { + const componentInfo = this.componentData_[id]; + if (!componentInfo) { + return; + } + for (let i = 0; i < componentInfo.capabilities.length; i++) { + const capability = String(componentInfo.capabilities[i]).toLowerCase(); + arrayUtils.removeElem(this.capabilityToComponentIds_[capability], id); } + delete this.componentData_[id]; } - this.componentData_[id].capabilities = stringCapabilities; -}; -/** - * Removes a component. - * @param {string} id The ID of the component to remove. - */ -ComponentManager.prototype.removeComponent = function(id) { - const componentInfo = this.componentData_[id]; - if (!componentInfo) { - return; + /** + * Adds a capability to a existing registered component. + * @param {string} id The ID of the component to add the capability to. + * @param {string|!ComponentManager.Capability} capability The + * capability to add. + * @template T + */ + addCapability(id, capability) { + if (!this.getComponent(id)) { + throw Error( + 'Cannot add capability, "' + capability + '". Plugin "' + id + + '" has not been added to the ComponentManager'); + } + if (this.hasCapability(id, capability)) { + console.warn( + 'Plugin "' + id + 'already has capability "' + capability + '"'); + return; + } + capability = String(capability).toLowerCase(); + this.componentData_[id].capabilities.push(capability); + this.capabilityToComponentIds_[capability].push(id); } - for (let i = 0; i < componentInfo.capabilities.length; i++) { - const capability = String(componentInfo.capabilities[i]).toLowerCase(); + + /** + * Removes a capability from an existing registered component. + * @param {string} id The ID of the component to remove the capability from. + * @param {string|!ComponentManager.Capability} capability The + * capability to remove. + * @template T + */ + removeCapability(id, capability) { + if (!this.getComponent(id)) { + throw Error( + 'Cannot remove capability, "' + capability + '". Plugin "' + id + + '" has not been added to the ComponentManager'); + } + if (!this.hasCapability(id, capability)) { + console.warn( + 'Plugin "' + id + 'doesn\'t have capability "' + capability + + '" to remove'); + return; + } + capability = String(capability).toLowerCase(); + arrayUtils.removeElem(this.componentData_[id].capabilities, capability); arrayUtils.removeElem(this.capabilityToComponentIds_[capability], id); } - delete this.componentData_[id]; -}; -/** - * Adds a capability to a existing registered component. - * @param {string} id The ID of the component to add the capability to. - * @param {string|!ComponentManager.Capability} capability The - * capability to add. - * @template T - */ -ComponentManager.prototype.addCapability = function(id, capability) { - if (!this.getComponent(id)) { - throw Error( - 'Cannot add capability, "' + capability + '". Plugin "' + id + - '" has not been added to the ComponentManager'); - } - if (this.hasCapability(id, capability)) { - console.warn( - 'Plugin "' + id + 'already has capability "' + capability + '"'); - return; + /** + * Returns whether the component with this id has the specified capability. + * @param {string} id The ID of the component to check. + * @param {string|!ComponentManager.Capability} capability The + * capability to check for. + * @return {boolean} Whether the component has the capability. + * @template T + */ + hasCapability(id, capability) { + capability = String(capability).toLowerCase(); + return this.componentData_[id].capabilities.indexOf(capability) !== -1; } - capability = String(capability).toLowerCase(); - this.componentData_[id].capabilities.push(capability); - this.capabilityToComponentIds_[capability].push(id); -}; -/** - * Removes a capability from an existing registered component. - * @param {string} id The ID of the component to remove the capability from. - * @param {string|!ComponentManager.Capability} capability The - * capability to remove. - * @template T - */ -ComponentManager.prototype.removeCapability = function(id, capability) { - if (!this.getComponent(id)) { - throw Error( - 'Cannot remove capability, "' + capability + '". Plugin "' + id + - '" has not been added to the ComponentManager'); - } - if (!this.hasCapability(id, capability)) { - console.warn( - 'Plugin "' + id + 'doesn\'t have capability "' + capability + - '" to remove'); - return; + /** + * Gets the component with the given ID. + * @param {string} id The ID of the component to get. + * @return {!IComponent|undefined} The component with the given name + * or undefined if not found. + */ + getComponent(id) { + return this.componentData_[id] && this.componentData_[id].component; } - capability = String(capability).toLowerCase(); - arrayUtils.removeElem(this.componentData_[id].capabilities, capability); - arrayUtils.removeElem(this.capabilityToComponentIds_[capability], id); -}; - -/** - * Returns whether the component with this id has the specified capability. - * @param {string} id The ID of the component to check. - * @param {string|!ComponentManager.Capability} capability The - * capability to check for. - * @return {boolean} Whether the component has the capability. - * @template T - */ -ComponentManager.prototype.hasCapability = function(id, capability) { - capability = String(capability).toLowerCase(); - return this.componentData_[id].capabilities.indexOf(capability) !== -1; -}; -/** - * Gets the component with the given ID. - * @param {string} id The ID of the component to get. - * @return {!IComponent|undefined} The component with the given name - * or undefined if not found. - */ -ComponentManager.prototype.getComponent = function(id) { - return this.componentData_[id] && this.componentData_[id].component; -}; + /** + * Gets all the components with the specified capability. + * @param {string|!ComponentManager.Capability + * } capability The capability of the component. + * @param {boolean} sorted Whether to return list ordered by weights. + * @return {!Array} The components that match the specified capability. + * @template T + */ + getComponents(capability, sorted) { + capability = String(capability).toLowerCase(); + const componentIds = this.capabilityToComponentIds_[capability]; + if (!componentIds) { + return []; + } + const components = []; + if (sorted) { + const componentDataList = []; + const componentData = this.componentData_; + componentIds.forEach(function(id) { + componentDataList.push(componentData[id]); + }); + componentDataList.sort(function(a, b) { + return a.weight - b.weight; + }); + componentDataList.forEach(function(ComponentDatum) { + components.push(ComponentDatum.component); + }); + } else { + const componentData = this.componentData_; + componentIds.forEach(function(id) { + components.push(componentData[id].component); + }); + } + return components; + } +} /** - * Gets all the components with the specified capability. - * @param {string|!ComponentManager.Capability - * } capability The capability of the component. - * @param {boolean} sorted Whether to return list ordered by weights. - * @return {!Array} The components that match the specified capability. - * @template T + * An object storing component information. + * @typedef {{ + * component: !IComponent, + * capabilities: ( + * !Array> + * ), + * weight: number + * }} */ -ComponentManager.prototype.getComponents = function(capability, sorted) { - capability = String(capability).toLowerCase(); - const componentIds = this.capabilityToComponentIds_[capability]; - if (!componentIds) { - return []; - } - const components = []; - if (sorted) { - const componentDataList = []; - const componentData = this.componentData_; - componentIds.forEach(function(id) { - componentDataList.push(componentData[id]); - }); - componentDataList.sort(function(a, b) { - return a.weight - b.weight; - }); - componentDataList.forEach(function(ComponentDatum) { - components.push(ComponentDatum.component); - }); - } else { - const componentData = this.componentData_; - componentIds.forEach(function(id) { - components.push(componentData[id].component); - }); - } - return components; -}; +ComponentManager.ComponentDatum; /** * A name with the capability of the element stored in the generic. - * @param {string} name The name of the component capability. - * @constructor * @template T */ -ComponentManager.Capability = function(name) { +ComponentManager.Capability = class { /** - * @type {string} - * @private + * @param {string} name The name of the component capability. */ - this.name_ = name; -}; + constructor(name) { + /** + * @type {string} + * @private + */ + this.name_ = name; + } -/** - * Returns the name of the capability. - * @return {string} The name. - * @override - */ -ComponentManager.Capability.prototype.toString = function() { - return this.name_; + /** + * Returns the name of the capability. + * @return {string} The name. + * @override + */ + toString() { + return this.name_; + } }; /** @type {!ComponentManager.Capability} */ diff --git a/core/connection.js b/core/connection.js index 962b884cac6..02e2575d890 100644 --- a/core/connection.js +++ b/core/connection.js @@ -20,6 +20,8 @@ const blocks = goog.require('Blockly.serialization.blocks'); const eventUtils = goog.require('Blockly.Events.utils'); /* eslint-disable-next-line no-unused-vars */ const {Block} = goog.requireType('Blockly.Block'); +/* eslint-disable-next-line no-unused-vars */ +const {BlockMove} = goog.requireType('Blockly.Events.BlockMove'); const {ConnectionType} = goog.require('Blockly.ConnectionType'); /* eslint-disable-next-line no-unused-vars */ const {IASTNodeLocationWithBlock} = goog.require('Blockly.IASTNodeLocationWithBlock'); @@ -134,7 +136,8 @@ class Connection { // Connect the new connection to the parent. let event; if (eventUtils.isEnabled()) { - event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock); + event = /** @type {!BlockMove} */ + (new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock)); } connectReciprocally(parentConnection, childConnection); childBlock.setParent(parentBlock); @@ -307,7 +310,8 @@ class Connection { disconnectInternal_(parentBlock, childBlock) { let event; if (eventUtils.isEnabled()) { - event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock); + event = /** @type {!BlockMove} */ + (new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock)); } const otherConnection = this.targetConnection; otherConnection.targetConnection = null; diff --git a/core/delete_area.js b/core/delete_area.js index b856d8a97cf..bc80ebf3743 100644 --- a/core/delete_area.js +++ b/core/delete_area.js @@ -18,7 +18,6 @@ */ goog.module('Blockly.DeleteArea'); -const object = goog.require('Blockly.utils.object'); const {BlockSvg} = goog.require('Blockly.BlockSvg'); const {DragTarget} = goog.require('Blockly.DragTarget'); /* eslint-disable-next-line no-unused-vars */ @@ -32,53 +31,55 @@ const {IDraggable} = goog.requireType('Blockly.IDraggable'); * dropped on top of it. * @extends {DragTarget} * @implements {IDeleteArea} - * @constructor - * @alias Blockly.DeleteArea */ -const DeleteArea = function() { - DeleteArea.superClass_.constructor.call(this); - +class DeleteArea extends DragTarget { /** - * Whether the last block or bubble dragged over this delete area would be - * deleted if dropped on this component. - * This property is not updated after the block or bubble is deleted. - * @type {boolean} - * @protected + * @alias Blockly.DeleteArea */ - this.wouldDelete_ = false; -}; -object.inherits(DeleteArea, DragTarget); + constructor() { + super(); -/** - * Returns whether the provided block or bubble would be deleted if dropped on - * this area. - * This method should check if the element is deletable and is always called - * before onDragEnter/onDragOver/onDragExit. - * @param {!IDraggable} element The block or bubble currently being - * dragged. - * @param {boolean} couldConnect Whether the element could could connect to - * another. - * @return {boolean} Whether the element provided would be deleted if dropped on - * this area. - */ -DeleteArea.prototype.wouldDelete = function(element, couldConnect) { - if (element instanceof BlockSvg) { - const block = /** @type {BlockSvg} */ (element); - const couldDeleteBlock = !block.getParent() && block.isDeletable(); - this.updateWouldDelete_(couldDeleteBlock && !couldConnect); - } else { - this.updateWouldDelete_(element.isDeletable()); + /** + * Whether the last block or bubble dragged over this delete area would be + * deleted if dropped on this component. + * This property is not updated after the block or bubble is deleted. + * @type {boolean} + * @protected + */ + this.wouldDelete_ = false; } - return this.wouldDelete_; -}; -/** - * Updates the internal wouldDelete_ state. - * @param {boolean} wouldDelete The new value for the wouldDelete state. - * @protected - */ -DeleteArea.prototype.updateWouldDelete_ = function(wouldDelete) { - this.wouldDelete_ = wouldDelete; -}; + /** + * Returns whether the provided block or bubble would be deleted if dropped on + * this area. + * This method should check if the element is deletable and is always called + * before onDragEnter/onDragOver/onDragExit. + * @param {!IDraggable} element The block or bubble currently being + * dragged. + * @param {boolean} couldConnect Whether the element could could connect to + * another. + * @return {boolean} Whether the element provided would be deleted if dropped + * on this area. + */ + wouldDelete(element, couldConnect) { + if (element instanceof BlockSvg) { + const block = /** @type {BlockSvg} */ (element); + const couldDeleteBlock = !block.getParent() && block.isDeletable(); + this.updateWouldDelete_(couldDeleteBlock && !couldConnect); + } else { + this.updateWouldDelete_(element.isDeletable()); + } + return this.wouldDelete_; + } + + /** + * Updates the internal wouldDelete_ state. + * @param {boolean} wouldDelete The new value for the wouldDelete state. + * @protected + */ + updateWouldDelete_(wouldDelete) { + this.wouldDelete_ = wouldDelete; + } +} exports.DeleteArea = DeleteArea; diff --git a/core/drag_target.js b/core/drag_target.js index f552586bdd9..9e425137740 100644 --- a/core/drag_target.js +++ b/core/drag_target.js @@ -30,68 +30,73 @@ const {Rect} = goog.requireType('Blockly.utils.Rect'); * Abstract class for a component with custom behaviour when a block or bubble * is dragged over or dropped on top of it. * @implements {IDragTarget} - * @constructor - * @alias Blockly.DragTarget */ -const DragTarget = function() {}; +class DragTarget { + /** + * @alias Blockly.DragTarget + */ + constructor() {} -/** - * Returns the bounding rectangle of the drag target area in pixel units - * relative to the Blockly injection div. - * @return {?Rect} The component's bounding box. Null if drag - * target area should be ignored. - */ -DragTarget.prototype.getClientRect; + /** + * Handles when a cursor with a block or bubble enters this drag target. + * @param {!IDraggable} _dragElement The block or bubble currently being + * dragged. + */ + onDragEnter(_dragElement) { + // no-op + } -/** - * Handles when a cursor with a block or bubble enters this drag target. - * @param {!IDraggable} _dragElement The block or bubble currently being - * dragged. - */ -DragTarget.prototype.onDragEnter = function(_dragElement) { - // no-op -}; + /** + * Handles when a cursor with a block or bubble is dragged over this drag + * target. + * @param {!IDraggable} _dragElement The block or bubble currently being + * dragged. + */ + onDragOver(_dragElement) { + // no-op + } -/** - * Handles when a cursor with a block or bubble is dragged over this drag - * target. - * @param {!IDraggable} _dragElement The block or bubble currently being - * dragged. - */ -DragTarget.prototype.onDragOver = function(_dragElement) { - // no-op -}; + /** + * Handles when a cursor with a block or bubble exits this drag target. + * @param {!IDraggable} _dragElement The block or bubble currently being + * dragged. + */ + onDragExit(_dragElement) { + // no-op + } -/** - * Handles when a cursor with a block or bubble exits this drag target. - * @param {!IDraggable} _dragElement The block or bubble currently being - * dragged. - */ -DragTarget.prototype.onDragExit = function(_dragElement) { - // no-op -}; + /** + * Handles when a block or bubble is dropped on this component. + * Should not handle delete here. + * @param {!IDraggable} _dragElement The block or bubble currently being + * dragged. + */ + onDrop(_dragElement) { + // no-op + } -/** - * Handles when a block or bubble is dropped on this component. - * Should not handle delete here. - * @param {!IDraggable} _dragElement The block or bubble currently being - * dragged. - */ -DragTarget.prototype.onDrop = function(_dragElement) { - // no-op -}; + /** + * Returns the bounding rectangle of the drag target area in pixel units + * relative to the Blockly injection div. + * @return {?Rect} The component's bounding box. Null if drag + * target area should be ignored. + */ + getClientRect() { + return null; + } -/** - * Returns whether the provided block or bubble should not be moved after being - * dropped on this component. If true, the element will return to where it was - * when the drag started. - * @param {!IDraggable} _dragElement The block or bubble currently being - * dragged. - * @return {boolean} Whether the block or bubble provided should be returned to - * drag start. - */ -DragTarget.prototype.shouldPreventMove = function(_dragElement) { - return false; -}; + /** + * Returns whether the provided block or bubble should not be moved after + * being dropped on this component. If true, the element will return to where + * it was when the drag started. + * @param {!IDraggable} _dragElement The block or bubble currently being + * dragged. + * @return {boolean} Whether the block or bubble provided should be returned + * to drag start. + */ + shouldPreventMove(_dragElement) { + return false; + } +} exports.DragTarget = DragTarget; diff --git a/core/events/events_block_base.js b/core/events/events_block_base.js index 3a0842def95..935575b1ad9 100644 --- a/core/events/events_block_base.js +++ b/core/events/events_block_base.js @@ -15,7 +15,6 @@ */ goog.module('Blockly.Events.BlockBase'); -const object = goog.require('Blockly.utils.object'); const {Abstract} = goog.require('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ const {Block} = goog.requireType('Blockly.Block'); @@ -23,47 +22,49 @@ const {Block} = goog.requireType('Blockly.Block'); /** * Abstract class for a block event. - * @param {!Block=} opt_block The block this event corresponds to. - * Undefined for a blank event. * @extends {Abstract} - * @constructor - * @alias Blockly.Events.BlockBase */ -const BlockBase = function(opt_block) { - BlockBase.superClass_.constructor.call(this); - this.isBlank = typeof opt_block === 'undefined'; - +class BlockBase extends Abstract { /** - * The block ID for the block this event pertains to - * @type {string} + * @param {!Block=} opt_block The block this event corresponds to. + * Undefined for a blank event. + * @alias Blockly.Events.BlockBase */ - this.blockId = this.isBlank ? '' : opt_block.id; + constructor(opt_block) { + super(); + this.isBlank = typeof opt_block === 'undefined'; + + /** + * The block ID for the block this event pertains to + * @type {string} + */ + this.blockId = this.isBlank ? '' : opt_block.id; + + /** + * The workspace identifier for this event. + * @type {string} + */ + this.workspaceId = this.isBlank ? '' : opt_block.workspace.id; + } /** - * The workspace identifier for this event. - * @type {string} + * Encode the event as JSON. + * @return {!Object} JSON representation. */ - this.workspaceId = this.isBlank ? '' : opt_block.workspace.id; -}; -object.inherits(BlockBase, Abstract); - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -BlockBase.prototype.toJson = function() { - const json = BlockBase.superClass_.toJson.call(this); - json['blockId'] = this.blockId; - return json; -}; + toJson() { + const json = super.toJson(); + json['blockId'] = this.blockId; + return json; + } -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -BlockBase.prototype.fromJson = function(json) { - BlockBase.superClass_.fromJson.call(this, json); - this.blockId = json['blockId']; -}; + /** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ + fromJson(json) { + super.fromJson(json); + this.blockId = json['blockId']; + } +} exports.BlockBase = BlockBase; diff --git a/core/events/events_bubble_open.js b/core/events/events_bubble_open.js index 95099951c2b..634ceba14b3 100644 --- a/core/events/events_bubble_open.js +++ b/core/events/events_bubble_open.js @@ -16,7 +16,6 @@ goog.module('Blockly.Events.BubbleOpen'); const eventUtils = goog.require('Blockly.Events.utils'); -const object = goog.require('Blockly.utils.object'); const registry = goog.require('Blockly.registry'); /* eslint-disable-next-line no-unused-vars */ const {BlockSvg} = goog.requireType('Blockly.BlockSvg'); @@ -25,64 +24,66 @@ const {UiBase} = goog.require('Blockly.Events.UiBase'); /** * Class for a bubble open event. - * @param {BlockSvg} opt_block The associated block. Undefined for a - * blank event. - * @param {boolean=} opt_isOpen Whether the bubble is opening (false if - * closing). Undefined for a blank event. - * @param {string=} opt_bubbleType The type of bubble. One of 'mutator', - * 'comment' - * or 'warning'. Undefined for a blank event. * @extends {UiBase} - * @constructor - * @alias Blockly.Events.BubbleOpen */ -const BubbleOpen = function(opt_block, opt_isOpen, opt_bubbleType) { - const workspaceId = opt_block ? opt_block.workspace.id : undefined; - BubbleOpen.superClass_.constructor.call(this, workspaceId); - this.blockId = opt_block ? opt_block.id : null; - +class BubbleOpen extends UiBase { /** - * Whether the bubble is opening (false if closing). - * @type {boolean|undefined} + * @param {BlockSvg} opt_block The associated block. Undefined for a + * blank event. + * @param {boolean=} opt_isOpen Whether the bubble is opening (false if + * closing). Undefined for a blank event. + * @param {string=} opt_bubbleType The type of bubble. One of 'mutator', + * 'comment' + * or 'warning'. Undefined for a blank event. + * @alias Blockly.Events.BubbleOpen */ - this.isOpen = opt_isOpen; + constructor(opt_block, opt_isOpen, opt_bubbleType) { + const workspaceId = opt_block ? opt_block.workspace.id : undefined; + super(workspaceId); + this.blockId = opt_block ? opt_block.id : null; + + /** + * Whether the bubble is opening (false if closing). + * @type {boolean|undefined} + */ + this.isOpen = opt_isOpen; + + /** + * The type of bubble. One of 'mutator', 'comment', or 'warning'. + * @type {string|undefined} + */ + this.bubbleType = opt_bubbleType; + + /** + * Type of this event. + * @type {string} + */ + this.type = eventUtils.BUBBLE_OPEN; + } /** - * The type of bubble. One of 'mutator', 'comment', or 'warning'. - * @type {string|undefined} + * Encode the event as JSON. + * @return {!Object} JSON representation. */ - this.bubbleType = opt_bubbleType; + toJson() { + const json = super.toJson(); + json['isOpen'] = this.isOpen; + json['bubbleType'] = this.bubbleType; + json['blockId'] = this.blockId; + return json; + } /** - * Type of this event. - * @type {string} + * Decode the JSON event. + * @param {!Object} json JSON representation. */ - this.type = eventUtils.BUBBLE_OPEN; -}; -object.inherits(BubbleOpen, UiBase); - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -BubbleOpen.prototype.toJson = function() { - const json = BubbleOpen.superClass_.toJson.call(this); - json['isOpen'] = this.isOpen; - json['bubbleType'] = this.bubbleType; - json['blockId'] = this.blockId; - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -BubbleOpen.prototype.fromJson = function(json) { - BubbleOpen.superClass_.fromJson.call(this, json); - this.isOpen = json['isOpen']; - this.bubbleType = json['bubbleType']; - this.blockId = json['blockId']; -}; + fromJson(json) { + super.fromJson(json); + this.isOpen = json['isOpen']; + this.bubbleType = json['bubbleType']; + this.blockId = json['blockId']; + } +} registry.register(registry.Type.EVENT, eventUtils.BUBBLE_OPEN, BubbleOpen); diff --git a/core/events/events_comment_base.js b/core/events/events_comment_base.js index a54e8e0e87d..c77e6e7613b 100644 --- a/core/events/events_comment_base.js +++ b/core/events/events_comment_base.js @@ -17,7 +17,6 @@ goog.module('Blockly.Events.CommentBase'); const Xml = goog.require('Blockly.Xml'); const eventUtils = goog.require('Blockly.Events.utils'); -const object = goog.require('Blockly.utils.object'); const utilsXml = goog.require('Blockly.utils.xml'); const {Abstract} = goog.require('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ @@ -30,89 +29,93 @@ const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment'); /** * Abstract class for a comment event. - * @param {!WorkspaceComment=} opt_comment The comment this event - * corresponds to. Undefined for a blank event. * @extends {Abstract} - * @constructor - * @alias Blockly.Events.CommentBase */ -const CommentBase = function(opt_comment) { +class CommentBase extends Abstract { /** - * Whether or not an event is blank. - * @type {boolean} + * @param {!WorkspaceComment=} opt_comment The comment this event + * corresponds to. Undefined for a blank event. + * @alias Blockly.Events.CommentBase */ - this.isBlank = typeof opt_comment === 'undefined'; + constructor(opt_comment) { + super(); + /** + * Whether or not an event is blank. + * @type {boolean} + */ + this.isBlank = typeof opt_comment === 'undefined'; - /** - * The ID of the comment this event pertains to. - * @type {string} - */ - this.commentId = this.isBlank ? '' : opt_comment.id; + /** + * The ID of the comment this event pertains to. + * @type {string} + */ + this.commentId = this.isBlank ? '' : opt_comment.id; - /** - * The workspace identifier for this event. - * @type {string} - */ - this.workspaceId = this.isBlank ? '' : opt_comment.workspace.id; + /** + * The workspace identifier for this event. + * @type {string} + */ + this.workspaceId = this.isBlank ? '' : opt_comment.workspace.id; + + /** + * The event group id for the group this event belongs to. Groups define + * events that should be treated as an single action from the user's + * perspective, and should be undone together. + * @type {string} + */ + this.group = eventUtils.getGroup(); + + /** + * Sets whether the event should be added to the undo stack. + * @type {boolean} + */ + this.recordUndo = eventUtils.getRecordUndo(); + } /** - * The event group id for the group this event belongs to. Groups define - * events that should be treated as an single action from the user's - * perspective, and should be undone together. - * @type {string} + * Encode the event as JSON. + * @return {!Object} JSON representation. */ - this.group = eventUtils.getGroup(); + toJson() { + const json = super.toJson(); + if (this.commentId) { + json['commentId'] = this.commentId; + } + return json; + } /** - * Sets whether the event should be added to the undo stack. - * @type {boolean} + * Decode the JSON event. + * @param {!Object} json JSON representation. */ - this.recordUndo = eventUtils.getRecordUndo(); -}; -object.inherits(CommentBase, Abstract); - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -CommentBase.prototype.toJson = function() { - const json = CommentBase.superClass_.toJson.call(this); - if (this.commentId) { - json['commentId'] = this.commentId; + fromJson(json) { + super.fromJson(json); + this.commentId = json['commentId']; } - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -CommentBase.prototype.fromJson = function(json) { - CommentBase.superClass_.fromJson.call(this, json); - this.commentId = json['commentId']; -}; -/** - * Helper function for Comment[Create|Delete] - * @param {!CommentCreate|!CommentDelete} event - * The event to run. - * @param {boolean} create if True then Create, if False then Delete - */ -CommentBase.CommentCreateDeleteHelper = function(event, create) { - const workspace = event.getEventWorkspace_(); - if (create) { - const xmlElement = utilsXml.createElement('xml'); - xmlElement.appendChild(event.xml); - Xml.domToWorkspace(xmlElement, workspace); - } else { - const comment = workspace.getCommentById(event.commentId); - if (comment) { - comment.dispose(); + /** + * Helper function for Comment[Create|Delete] + * @param {!CommentCreate|!CommentDelete} event + * The event to run. + * @param {boolean} create if True then Create, if False then Delete + */ + static CommentCreateDeleteHelper(event, create) { + const workspace = event.getEventWorkspace_(); + if (create) { + const xmlElement = utilsXml.createElement('xml'); + xmlElement.appendChild(event.xml); + Xml.domToWorkspace(xmlElement, workspace); } else { - // Only complain about root-level block. - console.warn('Can\'t uncreate non-existent comment: ' + event.commentId); + const comment = workspace.getCommentById(event.commentId); + if (comment) { + comment.dispose(); + } else { + // Only complain about root-level block. + console.warn( + 'Can\'t uncreate non-existent comment: ' + event.commentId); + } } } -}; +} exports.CommentBase = CommentBase; diff --git a/core/events/events_ui_base.js b/core/events/events_ui_base.js index 8f963f1757b..eb2bf4fcf53 100644 --- a/core/events/events_ui_base.js +++ b/core/events/events_ui_base.js @@ -17,7 +17,6 @@ */ goog.module('Blockly.Events.UiBase'); -const object = goog.require('Blockly.utils.object'); const {Abstract} = goog.require('Blockly.Events.Abstract'); @@ -27,36 +26,38 @@ const {Abstract} = goog.require('Blockly.Events.Abstract'); * editing to work (e.g. scrolling the workspace, zooming, opening toolbox * categories). * UI events do not undo or redo. - * @param {string=} opt_workspaceId The workspace identifier for this event. - * Undefined for a blank event. * @extends {Abstract} - * @constructor - * @alias Blockly.Events.UiBase */ -const UiBase = function(opt_workspaceId) { - UiBase.superClass_.constructor.call(this); - - /** - * Whether or not the event is blank (to be populated by fromJson). - * @type {boolean} - */ - this.isBlank = typeof opt_workspaceId === 'undefined'; - - /** - * The workspace identifier for this event. - * @type {string} - */ - this.workspaceId = opt_workspaceId ? opt_workspaceId : ''; - - // UI events do not undo or redo. - this.recordUndo = false; - +class UiBase extends Abstract { /** - * Whether or not the event is a UI event. - * @type {boolean} + * @param {string=} opt_workspaceId The workspace identifier for this event. + * Undefined for a blank event. + * @alias Blockly.Events.UiBase */ - this.isUiEvent = true; -}; -object.inherits(UiBase, Abstract); + constructor(opt_workspaceId) { + super(); + + /** + * Whether or not the event is blank (to be populated by fromJson). + * @type {boolean} + */ + this.isBlank = typeof opt_workspaceId === 'undefined'; + + /** + * The workspace identifier for this event. + * @type {string} + */ + this.workspaceId = opt_workspaceId ? opt_workspaceId : ''; + + // UI events do not undo or redo. + this.recordUndo = false; + + /** + * Whether or not the event is a UI event. + * @type {boolean} + */ + this.isUiEvent = true; + } +} exports.UiBase = UiBase; diff --git a/core/events/events_var_base.js b/core/events/events_var_base.js index 2f72cd9828d..82eef232344 100644 --- a/core/events/events_var_base.js +++ b/core/events/events_var_base.js @@ -15,7 +15,6 @@ */ goog.module('Blockly.Events.VarBase'); -const object = goog.require('Blockly.utils.object'); const {Abstract} = goog.require('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ const {VariableModel} = goog.requireType('Blockly.VariableModel'); @@ -23,47 +22,49 @@ const {VariableModel} = goog.requireType('Blockly.VariableModel'); /** * Abstract class for a variable event. - * @param {!VariableModel=} opt_variable The variable this event - * corresponds to. Undefined for a blank event. * @extends {Abstract} - * @constructor - * @alias Blockly.Events.VarBase */ -const VarBase = function(opt_variable) { - VarBase.superClass_.constructor.call(this); - this.isBlank = typeof opt_variable === 'undefined'; - +class VarBase extends Abstract { /** - * The variable id for the variable this event pertains to. - * @type {string} + * @param {!VariableModel=} opt_variable The variable this event + * corresponds to. Undefined for a blank event. + * @alias Blockly.Events.VarBase */ - this.varId = this.isBlank ? '' : opt_variable.getId(); + constructor(opt_variable) { + super(); + this.isBlank = typeof opt_variable === 'undefined'; + + /** + * The variable id for the variable this event pertains to. + * @type {string} + */ + this.varId = this.isBlank ? '' : opt_variable.getId(); + + /** + * The workspace identifier for this event. + * @type {string} + */ + this.workspaceId = this.isBlank ? '' : opt_variable.workspace.id; + } /** - * The workspace identifier for this event. - * @type {string} + * Encode the event as JSON. + * @return {!Object} JSON representation. */ - this.workspaceId = this.isBlank ? '' : opt_variable.workspace.id; -}; -object.inherits(VarBase, Abstract); - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -VarBase.prototype.toJson = function() { - const json = VarBase.superClass_.toJson.call(this); - json['varId'] = this.varId; - return json; -}; + toJson() { + const json = super.toJson(); + json['varId'] = this.varId; + return json; + } -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -VarBase.prototype.fromJson = function(json) { - VarBase.superClass_.toJson.call(this); - this.varId = json['varId']; -}; + /** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ + fromJson(json) { + super.fromJson(json); + this.varId = json['varId']; + } +} exports.VarBase = VarBase; diff --git a/core/keyboard_nav/ast_node.js b/core/keyboard_nav/ast_node.js index 7e0d045fe19..4001458ad00 100644 --- a/core/keyboard_nav/ast_node.js +++ b/core/keyboard_nav/ast_node.js @@ -39,694 +39,702 @@ const {Workspace} = goog.requireType('Blockly.Workspace'); * Class for an AST node. * It is recommended that you use one of the createNode methods instead of * creating a node directly. - * @param {string} type The type of the location. - * Must be in ASTNode.types. - * @param {!IASTNodeLocation} location The position in the AST. - * @param {!ASTNode.Params=} opt_params Optional dictionary of options. - * @constructor - * @alias Blockly.ASTNode */ -const ASTNode = function(type, location, opt_params) { - if (!location) { - throw Error('Cannot create a node without a location.'); +class ASTNode { + /** + * @param {string} type The type of the location. + * Must be in ASTNode.types. + * @param {!IASTNodeLocation} location The position in the AST. + * @param {!ASTNode.Params=} opt_params Optional dictionary of options. + * @alias Blockly.ASTNode + */ + constructor(type, location, opt_params) { + if (!location) { + throw Error('Cannot create a node without a location.'); + } + + /** + * The type of the location. + * One of ASTNode.types + * @type {string} + * @private + */ + this.type_ = type; + + /** + * Whether the location points to a connection. + * @type {boolean} + * @private + */ + this.isConnection_ = ASTNode.isConnectionType_(type); + + /** + * The location of the AST node. + * @type {!IASTNodeLocation} + * @private + */ + this.location_ = location; + + /** + * The coordinate on the workspace. + * @type {Coordinate} + * @private + */ + this.wsCoordinate_ = null; + + this.processParams_(opt_params || null); } /** - * The type of the location. - * One of ASTNode.types - * @type {string} + * Parse the optional parameters. + * @param {?ASTNode.Params} params The user specified parameters. * @private */ - this.type_ = type; + processParams_(params) { + if (!params) { + return; + } + if (params.wsCoordinate) { + this.wsCoordinate_ = params.wsCoordinate; + } + } /** - * Whether the location points to a connection. - * @type {boolean} - * @private + * 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 {!IASTNodeLocation} The current field, connection, workspace, or + * block the cursor is on. */ - this.isConnection_ = ASTNode.isConnectionType_(type); + getLocation() { + return this.location_; + } /** - * The location of the AST node. - * @type {!IASTNodeLocation} - * @private + * The type of the current location. + * One of ASTNode.types + * @return {string} The type of the location. */ - this.location_ = location; + getType() { + return this.type_; + } /** * The coordinate on the workspace. - * @type {Coordinate} - * @private + * @return {Coordinate} The workspace coordinate or null if the + * location is not a workspace. */ - this.wsCoordinate_ = null; - - this.processParams_(opt_params || null); -}; - -/** - * @typedef {{ - * wsCoordinate: Coordinate - * }} - */ -ASTNode.Params; - -/** - * Object holding different types for an AST node. - * @enum {string} - */ -ASTNode.types = { - FIELD: 'field', - BLOCK: 'block', - INPUT: 'input', - OUTPUT: 'output', - NEXT: 'next', - PREVIOUS: 'previous', - STACK: 'stack', - WORKSPACE: 'workspace', -}; - -/** - * True to navigate to all fields. False to only navigate to clickable fields. - * @type {boolean} - */ -ASTNode.NAVIGATE_ALL_FIELDS = false; - -/** - * The default y offset to use when moving the cursor from a stack to the - * workspace. - * @type {number} - * @private - */ -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 ASTNode.types. - * @return {boolean} True if a node of the given type points to a connection. - * @private - */ -ASTNode.isConnectionType_ = function(type) { - switch (type) { - case ASTNode.types.PREVIOUS: - case ASTNode.types.NEXT: - case ASTNode.types.INPUT: - case ASTNode.types.OUTPUT: - return true; + getWsCoordinate() { + return this.wsCoordinate_; } - return false; -}; -/** - * Create an AST node pointing to a field. - * @param {Field} field The location of the AST node. - * @return {ASTNode} An AST node pointing to a field. - */ -ASTNode.createFieldNode = function(field) { - if (!field) { - return null; + /** + * Whether the node points to a connection. + * @return {boolean} [description] + * @package + */ + isConnection() { + return this.isConnection_; } - return new ASTNode(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 {Connection} connection This is the connection the node will - * point to. - * @return {ASTNode} An AST node pointing to a connection. - */ -ASTNode.createConnectionNode = function(connection) { - if (!connection) { + /** + * 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 {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 + */ + findNextForInput_() { + const location = /** @type {!Connection} */ (this.location_); + const parentInput = location.getParentInput(); + const block = parentInput.getSourceBlock(); + const curIdx = block.inputList.indexOf(parentInput); + for (let i = curIdx + 1; i < block.inputList.length; i++) { + const input = block.inputList[i]; + const fieldRow = input.fieldRow; + for (let j = 0; j < fieldRow.length; j++) { + const field = fieldRow[j]; + if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return ASTNode.createFieldNode(field); + } + } + if (input.connection) { + return ASTNode.createInputNode(input); + } + } return null; } - const type = connection.type; - if (type === ConnectionType.INPUT_VALUE) { - return ASTNode.createInputNode(connection.getParentInput()); - } else if ( - type === ConnectionType.NEXT_STATEMENT && connection.getParentInput()) { - return ASTNode.createInputNode(connection.getParentInput()); - } else if (type === ConnectionType.NEXT_STATEMENT) { - return new ASTNode(ASTNode.types.NEXT, connection); - } else if (type === ConnectionType.OUTPUT_VALUE) { - return new ASTNode(ASTNode.types.OUTPUT, connection); - } else if (type === ConnectionType.PREVIOUS_STATEMENT) { - return new ASTNode(ASTNode.types.PREVIOUS, connection); - } - return null; -}; -/** - * Creates an AST node pointing to an input. Stores the input connection as the - * location. - * @param {Input} input The input used to create an AST node. - * @return {ASTNode} An AST node pointing to a input. - */ -ASTNode.createInputNode = function(input) { - if (!input || !input.connection) { + /** + * 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 {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 + */ + findNextForField_() { + const location = /** @type {!Field} */ (this.location_); + const input = location.getParentInput(); + const block = location.getSourceBlock(); + const curIdx = block.inputList.indexOf(/** @type {!Input} */ (input)); + let fieldIdx = input.fieldRow.indexOf(location) + 1; + for (let i = curIdx; i < block.inputList.length; i++) { + const newInput = block.inputList[i]; + const fieldRow = newInput.fieldRow; + while (fieldIdx < fieldRow.length) { + if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return ASTNode.createFieldNode(fieldRow[fieldIdx]); + } + fieldIdx++; + } + fieldIdx = 0; + if (newInput.connection) { + return ASTNode.createInputNode(newInput); + } + } return null; } - return new ASTNode(ASTNode.types.INPUT, input.connection); -}; -/** - * Creates an AST node pointing to a block. - * @param {Block} block The block used to create an AST node. - * @return {ASTNode} An AST node pointing to a block. - */ -ASTNode.createBlockNode = function(block) { - if (!block) { + /** + * 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 {ASTNode} The AST node holding the previous field or + * connection. + * @private + */ + findPrevForInput_() { + const location = /** @type {!Connection} */ (this.location_); + const parentInput = location.getParentInput(); + const block = parentInput.getSourceBlock(); + const curIdx = block.inputList.indexOf(parentInput); + for (let i = curIdx; i >= 0; i--) { + const input = block.inputList[i]; + if (input.connection && input !== parentInput) { + return ASTNode.createInputNode(input); + } + const fieldRow = input.fieldRow; + for (let j = fieldRow.length - 1; j >= 0; j--) { + const field = fieldRow[j]; + if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return ASTNode.createFieldNode(field); + } + } + } return null; } - return new ASTNode(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 {Block} topBlock A top block has no parent and can be found - * in the list returned by workspace.getTopBlocks(). - * @return {ASTNode} An AST node of type stack that points to the top - * block on the stack. - */ -ASTNode.createStackNode = function(topBlock) { - if (!topBlock) { + /** + * 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 {ASTNode} The AST node holding the previous input or field. + * @private + */ + findPrevForField_() { + const location = /** @type {!Field} */ (this.location_); + const parentInput = location.getParentInput(); + const block = location.getSourceBlock(); + const curIdx = block.inputList.indexOf( + /** @type {!Input} */ (parentInput)); + let fieldIdx = parentInput.fieldRow.indexOf(location) - 1; + for (let i = curIdx; i >= 0; i--) { + const input = block.inputList[i]; + if (input.connection && input !== parentInput) { + return ASTNode.createInputNode(input); + } + const fieldRow = input.fieldRow; + while (fieldIdx > -1) { + if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return 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; } - return new ASTNode(ASTNode.types.STACK, topBlock); -}; -/** - * Creates an AST node pointing to a workspace. - * @param {!Workspace} workspace The workspace that we are on. - * @param {Coordinate} wsCoordinate The position on the workspace - * for this node. - * @return {ASTNode} An AST node pointing to a workspace and a position - * on the workspace. - */ -ASTNode.createWorkspaceNode = function(workspace, wsCoordinate) { - if (!wsCoordinate || !workspace) { - return null; + /** + * Navigate between stacks of blocks on the workspace. + * @param {boolean} forward True to go forward. False to go backwards. + * @return {ASTNode} The first block of the next stack or null if there + * are no blocks on the workspace. + * @private + */ + navigateBetweenStacks_(forward) { + let curLocation = this.getLocation(); + if (curLocation.getSourceBlock) { + curLocation = /** @type {!IASTNodeLocationWithBlock} */ (curLocation) + .getSourceBlock(); + } + if (!curLocation || !curLocation.workspace) { + return null; + } + const curRoot = curLocation.getRootBlock(); + const topBlocks = curRoot.workspace.getTopBlocks(true); + for (let i = 0; i < topBlocks.length; i++) { + const topBlock = topBlocks[i]; + if (curRoot.id === topBlock.id) { + const offset = forward ? 1 : -1; + const resultIndex = i + offset; + if (resultIndex === -1 || resultIndex === topBlocks.length) { + return null; + } + return ASTNode.createStackNode(topBlocks[resultIndex]); + } + } + throw Error( + 'Couldn\'t find ' + (forward ? 'next' : 'previous') + ' stack?!'); } - const params = {wsCoordinate: wsCoordinate}; - return new ASTNode(ASTNode.types.WORKSPACE, workspace, params); -}; -/** - * Gets the parent connection on a block. - * This is either an output connection, previous connection or undefined. - * If both connections exist return the one that is actually connected - * to another block. - * @param {!Block} block The block to find the parent connection on. - * @return {Connection} The connection connecting to the parent of the - * block. - * @private - */ -const getParentConnection = function(block) { - let topConnection = block.outputConnection; - if (!topConnection || - (block.previousConnection && block.previousConnection.isConnected())) { - topConnection = block.previousConnection; + /** + * 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 {!Block} block The block that we want to find the top + * connection on. + * @return {!ASTNode} The AST node containing the top connection. + * @private + */ + findTopASTNodeForBlock_(block) { + const topConnection = getParentConnection(block); + if (topConnection) { + return /** @type {!ASTNode} */ ( + ASTNode.createConnectionNode(topConnection)); + } else { + return /** @type {!ASTNode} */ (ASTNode.createBlockNode(block)); + } } - return topConnection; -}; -/** - * Creates an AST node for the top position on a block. - * This is either an output connection, previous connection, or block. - * @param {!Block} block The block to find the top most AST node on. - * @return {ASTNode} The AST node holding the top most position on the - * block. - */ -ASTNode.createTopNode = function(block) { - let astNode; - const topConnection = getParentConnection(block); - if (topConnection) { - astNode = ASTNode.createConnectionNode(topConnection); - } else { - astNode = 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 {Block} block The source block of the current location. + * @return {ASTNode} The AST node pointing to the input connection or + * the top block of the stack this block is in. + * @private + */ + getOutAstNodeForBlock_(block) { + if (!block) { + return null; + } + // If the block doesn't have a previous connection then it is the top of the + // substack. + const topBlock = block.getTopStackBlock(); + const topConnection = getParentConnection(topBlock); + // If the top connection has a parentInput, create an AST node pointing to + // that input. + if (topConnection && topConnection.targetConnection && + topConnection.targetConnection.getParentInput()) { + return ASTNode.createInputNode( + topConnection.targetConnection.getParentInput()); + } else { + // Go to stack level if you are not underneath an input. + return ASTNode.createStackNode(topBlock); + } } - return astNode; -}; -/** - * Parse the optional parameters. - * @param {?ASTNode.Params} params The user specified parameters. - * @private - */ -ASTNode.prototype.processParams_ = function(params) { - if (!params) { - return; - } - if (params.wsCoordinate) { - this.wsCoordinate_ = params.wsCoordinate; + /** + * Find the first editable field or input with a connection on a given block. + * @param {!Block} block The source block of the current location. + * @return {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 + */ + findFirstFieldOrInput_(block) { + const inputs = block.inputList; + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + const fieldRow = input.fieldRow; + for (let j = 0; j < fieldRow.length; j++) { + const field = fieldRow[j]; + if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { + return ASTNode.createFieldNode(field); + } + } + if (input.connection) { + return ASTNode.createInputNode(input); + } + } + return null; } -}; -/** - * 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 {!IASTNodeLocation} The current field, connection, workspace, or - * block the cursor is on. - */ -ASTNode.prototype.getLocation = function() { - return this.location_; -}; - -/** - * The type of the current location. - * One of ASTNode.types - * @return {string} The type of the location. - */ -ASTNode.prototype.getType = function() { - return this.type_; -}; + /** + * Finds the source block of the location of this node. + * @return {Block} The source block of the location, or null if the node + * is of type workspace. + */ + getSourceBlock() { + if (this.getType() === ASTNode.types.BLOCK) { + return /** @type {Block} */ (this.getLocation()); + } else if (this.getType() === ASTNode.types.STACK) { + return /** @type {Block} */ (this.getLocation()); + } else if (this.getType() === ASTNode.types.WORKSPACE) { + return null; + } else { + return /** @type {IASTNodeLocationWithBlock} */ (this.getLocation()) + .getSourceBlock(); + } + } -/** - * The coordinate on the workspace. - * @return {Coordinate} The workspace coordinate or null if the - * location is not a workspace. - */ -ASTNode.prototype.getWsCoordinate = function() { - return this.wsCoordinate_; -}; + /** + * Find the element to the right of the current element in the AST. + * @return {ASTNode} An AST node that wraps the next field, connection, + * block, or workspace. Or null if there is no node to the right. + */ + next() { + switch (this.type_) { + case ASTNode.types.STACK: + return this.navigateBetweenStacks_(true); + + case ASTNode.types.OUTPUT: { + const connection = /** @type {!Connection} */ (this.location_); + return ASTNode.createBlockNode(connection.getSourceBlock()); + } + case ASTNode.types.FIELD: + return this.findNextForField_(); -/** - * Whether the node points to a connection. - * @return {boolean} [description] - * @package - */ -ASTNode.prototype.isConnection = function() { - return this.isConnection_; -}; + case ASTNode.types.INPUT: + return this.findNextForInput_(); -/** - * 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 {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 - */ -ASTNode.prototype.findNextForInput_ = function() { - const location = /** @type {!Connection} */ (this.location_); - const parentInput = location.getParentInput(); - const block = parentInput.getSourceBlock(); - const curIdx = block.inputList.indexOf(parentInput); - for (let i = curIdx + 1; i < block.inputList.length; i++) { - const input = block.inputList[i]; - const fieldRow = input.fieldRow; - for (let j = 0; j < fieldRow.length; j++) { - const field = fieldRow[j]; - if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(field); + case ASTNode.types.BLOCK: { + const block = /** @type {!Block} */ (this.location_); + const nextConnection = block.nextConnection; + return ASTNode.createConnectionNode(nextConnection); + } + case ASTNode.types.PREVIOUS: { + const connection = /** @type {!Connection} */ (this.location_); + return ASTNode.createBlockNode(connection.getSourceBlock()); + } + case ASTNode.types.NEXT: { + const connection = /** @type {!Connection} */ (this.location_); + const targetConnection = connection.targetConnection; + return ASTNode.createConnectionNode(targetConnection); } } - if (input.connection) { - return ASTNode.createInputNode(input); - } + + return null; } - 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 {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 - */ -ASTNode.prototype.findNextForField_ = function() { - const location = /** @type {!Field} */ (this.location_); - const input = location.getParentInput(); - const block = location.getSourceBlock(); - const curIdx = block.inputList.indexOf(/** @type {!Input} */ (input)); - let fieldIdx = input.fieldRow.indexOf(location) + 1; - for (let i = curIdx; i < block.inputList.length; i++) { - const newInput = block.inputList[i]; - const fieldRow = newInput.fieldRow; - while (fieldIdx < fieldRow.length) { - if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(fieldRow[fieldIdx]); + /** + * Find the element one level below and all the way to the left of the current + * location. + * @return {ASTNode} An AST node that wraps the next field, connection, + * workspace, or block. Or null if there is nothing below this node. + */ + in() { + switch (this.type_) { + case ASTNode.types.WORKSPACE: { + const workspace = /** @type {!Workspace} */ (this.location_); + const topBlocks = workspace.getTopBlocks(true); + if (topBlocks.length > 0) { + return ASTNode.createStackNode(topBlocks[0]); + } + break; + } + case ASTNode.types.STACK: { + const block = /** @type {!Block} */ (this.location_); + return this.findTopASTNodeForBlock_(block); + } + case ASTNode.types.BLOCK: { + const block = /** @type {!Block} */ (this.location_); + return this.findFirstFieldOrInput_(block); + } + case ASTNode.types.INPUT: { + const connection = /** @type {!Connection} */ (this.location_); + const targetConnection = connection.targetConnection; + return ASTNode.createConnectionNode(targetConnection); } - fieldIdx++; - } - fieldIdx = 0; - if (newInput.connection) { - return ASTNode.createInputNode(newInput); } + + return null; } - 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 {ASTNode} The AST node holding the previous field or - * connection. - * @private - */ -ASTNode.prototype.findPrevForInput_ = function() { - const location = /** @type {!Connection} */ (this.location_); - const parentInput = location.getParentInput(); - const block = parentInput.getSourceBlock(); - const curIdx = block.inputList.indexOf(parentInput); - for (let i = curIdx; i >= 0; i--) { - const input = block.inputList[i]; - if (input.connection && input !== parentInput) { - return ASTNode.createInputNode(input); - } - const fieldRow = input.fieldRow; - for (let j = fieldRow.length - 1; j >= 0; j--) { - const field = fieldRow[j]; - if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(field); + /** + * Find the element to the left of the current element in the AST. + * @return {ASTNode} An AST node that wraps the previous field, + * connection, workspace or block. Or null if no node exists to the left. + * null. + */ + prev() { + switch (this.type_) { + case ASTNode.types.STACK: + return this.navigateBetweenStacks_(false); + + case ASTNode.types.OUTPUT: + return null; + + case ASTNode.types.FIELD: + return this.findPrevForField_(); + + case ASTNode.types.INPUT: + return this.findPrevForInput_(); + + case ASTNode.types.BLOCK: { + const block = /** @type {!Block} */ (this.location_); + const topConnection = getParentConnection(block); + return ASTNode.createConnectionNode(topConnection); + } + case ASTNode.types.PREVIOUS: { + const connection = /** @type {!Connection} */ (this.location_); + const targetConnection = connection.targetConnection; + if (targetConnection && !targetConnection.getParentInput()) { + return ASTNode.createConnectionNode(targetConnection); + } + break; + } + case ASTNode.types.NEXT: { + const connection = /** @type {!Connection} */ (this.location_); + return ASTNode.createBlockNode(connection.getSourceBlock()); } } + + return null; } - 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 {ASTNode} The AST node holding the previous input or field. - * @private - */ -ASTNode.prototype.findPrevForField_ = function() { - const location = /** @type {!Field} */ (this.location_); - const parentInput = location.getParentInput(); - const block = location.getSourceBlock(); - const curIdx = block.inputList.indexOf( - /** @type {!Input} */ (parentInput)); - let fieldIdx = parentInput.fieldRow.indexOf(location) - 1; - for (let i = curIdx; i >= 0; i--) { - const input = block.inputList[i]; - if (input.connection && input !== parentInput) { - return ASTNode.createInputNode(input); - } - const fieldRow = input.fieldRow; - while (fieldIdx > -1) { - if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(fieldRow[fieldIdx]); + /** + * Find the next element that is one position above and all the way to the + * left of the current location. + * @return {ASTNode} An AST node that wraps the next field, connection, + * workspace or block. Or null if we are at the workspace level. + */ + out() { + switch (this.type_) { + case ASTNode.types.STACK: { + const block = /** @type {!Block} */ (this.location_); + const blockPos = block.getRelativeToSurfaceXY(); + // TODO: Make sure this is in the bounds of the workspace. + const wsCoordinate = + new Coordinate(blockPos.x, blockPos.y + ASTNode.DEFAULT_OFFSET_Y); + return ASTNode.createWorkspaceNode(block.workspace, wsCoordinate); + } + case ASTNode.types.OUTPUT: { + const connection = /** @type {!Connection} */ (this.location_); + const target = connection.targetConnection; + if (target) { + return ASTNode.createConnectionNode(target); + } + return ASTNode.createStackNode(connection.getSourceBlock()); + } + case ASTNode.types.FIELD: { + const field = /** @type {!Field} */ (this.location_); + return ASTNode.createBlockNode(field.getSourceBlock()); + } + case ASTNode.types.INPUT: { + const connection = /** @type {!Connection} */ (this.location_); + return ASTNode.createBlockNode(connection.getSourceBlock()); + } + case ASTNode.types.BLOCK: { + const block = /** @type {!Block} */ (this.location_); + return this.getOutAstNodeForBlock_(block); + } + case ASTNode.types.PREVIOUS: { + const connection = /** @type {!Connection} */ (this.location_); + return this.getOutAstNodeForBlock_(connection.getSourceBlock()); + } + case ASTNode.types.NEXT: { + const connection = /** @type {!Connection} */ (this.location_); + return this.getOutAstNodeForBlock_(connection.getSourceBlock()); } - 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 {ASTNode} The first block of the next stack or null if there - * are no blocks on the workspace. - * @private - */ -ASTNode.prototype.navigateBetweenStacks_ = function(forward) { - let curLocation = this.getLocation(); - if (curLocation.getSourceBlock) { - curLocation = /** @type {!IASTNodeLocationWithBlock} */ (curLocation) - .getSourceBlock(); - } - if (!curLocation || !curLocation.workspace) { return null; } - const curRoot = curLocation.getRootBlock(); - const topBlocks = curRoot.workspace.getTopBlocks(true); - for (let i = 0; i < topBlocks.length; i++) { - const topBlock = topBlocks[i]; - if (curRoot.id === topBlock.id) { - const offset = forward ? 1 : -1; - const resultIndex = i + offset; - if (resultIndex === -1 || resultIndex === topBlocks.length) { - return null; - } - return ASTNode.createStackNode(topBlocks[resultIndex]); + + /** + * Whether an AST node of the given type points to a connection. + * @param {string} type The type to check. One of ASTNode.types. + * @return {boolean} True if a node of the given type points to a connection. + * @private + */ + static isConnectionType_(type) { + switch (type) { + case ASTNode.types.PREVIOUS: + case ASTNode.types.NEXT: + case ASTNode.types.INPUT: + case ASTNode.types.OUTPUT: + return true; } + return false; } - 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 {!Block} block The block that we want to find the top - * connection on. - * @return {!ASTNode} The AST node containing the top connection. - * @private - */ -ASTNode.prototype.findTopASTNodeForBlock_ = function(block) { - const topConnection = getParentConnection(block); - if (topConnection) { - return /** @type {!ASTNode} */ ( - ASTNode.createConnectionNode(topConnection)); - } else { - return /** @type {!ASTNode} */ (ASTNode.createBlockNode(block)); + /** + * Create an AST node pointing to a field. + * @param {Field} field The location of the AST node. + * @return {ASTNode} An AST node pointing to a field. + */ + static createFieldNode(field) { + if (!field) { + return null; + } + return new ASTNode(ASTNode.types.FIELD, field); } -}; -/** - * 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 {Block} block The source block of the current location. - * @return {ASTNode} The AST node pointing to the input connection or - * the top block of the stack this block is in. - * @private - */ -ASTNode.prototype.getOutAstNodeForBlock_ = function(block) { - if (!block) { + /** + * 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 {Connection} connection This is the connection the node will + * point to. + * @return {ASTNode} An AST node pointing to a connection. + */ + static createConnectionNode(connection) { + if (!connection) { + return null; + } + const type = connection.type; + if (type === ConnectionType.INPUT_VALUE) { + return ASTNode.createInputNode(connection.getParentInput()); + } else if ( + type === ConnectionType.NEXT_STATEMENT && connection.getParentInput()) { + return ASTNode.createInputNode(connection.getParentInput()); + } else if (type === ConnectionType.NEXT_STATEMENT) { + return new ASTNode(ASTNode.types.NEXT, connection); + } else if (type === ConnectionType.OUTPUT_VALUE) { + return new ASTNode(ASTNode.types.OUTPUT, connection); + } else if (type === ConnectionType.PREVIOUS_STATEMENT) { + return new ASTNode(ASTNode.types.PREVIOUS, connection); + } return null; } - // If the block doesn't have a previous connection then it is the top of the - // substack. - const topBlock = block.getTopStackBlock(); - const topConnection = getParentConnection(topBlock); - // If the top connection has a parentInput, create an AST node pointing to - // that input. - if (topConnection && topConnection.targetConnection && - topConnection.targetConnection.getParentInput()) { - return ASTNode.createInputNode( - topConnection.targetConnection.getParentInput()); - } else { - // Go to stack level if you are not underneath an input. - return ASTNode.createStackNode(topBlock); - } -}; -/** - * Find the first editable field or input with a connection on a given block. - * @param {!Block} block The source block of the current location. - * @return {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 - */ -ASTNode.prototype.findFirstFieldOrInput_ = function(block) { - const inputs = block.inputList; - for (let i = 0; i < inputs.length; i++) { - const input = inputs[i]; - const fieldRow = input.fieldRow; - for (let j = 0; j < fieldRow.length; j++) { - const field = fieldRow[j]; - if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) { - return ASTNode.createFieldNode(field); - } - } - if (input.connection) { - return ASTNode.createInputNode(input); + /** + * Creates an AST node pointing to an input. Stores the input connection as + * the location. + * @param {Input} input The input used to create an AST node. + * @return {ASTNode} An AST node pointing to a input. + */ + static createInputNode(input) { + if (!input || !input.connection) { + return null; } + return new ASTNode(ASTNode.types.INPUT, input.connection); } - return null; -}; -/** - * Finds the source block of the location of this node. - * @return {Block} The source block of the location, or null if the node - * is of type workspace. - */ -ASTNode.prototype.getSourceBlock = function() { - if (this.getType() === ASTNode.types.BLOCK) { - return /** @type {Block} */ (this.getLocation()); - } else if (this.getType() === ASTNode.types.STACK) { - return /** @type {Block} */ (this.getLocation()); - } else if (this.getType() === ASTNode.types.WORKSPACE) { - return null; - } else { - return /** @type {IASTNodeLocationWithBlock} */ (this.getLocation()) - .getSourceBlock(); + /** + * Creates an AST node pointing to a block. + * @param {Block} block The block used to create an AST node. + * @return {ASTNode} An AST node pointing to a block. + */ + static createBlockNode(block) { + if (!block) { + return null; + } + return new ASTNode(ASTNode.types.BLOCK, block); } -}; -/** - * Find the element to the right of the current element in the AST. - * @return {ASTNode} An AST node that wraps the next field, connection, - * block, or workspace. Or null if there is no node to the right. - */ -ASTNode.prototype.next = function() { - switch (this.type_) { - case ASTNode.types.STACK: - return this.navigateBetweenStacks_(true); - - case ASTNode.types.OUTPUT: { - const connection = /** @type {!Connection} */ (this.location_); - return ASTNode.createBlockNode(connection.getSourceBlock()); + /** + * 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 {Block} topBlock A top block has no parent and can be found + * in the list returned by workspace.getTopBlocks(). + * @return {ASTNode} An AST node of type stack that points to the top + * block on the stack. + */ + static createStackNode(topBlock) { + if (!topBlock) { + return null; } - case ASTNode.types.FIELD: - return this.findNextForField_(); - - case ASTNode.types.INPUT: - return this.findNextForInput_(); + return new ASTNode(ASTNode.types.STACK, topBlock); + } - case ASTNode.types.BLOCK: { - const block = /** @type {!Block} */ (this.location_); - const nextConnection = block.nextConnection; - return ASTNode.createConnectionNode(nextConnection); - } - case ASTNode.types.PREVIOUS: { - const connection = /** @type {!Connection} */ (this.location_); - return ASTNode.createBlockNode(connection.getSourceBlock()); - } - case ASTNode.types.NEXT: { - const connection = /** @type {!Connection} */ (this.location_); - const targetConnection = connection.targetConnection; - return ASTNode.createConnectionNode(targetConnection); + /** + * Creates an AST node pointing to a workspace. + * @param {!Workspace} workspace The workspace that we are on. + * @param {Coordinate} wsCoordinate The position on the workspace + * for this node. + * @return {ASTNode} An AST node pointing to a workspace and a position + * on the workspace. + */ + static createWorkspaceNode(workspace, wsCoordinate) { + if (!wsCoordinate || !workspace) { + return null; } + const params = {wsCoordinate: wsCoordinate}; + return new ASTNode(ASTNode.types.WORKSPACE, workspace, params); } - return null; -}; + /** + * Creates an AST node for the top position on a block. + * This is either an output connection, previous connection, or block. + * @param {!Block} block The block to find the top most AST node on. + * @return {ASTNode} The AST node holding the top most position on the + * block. + */ + static createTopNode(block) { + let astNode; + const topConnection = getParentConnection(block); + if (topConnection) { + astNode = ASTNode.createConnectionNode(topConnection); + } else { + astNode = ASTNode.createBlockNode(block); + } + return astNode; + } +} /** - * Find the element one level below and all the way to the left of the current - * location. - * @return {ASTNode} An AST node that wraps the next field, connection, - * workspace, or block. Or null if there is nothing below this node. + * @typedef {{ + * wsCoordinate: Coordinate + * }} */ -ASTNode.prototype.in = function() { - switch (this.type_) { - case ASTNode.types.WORKSPACE: { - const workspace = /** @type {!Workspace} */ (this.location_); - const topBlocks = workspace.getTopBlocks(true); - if (topBlocks.length > 0) { - return ASTNode.createStackNode(topBlocks[0]); - } - break; - } - case ASTNode.types.STACK: { - const block = /** @type {!Block} */ (this.location_); - return this.findTopASTNodeForBlock_(block); - } - case ASTNode.types.BLOCK: { - const block = /** @type {!Block} */ (this.location_); - return this.findFirstFieldOrInput_(block); - } - case ASTNode.types.INPUT: { - const connection = /** @type {!Connection} */ (this.location_); - const targetConnection = connection.targetConnection; - return ASTNode.createConnectionNode(targetConnection); - } - } +ASTNode.Params; - return null; +/** + * Object holding different types for an AST node. + * @enum {string} + */ +ASTNode.types = { + FIELD: 'field', + BLOCK: 'block', + INPUT: 'input', + OUTPUT: 'output', + NEXT: 'next', + PREVIOUS: 'previous', + STACK: 'stack', + WORKSPACE: 'workspace', }; /** - * Find the element to the left of the current element in the AST. - * @return {ASTNode} An AST node that wraps the previous field, - * connection, workspace or block. Or null if no node exists to the left. - * null. + * True to navigate to all fields. False to only navigate to clickable fields. + * @type {boolean} */ -ASTNode.prototype.prev = function() { - switch (this.type_) { - case ASTNode.types.STACK: - return this.navigateBetweenStacks_(false); - - case ASTNode.types.OUTPUT: - return null; - - case ASTNode.types.FIELD: - return this.findPrevForField_(); - - case ASTNode.types.INPUT: - return this.findPrevForInput_(); - - case ASTNode.types.BLOCK: { - const block = /** @type {!Block} */ (this.location_); - const topConnection = getParentConnection(block); - return ASTNode.createConnectionNode(topConnection); - } - case ASTNode.types.PREVIOUS: { - const connection = /** @type {!Connection} */ (this.location_); - const targetConnection = connection.targetConnection; - if (targetConnection && !targetConnection.getParentInput()) { - return ASTNode.createConnectionNode(targetConnection); - } - break; - } - case ASTNode.types.NEXT: { - const connection = /** @type {!Connection} */ (this.location_); - return ASTNode.createBlockNode(connection.getSourceBlock()); - } - } +ASTNode.NAVIGATE_ALL_FIELDS = false; - return null; -}; +/** + * The default y offset to use when moving the cursor from a stack to the + * workspace. + * @type {number} + * @private + */ +ASTNode.DEFAULT_OFFSET_Y = -20; /** - * Find the next element that is one position above and all the way to the left - * of the current location. - * @return {ASTNode} An AST node that wraps the next field, connection, - * workspace or block. Or null if we are at the workspace level. + * Gets the parent connection on a block. + * This is either an output connection, previous connection or undefined. + * If both connections exist return the one that is actually connected + * to another block. + * @param {!Block} block The block to find the parent connection on. + * @return {Connection} The connection connecting to the parent of the + * block. + * @private */ -ASTNode.prototype.out = function() { - switch (this.type_) { - case ASTNode.types.STACK: { - const block = /** @type {!Block} */ (this.location_); - const blockPos = block.getRelativeToSurfaceXY(); - // TODO: Make sure this is in the bounds of the workspace. - const wsCoordinate = - new Coordinate(blockPos.x, blockPos.y + ASTNode.DEFAULT_OFFSET_Y); - return ASTNode.createWorkspaceNode(block.workspace, wsCoordinate); - } - case ASTNode.types.OUTPUT: { - const connection = /** @type {!Connection} */ (this.location_); - const target = connection.targetConnection; - if (target) { - return ASTNode.createConnectionNode(target); - } - return ASTNode.createStackNode(connection.getSourceBlock()); - } - case ASTNode.types.FIELD: { - const field = /** @type {!Field} */ (this.location_); - return ASTNode.createBlockNode(field.getSourceBlock()); - } - case ASTNode.types.INPUT: { - const connection = /** @type {!Connection} */ (this.location_); - return ASTNode.createBlockNode(connection.getSourceBlock()); - } - case ASTNode.types.BLOCK: { - const block = /** @type {!Block} */ (this.location_); - return this.getOutAstNodeForBlock_(block); - } - case ASTNode.types.PREVIOUS: { - const connection = /** @type {!Connection} */ (this.location_); - return this.getOutAstNodeForBlock_(connection.getSourceBlock()); - } - case ASTNode.types.NEXT: { - const connection = /** @type {!Connection} */ (this.location_); - return this.getOutAstNodeForBlock_(connection.getSourceBlock()); - } +const getParentConnection = function(block) { + let topConnection = block.outputConnection; + if (!topConnection || + (block.previousConnection && block.previousConnection.isConnected())) { + topConnection = block.previousConnection; } - - return null; + return topConnection; }; exports.ASTNode = ASTNode; diff --git a/core/keyboard_nav/basic_cursor.js b/core/keyboard_nav/basic_cursor.js index c6b5c0ba59a..f459ea6ee7a 100644 --- a/core/keyboard_nav/basic_cursor.js +++ b/core/keyboard_nav/basic_cursor.js @@ -17,7 +17,6 @@ */ goog.module('Blockly.BasicCursor'); -const object = goog.require('Blockly.utils.object'); const registry = goog.require('Blockly.registry'); const {ASTNode} = goog.require('Blockly.ASTNode'); const {Cursor} = goog.require('Blockly.Cursor'); @@ -27,196 +26,197 @@ const {Cursor} = goog.require('Blockly.Cursor'); * Class for a basic cursor. * This will allow the user to get to all nodes in the AST by hitting next or * previous. - * @constructor * @extends {Cursor} - * @alias Blockly.BasicCursor */ -const BasicCursor = function() { - BasicCursor.superClass_.constructor.call(this); -}; -object.inherits(BasicCursor, Cursor); - -/** - * Name used for registering a basic cursor. - * @const {string} - */ -BasicCursor.registrationName = 'basicCursor'; - -/** - * Find the next node in the pre order traversal. - * @return {?ASTNode} The next node, or null if the current node is - * not set or there is no next value. - * @override - */ -BasicCursor.prototype.next = function() { - const curNode = this.getCurNode(); - if (!curNode) { - return null; +class BasicCursor extends Cursor { + /** + * @alias Blockly.BasicCursor + */ + constructor() { + super(); } - const newNode = this.getNextNode_(curNode, this.validNode_); - if (newNode) { - this.setCurNode(newNode); + /** + * Find the next node in the pre order traversal. + * @return {?ASTNode} The next node, or null if the current node is + * not set or there is no next value. + * @override + */ + next() { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getNextNode_(curNode, this.validNode_); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; } - return newNode; -}; -/** - * For a basic cursor we only have the ability to go next and previous, so - * in will also allow the user to get to the next node in the pre order - * traversal. - * @return {?ASTNode} The next node, or null if the current node is - * not set or there is no next value. - * @override - */ -BasicCursor.prototype.in = function() { - return this.next(); -}; - -/** - * Find the previous node in the pre order traversal. - * @return {?ASTNode} The previous node, or null if the current node - * is not set or there is no previous value. - * @override - */ -BasicCursor.prototype.prev = function() { - const curNode = this.getCurNode(); - if (!curNode) { - return null; + /** + * For a basic cursor we only have the ability to go next and previous, so + * in will also allow the user to get to the next node in the pre order + * traversal. + * @return {?ASTNode} The next node, or null if the current node is + * not set or there is no next value. + * @override + */ + in() { + return this.next(); } - const newNode = this.getPreviousNode_(curNode, this.validNode_); - if (newNode) { - this.setCurNode(newNode); + /** + * Find the previous node in the pre order traversal. + * @return {?ASTNode} The previous node, or null if the current node + * is not set or there is no previous value. + * @override + */ + prev() { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getPreviousNode_(curNode, this.validNode_); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; } - return newNode; -}; -/** - * For a basic cursor we only have the ability to go next and previous, so - * out will allow the user to get to the previous node in the pre order - * traversal. - * @return {?ASTNode} The previous node, or null if the current node is - * not set or there is no previous value. - * @override - */ -BasicCursor.prototype.out = function() { - return this.prev(); -}; + /** + * For a basic cursor we only have the ability to go next and previous, so + * out will allow the user to get to the previous node in the pre order + * traversal. + * @return {?ASTNode} The previous node, or null if the current node is + * not set or there is no previous value. + * @override + */ + out() { + return this.prev(); + } -/** - * Uses pre order traversal to navigate the Blockly AST. This will allow - * a user to easily navigate the entire Blockly AST without having to go in - * and out levels on the tree. - * @param {?ASTNode} node The current position in the AST. - * @param {!function(ASTNode) : boolean} isValid A function true/false - * depending on whether the given node should be traversed. - * @return {?ASTNode} The next node in the traversal. - * @protected - */ -BasicCursor.prototype.getNextNode_ = function(node, isValid) { - if (!node) { + /** + * Uses pre order traversal to navigate the Blockly AST. This will allow + * a user to easily navigate the entire Blockly AST without having to go in + * and out levels on the tree. + * @param {?ASTNode} node The current position in the AST. + * @param {!function(ASTNode) : boolean} isValid A function true/false + * depending on whether the given node should be traversed. + * @return {?ASTNode} The next node in the traversal. + * @protected + */ + getNextNode_(node, isValid) { + if (!node) { + return null; + } + const newNode = node.in() || node.next(); + if (isValid(newNode)) { + return newNode; + } else if (newNode) { + return this.getNextNode_(newNode, isValid); + } + const siblingOrParent = this.findSiblingOrParent_(node.out()); + if (isValid(siblingOrParent)) { + return siblingOrParent; + } else if (siblingOrParent) { + return this.getNextNode_(siblingOrParent, isValid); + } return null; } - const newNode = node.in() || node.next(); - if (isValid(newNode)) { - return newNode; - } else if (newNode) { - return this.getNextNode_(newNode, isValid); - } - const siblingOrParent = this.findSiblingOrParent_(node.out()); - if (isValid(siblingOrParent)) { - return siblingOrParent; - } else if (siblingOrParent) { - return this.getNextNode_(siblingOrParent, isValid); - } - return null; -}; -/** - * Reverses the pre order traversal in order to find the previous node. This - * will allow a user to easily navigate the entire Blockly AST without having to - * go in and out levels on the tree. - * @param {?ASTNode} node The current position in the AST. - * @param {!function(ASTNode) : boolean} isValid A function true/false - * depending on whether the given node should be traversed. - * @return {?ASTNode} The previous node in the traversal or null if no - * previous node exists. - * @protected - */ -BasicCursor.prototype.getPreviousNode_ = function(node, isValid) { - if (!node) { + /** + * Reverses the pre order traversal in order to find the previous node. This + * will allow a user to easily navigate the entire Blockly AST without having + * to go in and out levels on the tree. + * @param {?ASTNode} node The current position in the AST. + * @param {!function(ASTNode) : boolean} isValid A function true/false + * depending on whether the given node should be traversed. + * @return {?ASTNode} The previous node in the traversal or null if no + * previous node exists. + * @protected + */ + getPreviousNode_(node, isValid) { + if (!node) { + return null; + } + let newNode = node.prev(); + + if (newNode) { + newNode = this.getRightMostChild_(newNode); + } else { + newNode = node.out(); + } + if (isValid(newNode)) { + return newNode; + } else if (newNode) { + return this.getPreviousNode_(newNode, isValid); + } return null; } - let newNode = node.prev(); - if (newNode) { - newNode = this.getRightMostChild_(newNode); - } else { - newNode = node.out(); - } - if (isValid(newNode)) { - return newNode; - } else if (newNode) { - return this.getPreviousNode_(newNode, isValid); + /** + * Decides what nodes to traverse and which ones to skip. Currently, it + * skips output, stack and workspace nodes. + * @param {?ASTNode} node The AST node to check whether it is valid. + * @return {boolean} True if the node should be visited, false otherwise. + * @protected + */ + validNode_(node) { + let isValid = false; + const type = node && node.getType(); + if (type === ASTNode.types.OUTPUT || type === ASTNode.types.INPUT || + type === ASTNode.types.FIELD || type === ASTNode.types.NEXT || + type === ASTNode.types.PREVIOUS || type === ASTNode.types.WORKSPACE) { + isValid = true; + } + return isValid; } - return null; -}; -/** - * Decides what nodes to traverse and which ones to skip. Currently, it - * skips output, stack and workspace nodes. - * @param {?ASTNode} node The AST node to check whether it is valid. - * @return {boolean} True if the node should be visited, false otherwise. - * @protected - */ -BasicCursor.prototype.validNode_ = function(node) { - let isValid = false; - const type = node && node.getType(); - if (type === ASTNode.types.OUTPUT || type === ASTNode.types.INPUT || - type === ASTNode.types.FIELD || type === ASTNode.types.NEXT || - type === ASTNode.types.PREVIOUS || type === ASTNode.types.WORKSPACE) { - isValid = true; + /** + * From the given node find either the next valid sibling or parent. + * @param {?ASTNode} node The current position in the AST. + * @return {?ASTNode} The parent AST node or null if there are no + * valid parents. + * @private + */ + findSiblingOrParent_(node) { + if (!node) { + return null; + } + const nextNode = node.next(); + if (nextNode) { + return nextNode; + } + return this.findSiblingOrParent_(node.out()); } - return isValid; -}; -/** - * From the given node find either the next valid sibling or parent. - * @param {?ASTNode} node The current position in the AST. - * @return {?ASTNode} The parent AST node or null if there are no - * valid parents. - * @private - */ -BasicCursor.prototype.findSiblingOrParent_ = function(node) { - if (!node) { - return null; + /** + * Get the right most child of a node. + * @param {?ASTNode} node The node to find the right most child of. + * @return {?ASTNode} The right most child of the given node, or the node + * if no child exists. + * @private + */ + getRightMostChild_(node) { + if (!node.in()) { + return node; + } + let newNode = node.in(); + while (newNode.next()) { + newNode = newNode.next(); + } + return this.getRightMostChild_(newNode); } - const nextNode = node.next(); - if (nextNode) { - return nextNode; - } - return this.findSiblingOrParent_(node.out()); -}; - +} /** - * Get the right most child of a node. - * @param {?ASTNode} node The node to find the right most child of. - * @return {?ASTNode} The right most child of the given node, or the node - * if no child exists. - * @private + * Name used for registering a basic cursor. + * @const {string} */ -BasicCursor.prototype.getRightMostChild_ = function(node) { - if (!node.in()) { - return node; - } - let newNode = node.in(); - while (newNode.next()) { - newNode = newNode.next(); - } - return this.getRightMostChild_(newNode); -}; +BasicCursor.registrationName = 'basicCursor'; registry.register( registry.Type.CURSOR, BasicCursor.registrationName, BasicCursor); diff --git a/core/keyboard_nav/cursor.js b/core/keyboard_nav/cursor.js index 6360fb8666f..80f0ff695a6 100644 --- a/core/keyboard_nav/cursor.js +++ b/core/keyboard_nav/cursor.js @@ -17,7 +17,6 @@ */ goog.module('Blockly.Cursor'); -const object = goog.require('Blockly.utils.object'); const registry = goog.require('Blockly.registry'); const {ASTNode} = goog.require('Blockly.ASTNode'); const {Marker} = goog.require('Blockly.Marker'); @@ -25,117 +24,119 @@ const {Marker} = goog.require('Blockly.Marker'); /** * Class for a cursor. * A cursor controls how a user navigates the Blockly AST. - * @constructor * @extends {Marker} - * @alias Blockly.Cursor */ -const Cursor = function() { - Cursor.superClass_.constructor.call(this); - +class Cursor extends Marker { /** - * @override + * @alias Blockly.Cursor */ - this.type = 'cursor'; -}; -object.inherits(Cursor, Marker); - -/** - * Find the next connection, field, or block. - * @return {ASTNode} The next element, or null if the current node is - * not set or there is no next value. - * @public - */ -Cursor.prototype.next = function() { - const curNode = this.getCurNode(); - if (!curNode) { - return null; - } - - let newNode = curNode.next(); - while (newNode && newNode.next() && - (newNode.getType() === ASTNode.types.NEXT || - newNode.getType() === ASTNode.types.BLOCK)) { - newNode = newNode.next(); - } - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; -}; - -/** - * Find the in connection or field. - * @return {ASTNode} The in element, or null if the current node is - * not set or there is no in value. - * @public - */ -Cursor.prototype.in = function() { - let curNode = this.getCurNode(); - if (!curNode) { - return null; - } - // If we are on a previous or output connection, go to the block level before - // performing next operation. - if (curNode.getType() === ASTNode.types.PREVIOUS || - curNode.getType() === ASTNode.types.OUTPUT) { - curNode = curNode.next(); - } - const newNode = curNode.in(); - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; -}; - -/** - * Find the previous connection, field, or block. - * @return {ASTNode} The previous element, or null if the current node - * is not set or there is no previous value. - * @public - */ -Cursor.prototype.prev = function() { - const curNode = this.getCurNode(); - if (!curNode) { - return null; - } - let newNode = curNode.prev(); + constructor() { + super(); - while (newNode && newNode.prev() && - (newNode.getType() === ASTNode.types.NEXT || - newNode.getType() === ASTNode.types.BLOCK)) { - newNode = newNode.prev(); + /** + * @override + */ + this.type = 'cursor'; } - if (newNode) { - this.setCurNode(newNode); + /** + * Find the next connection, field, or block. + * @return {ASTNode} The next element, or null if the current node is + * not set or there is no next value. + * @public + */ + next() { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + + let newNode = curNode.next(); + while (newNode && newNode.next() && + (newNode.getType() === ASTNode.types.NEXT || + newNode.getType() === ASTNode.types.BLOCK)) { + newNode = newNode.next(); + } + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; } - return newNode; -}; -/** - * Find the out connection, field, or block. - * @return {ASTNode} The out element, or null if the current node is - * not set or there is no out value. - * @public - */ -Cursor.prototype.out = function() { - const curNode = this.getCurNode(); - if (!curNode) { - return null; + /** + * Find the in connection or field. + * @return {ASTNode} The in element, or null if the current node is + * not set or there is no in value. + * @public + */ + in() { + let curNode = this.getCurNode(); + if (!curNode) { + return null; + } + // If we are on a previous or output connection, go to the block level + // before performing next operation. + if (curNode.getType() === ASTNode.types.PREVIOUS || + curNode.getType() === ASTNode.types.OUTPUT) { + curNode = curNode.next(); + } + const newNode = curNode.in(); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; } - let newNode = curNode.out(); - if (newNode && newNode.getType() === ASTNode.types.BLOCK) { - newNode = newNode.prev() || newNode; + /** + * Find the previous connection, field, or block. + * @return {ASTNode} The previous element, or null if the current node + * is not set or there is no previous value. + * @public + */ + prev() { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + let newNode = curNode.prev(); + + while (newNode && newNode.prev() && + (newNode.getType() === ASTNode.types.NEXT || + newNode.getType() === ASTNode.types.BLOCK)) { + newNode = newNode.prev(); + } + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; } - if (newNode) { - this.setCurNode(newNode); + /** + * Find the out connection, field, or block. + * @return {ASTNode} The out element, or null if the current node is + * not set or there is no out value. + * @public + */ + out() { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + let newNode = curNode.out(); + + if (newNode && newNode.getType() === ASTNode.types.BLOCK) { + newNode = newNode.prev() || newNode; + } + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; } - return newNode; -}; +} registry.register(registry.Type.CURSOR, registry.DEFAULT, Cursor); diff --git a/core/keyboard_nav/marker.js b/core/keyboard_nav/marker.js index a38ad0c8ec8..434c8253063 100644 --- a/core/keyboard_nav/marker.js +++ b/core/keyboard_nav/marker.js @@ -26,105 +26,108 @@ const {MarkerSvg} = goog.requireType('Blockly.blockRendering.MarkerSvg'); /** * Class for a marker. * This is used in keyboard navigation to save a location in the Blockly AST. - * @constructor - * @alias Blockly.Marker */ -const Marker = function() { +class Marker { /** - * The colour of the marker. - * @type {?string} + * @alias Blockly.Marker */ - this.colour = null; + constructor() { + /** + * The colour of the marker. + * @type {?string} + */ + this.colour = null; + + /** + * The current location of the marker. + * @type {ASTNode} + * @private + */ + this.curNode_ = null; + + /** + * The object in charge of drawing the visual representation of the current + * node. + * @type {MarkerSvg} + * @private + */ + this.drawer_ = null; + + /** + * The type of the marker. + * @type {string} + */ + this.type = 'marker'; + } /** - * The current location of the marker. - * @type {ASTNode} - * @private + * Sets the object in charge of drawing the marker. + * @param {MarkerSvg} drawer The object in charge of + * drawing the marker. */ - this.curNode_ = null; + setDrawer(drawer) { + this.drawer_ = drawer; + } /** - * The object in charge of drawing the visual representation of the current - * node. - * @type {MarkerSvg} - * @private + * Get the current drawer for the marker. + * @return {MarkerSvg} The object in charge of drawing + * the marker. */ - this.drawer_ = null; + getDrawer() { + return this.drawer_; + } /** - * The type of the marker. - * @type {string} + * Gets the current location of the marker. + * @return {ASTNode} The current field, connection, or block the marker + * is on. */ - this.type = 'marker'; -}; - -/** - * Sets the object in charge of drawing the marker. - * @param {MarkerSvg} drawer The object in charge of - * drawing the marker. - */ -Marker.prototype.setDrawer = function(drawer) { - this.drawer_ = drawer; -}; - -/** - * Get the current drawer for the marker. - * @return {MarkerSvg} The object in charge of drawing - * the marker. - */ -Marker.prototype.getDrawer = function() { - return this.drawer_; -}; - -/** - * Gets the current location of the marker. - * @return {ASTNode} The current field, connection, or block the marker - * is on. - */ -Marker.prototype.getCurNode = function() { - return this.curNode_; -}; + getCurNode() { + return this.curNode_; + } -/** - * Set the location of the marker 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 {ASTNode} newNode The new location of the marker. - */ -Marker.prototype.setCurNode = function(newNode) { - const oldNode = this.curNode_; - this.curNode_ = newNode; - if (this.drawer_) { - this.drawer_.draw(oldNode, this.curNode_); + /** + * Set the location of the marker 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 {ASTNode} newNode The new location of the marker. + */ + setCurNode(newNode) { + const oldNode = this.curNode_; + this.curNode_ = newNode; + if (this.drawer_) { + this.drawer_.draw(oldNode, this.curNode_); + } } -}; -/** - * Redraw the current marker. - * @package - */ -Marker.prototype.draw = function() { - if (this.drawer_) { - this.drawer_.draw(this.curNode_, this.curNode_); + /** + * Redraw the current marker. + * @package + */ + draw() { + if (this.drawer_) { + this.drawer_.draw(this.curNode_, this.curNode_); + } } -}; -/** - * Hide the marker SVG. - */ -Marker.prototype.hide = function() { - if (this.drawer_) { - this.drawer_.hide(); + /** + * Hide the marker SVG. + */ + hide() { + if (this.drawer_) { + this.drawer_.hide(); + } } -}; -/** - * Dispose of this marker. - */ -Marker.prototype.dispose = function() { - if (this.getDrawer()) { - this.getDrawer().dispose(); + /** + * Dispose of this marker. + */ + dispose() { + if (this.getDrawer()) { + this.getDrawer().dispose(); + } } -}; +} exports.Marker = Marker; diff --git a/core/keyboard_nav/tab_navigate_cursor.js b/core/keyboard_nav/tab_navigate_cursor.js index 88aaeb357f3..5124201caca 100644 --- a/core/keyboard_nav/tab_navigate_cursor.js +++ b/core/keyboard_nav/tab_navigate_cursor.js @@ -17,7 +17,6 @@ */ goog.module('Blockly.TabNavigateCursor'); -const object = goog.require('Blockly.utils.object'); const {ASTNode} = goog.require('Blockly.ASTNode'); const {BasicCursor} = goog.require('Blockly.BasicCursor'); /* eslint-disable-next-line no-unused-vars */ @@ -26,32 +25,34 @@ const {Field} = goog.requireType('Blockly.Field'); /** * A cursor for navigating between tab navigable fields. - * @constructor * @extends {BasicCursor} - * @alias Blockly.TabNavigateCursor */ -const TabNavigateCursor = function() { - TabNavigateCursor.superClass_.constructor.call(this); -}; -object.inherits(TabNavigateCursor, BasicCursor); +class TabNavigateCursor extends BasicCursor { + /** + * @alias Blockly.TabNavigateCursor + */ + constructor() { + super(); + } -/** - * Skip all nodes except for tab navigable fields. - * @param {?ASTNode} node The AST node to check whether it is valid. - * @return {boolean} True if the node should be visited, false otherwise. - * @override - */ -TabNavigateCursor.prototype.validNode_ = function(node) { - let isValid = false; - const type = node && node.getType(); - if (node) { - const location = /** @type {Field} */ (node.getLocation()); - if (type === ASTNode.types.FIELD && location && location.isTabNavigable() && - location.isClickable()) { - isValid = true; + /** + * Skip all nodes except for tab navigable fields. + * @param {?ASTNode} node The AST node to check whether it is valid. + * @return {boolean} True if the node should be visited, false otherwise. + * @override + */ + validNode_(node) { + let isValid = false; + const type = node && node.getType(); + if (node) { + const location = /** @type {Field} */ (node.getLocation()); + if (type === ASTNode.types.FIELD && location && + location.isTabNavigable() && location.isClickable()) { + isValid = true; + } } + return isValid; } - return isValid; -}; +} exports.TabNavigateCursor = TabNavigateCursor; diff --git a/core/toolbox/toolbox.js b/core/toolbox/toolbox.js index a31174d4af5..03bcbec71e3 100644 --- a/core/toolbox/toolbox.js +++ b/core/toolbox/toolbox.js @@ -22,7 +22,6 @@ const browserEvents = goog.require('Blockly.browserEvents'); const common = goog.require('Blockly.common'); const dom = goog.require('Blockly.utils.dom'); const eventUtils = goog.require('Blockly.Events.utils'); -const object = goog.require('Blockly.utils.object'); const registry = goog.require('Blockly.registry'); const toolbox = goog.require('Blockly.utils.toolbox'); const {BlockSvg} = goog.require('Blockly.BlockSvg'); @@ -63,1054 +62,1060 @@ goog.require('Blockly.Events.ToolboxItemSelect'); /** * Class for a Toolbox. * Creates the toolbox's DOM. - * @param {!WorkspaceSvg} workspace The workspace in which to create new - * blocks. - * @constructor * @implements {IAutoHideable} * @implements {IKeyboardAccessible} * @implements {IStyleable} * @implements {IToolbox} * @extends {DeleteArea} - * @alias Blockly.Toolbox */ -const Toolbox = function(workspace) { - Toolbox.superClass_.constructor.call(this); +class Toolbox extends DeleteArea { /** - * The workspace this toolbox is on. - * @type {!WorkspaceSvg} - * @protected + * @param {!WorkspaceSvg} workspace The workspace in which to create new + * blocks. + * @alias Blockly.Toolbox */ - this.workspace_ = workspace; + constructor(workspace) { + super(); + + /** + * The workspace this toolbox is on. + * @type {!WorkspaceSvg} + * @protected + */ + this.workspace_ = workspace; + + /** + * The unique id for this component that is used to register with the + * ComponentManager. + * @type {string} + */ + this.id = 'toolbox'; + + /** + * The JSON describing the contents of this toolbox. + * @type {!toolbox.ToolboxInfo} + * @protected + */ + this.toolboxDef_ = workspace.options.languageTree || {'contents': []}; + + /** + * Whether the toolbox should be laid out horizontally. + * @type {boolean} + * @private + */ + this.horizontalLayout_ = workspace.options.horizontalLayout; + + /** + * The html container for the toolbox. + * @type {?Element} + */ + this.HtmlDiv = null; + + /** + * The html container for the contents of a toolbox. + * @type {?Element} + * @protected + */ + this.contentsDiv_ = null; + + /** + * Whether the Toolbox is visible. + * @type {boolean} + * @protected + */ + this.isVisible_ = false; + + /** + * The list of items in the toolbox. + * @type {!Array} + * @protected + */ + this.contents_ = []; + + /** + * The width of the toolbox. + * @type {number} + * @protected + */ + this.width_ = 0; + + /** + * The height of the toolbox. + * @type {number} + * @protected + */ + this.height_ = 0; + + /** + * Is RTL vs LTR. + * @type {boolean} + */ + this.RTL = workspace.options.RTL; + + /** + * The flyout for the toolbox. + * @type {?IFlyout} + * @private + */ + this.flyout_ = null; + + /** + * A map from toolbox item IDs to toolbox items. + * @type {!Object} + * @protected + */ + this.contentMap_ = Object.create(null); + + /** + * Position of the toolbox and flyout relative to the workspace. + * @type {!toolbox.Position} + */ + this.toolboxPosition = workspace.options.toolboxPosition; + + /** + * The currently selected item. + * @type {?ISelectableToolboxItem} + * @protected + */ + this.selectedItem_ = null; + + /** + * The previously selected item. + * @type {?ISelectableToolboxItem} + * @protected + */ + this.previouslySelectedItem_ = null; + + /** + * Array holding info needed to unbind event handlers. + * Used for disposing. + * Ex: [[node, name, func], [node, name, func]]. + * @type {!Array} + * @protected + */ + this.boundEvents_ = []; + } /** - * The unique id for this component that is used to register with the - * ComponentManager. - * @type {string} + * Handles the given keyboard shortcut. + * @param {!ShortcutRegistry.KeyboardShortcut} _shortcut The shortcut to be + * handled. + * @return {boolean} True if the shortcut has been handled, false otherwise. + * @public */ - this.id = 'toolbox'; + onShortcut(_shortcut) { + return false; + } /** - * The JSON describing the contents of this toolbox. - * @type {!toolbox.ToolboxInfo} - * @protected + * Initializes the toolbox + * @public */ - this.toolboxDef_ = workspace.options.languageTree || {'contents': []}; + init() { + const workspace = this.workspace_; + const svg = workspace.getParentSvg(); + + this.flyout_ = this.createFlyout_(); + + this.HtmlDiv = this.createDom_(this.workspace_); + dom.insertAfter(this.flyout_.createDom('svg'), svg); + this.setVisible(true); + this.flyout_.init(workspace); + + this.render(this.toolboxDef_); + const themeManager = workspace.getThemeManager(); + themeManager.subscribe( + this.HtmlDiv, 'toolboxBackgroundColour', 'background-color'); + themeManager.subscribe(this.HtmlDiv, 'toolboxForegroundColour', 'color'); + this.workspace_.getComponentManager().addComponent({ + component: this, + weight: 1, + capabilities: [ + ComponentManager.Capability.AUTOHIDEABLE, + ComponentManager.Capability.DELETE_AREA, + ComponentManager.Capability.DRAG_TARGET, + ], + }); + } /** - * Whether the toolbox should be laid out horizontally. - * @type {boolean} - * @private + * Creates the DOM for the toolbox. + * @param {!WorkspaceSvg} workspace The workspace this toolbox is on. + * @return {!Element} The HTML container for the toolbox. + * @protected */ - this.horizontalLayout_ = workspace.options.horizontalLayout; + createDom_(workspace) { + const svg = workspace.getParentSvg(); - /** - * The html container for the toolbox. - * @type {?Element} - */ - this.HtmlDiv = null; + const container = this.createContainer_(); + + this.contentsDiv_ = this.createContentsContainer_(); + this.contentsDiv_.tabIndex = 0; + aria.setRole(this.contentsDiv_, aria.Role.TREE); + container.appendChild(this.contentsDiv_); + + svg.parentNode.insertBefore(container, svg); + + this.attachEvents_(container, this.contentsDiv_); + return container; + } /** - * The html container for the contents of a toolbox. - * @type {?Element} + * Creates the container div for the toolbox. + * @return {!Element} The HTML container for the toolbox. * @protected */ - this.contentsDiv_ = null; + createContainer_() { + const toolboxContainer = document.createElement('div'); + toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); + dom.addClass(toolboxContainer, 'blocklyToolboxDiv'); + dom.addClass(toolboxContainer, 'blocklyNonSelectable'); + toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); + return toolboxContainer; + } /** - * Whether the Toolbox is visible. - * @type {boolean} + * Creates the container for all the contents in the toolbox. + * @return {!Element} The HTML container for the toolbox contents. * @protected */ - this.isVisible_ = false; + createContentsContainer_() { + const contentsContainer = document.createElement('div'); + dom.addClass(contentsContainer, 'blocklyToolboxContents'); + if (this.isHorizontal()) { + contentsContainer.style.flexDirection = 'row'; + } + return contentsContainer; + } /** - * The list of items in the toolbox. - * @type {!Array} + * Adds event listeners to the toolbox container div. + * @param {!Element} container The HTML container for the toolbox. + * @param {!Element} contentsContainer The HTML container for the contents + * of the toolbox. * @protected */ - this.contents_ = []; + attachEvents_(container, contentsContainer) { + // Clicking on toolbox closes popups. + const clickEvent = browserEvents.conditionalBind( + container, 'click', this, this.onClick_, + /* opt_noCaptureIdentifier */ false, + /* opt_noPreventDefault */ true); + this.boundEvents_.push(clickEvent); + + const keyDownEvent = browserEvents.conditionalBind( + contentsContainer, 'keydown', this, this.onKeyDown_, + /* opt_noCaptureIdentifier */ false, + /* opt_noPreventDefault */ true); + this.boundEvents_.push(keyDownEvent); + } /** - * The width of the toolbox. - * @type {number} + * Handles on click events for when the toolbox or toolbox items are clicked. + * @param {!Event} e Click event to handle. * @protected */ - this.width_ = 0; + onClick_(e) { + if (browserEvents.isRightButton(e) || e.target === this.HtmlDiv) { + // Close flyout. + common.getMainWorkspace().hideChaff(false); + } else { + const targetElement = e.target; + const itemId = targetElement.getAttribute('id'); + if (itemId) { + const item = this.getToolboxItemById(itemId); + if (item.isSelectable()) { + this.setSelectedItem(item); + item.onClick(e); + } + } + // Just close popups. + common.getMainWorkspace().hideChaff(true); + } + Touch.clearTouchIdentifier(); // Don't block future drags. + } /** - * The height of the toolbox. - * @type {number} + * Handles key down events for the toolbox. + * @param {!KeyboardEvent} e The key down event. * @protected */ - this.height_ = 0; + onKeyDown_(e) { + let handled = false; + switch (e.keyCode) { + case KeyCodes.DOWN: + handled = this.selectNext_(); + break; + case KeyCodes.UP: + handled = this.selectPrevious_(); + break; + case KeyCodes.LEFT: + handled = this.selectParent_(); + break; + case KeyCodes.RIGHT: + handled = this.selectChild_(); + break; + case KeyCodes.ENTER: + case KeyCodes.SPACE: + if (this.selectedItem_ && this.selectedItem_.isCollapsible()) { + const collapsibleItem = + /** @type {!ICollapsibleToolboxItem} */ (this.selectedItem_); + collapsibleItem.toggleExpanded(); + handled = true; + } + break; + default: + handled = false; + break; + } + if (!handled && this.selectedItem_ && this.selectedItem_.onKeyDown) { + handled = this.selectedItem_.onKeyDown(e); + } + + if (handled) { + e.preventDefault(); + } + } /** - * Is RTL vs LTR. - * @type {boolean} + * Creates the flyout based on the toolbox layout. + * @return {!IFlyout} The flyout for the toolbox. + * @throws {Error} If missing a require for `Blockly.HorizontalFlyout`, + * `Blockly.VerticalFlyout`, and no flyout plugin is specified. + * @protected */ - this.RTL = workspace.options.RTL; + createFlyout_() { + const workspace = this.workspace_; + // TODO (#4247): Look into adding a makeFlyout method to Blockly Options. + const workspaceOptions = new Options( + /** @type {!BlocklyOptions} */ + ({ + 'parentWorkspace': workspace, + 'rtl': workspace.RTL, + 'oneBasedIndex': workspace.options.oneBasedIndex, + 'horizontalLayout': workspace.horizontalLayout, + 'renderer': workspace.options.renderer, + 'rendererOverrides': workspace.options.rendererOverrides, + 'move': { + 'scrollbars': true, + }, + })); + // Options takes in either 'end' or 'start'. This has already been parsed to + // be either 0 or 1, so set it after. + workspaceOptions.toolboxPosition = workspace.options.toolboxPosition; + let FlyoutClass = null; + if (workspace.horizontalLayout) { + FlyoutClass = registry.getClassFromOptions( + registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, workspace.options, true); + } else { + FlyoutClass = registry.getClassFromOptions( + registry.Type.FLYOUTS_VERTICAL_TOOLBOX, workspace.options, true); + } + return new FlyoutClass(workspaceOptions); + } /** - * The flyout for the toolbox. - * @type {?IFlyout} - * @private + * Fills the toolbox with new toolbox items and removes any old contents. + * @param {!toolbox.ToolboxInfo} toolboxDef Object holding information + * for creating a toolbox. + * @package */ - this.flyout_ = null; + render(toolboxDef) { + this.toolboxDef_ = toolboxDef; + for (let i = 0; i < this.contents_.length; i++) { + const toolboxItem = this.contents_[i]; + if (toolboxItem) { + toolboxItem.dispose(); + } + } + this.contents_ = []; + this.contentMap_ = Object.create(null); + this.renderContents_(toolboxDef['contents']); + this.position(); + this.handleToolboxItemResize(); + } /** - * A map from toolbox item IDs to toolbox items. - * @type {!Object} + * Adds all the toolbox items to the toolbox. + * @param {!Array} toolboxDef Array + * holding objects containing information on the contents of the toolbox. * @protected */ - this.contentMap_ = Object.create(null); + renderContents_(toolboxDef) { + // This is for performance reasons. By using document fragment we only have + // to add to the DOM once. + const fragment = document.createDocumentFragment(); + for (let i = 0; i < toolboxDef.length; i++) { + const toolboxItemDef = toolboxDef[i]; + this.createToolboxItem_(toolboxItemDef, fragment); + } + this.contentsDiv_.appendChild(fragment); + } /** - * Position of the toolbox and flyout relative to the workspace. - * @type {!toolbox.Position} + * Creates and renders the toolbox item. + * @param {!toolbox.ToolboxItemInfo} toolboxItemDef Any information + * that can be used to create an item in the toolbox. + * @param {!DocumentFragment} fragment The document fragment to add the child + * toolbox elements to. + * @private */ - this.toolboxPosition = workspace.options.toolboxPosition; + createToolboxItem_(toolboxItemDef, fragment) { + let registryName = toolboxItemDef['kind']; + + // Categories that are collapsible are created using a class registered + // under a different name. + if (registryName.toUpperCase() === 'CATEGORY' && + toolbox.isCategoryCollapsible( + /** @type {!toolbox.CategoryInfo} */ (toolboxItemDef))) { + registryName = CollapsibleToolboxCategory.registrationName; + } + + const ToolboxItemClass = registry.getClass( + registry.Type.TOOLBOX_ITEM, registryName.toLowerCase()); + if (ToolboxItemClass) { + const toolboxItem = new ToolboxItemClass(toolboxItemDef, this); + this.addToolboxItem_(toolboxItem); + toolboxItem.init(); + const toolboxItemDom = toolboxItem.getDiv(); + if (toolboxItemDom) { + fragment.appendChild(toolboxItemDom); + } + // Adds the ID to the HTML element that can receive a click. + // This is used in onClick_ to find the toolboxItem that was clicked. + if (toolboxItem.getClickTarget) { + toolboxItem.getClickTarget().setAttribute('id', toolboxItem.getId()); + } + } + } /** - * The currently selected item. - * @type {?ISelectableToolboxItem} + * Adds an item to the toolbox. + * @param {!IToolboxItem} toolboxItem The item in the toolbox. * @protected */ - this.selectedItem_ = null; + addToolboxItem_(toolboxItem) { + this.contents_.push(toolboxItem); + this.contentMap_[toolboxItem.getId()] = toolboxItem; + if (toolboxItem.isCollapsible()) { + const collapsibleItem = /** @type {ICollapsibleToolboxItem} */ + (toolboxItem); + const childToolboxItems = collapsibleItem.getChildToolboxItems(); + for (let i = 0; i < childToolboxItems.length; i++) { + const child = childToolboxItems[i]; + this.addToolboxItem_(child); + } + } + } /** - * The previously selected item. - * @type {?ISelectableToolboxItem} - * @protected + * Gets the items in the toolbox. + * @return {!Array} The list of items in the toolbox. + * @public */ - this.previouslySelectedItem_ = null; + getToolboxItems() { + return this.contents_; + } /** - * Array holding info needed to unbind event handlers. - * Used for disposing. - * Ex: [[node, name, func], [node, name, func]]. - * @type {!Array} - * @protected + * Adds a style on the toolbox. Usually used to change the cursor. + * @param {string} style The name of the class to add. + * @package */ - this.boundEvents_ = []; -}; -object.inherits(Toolbox, DeleteArea); - -/** - * Handles the given keyboard shortcut. - * @param {!ShortcutRegistry.KeyboardShortcut} _shortcut The shortcut to be - * handled. - * @return {boolean} True if the shortcut has been handled, false otherwise. - * @public - */ -Toolbox.prototype.onShortcut = function(_shortcut) { - return false; -}; - -/** - * Initializes the toolbox - * @public - */ -Toolbox.prototype.init = function() { - const workspace = this.workspace_; - const svg = workspace.getParentSvg(); - - this.flyout_ = this.createFlyout_(); - - this.HtmlDiv = this.createDom_(this.workspace_); - dom.insertAfter(this.flyout_.createDom('svg'), svg); - this.setVisible(true); - this.flyout_.init(workspace); - - this.render(this.toolboxDef_); - const themeManager = workspace.getThemeManager(); - themeManager.subscribe( - this.HtmlDiv, 'toolboxBackgroundColour', 'background-color'); - themeManager.subscribe(this.HtmlDiv, 'toolboxForegroundColour', 'color'); - this.workspace_.getComponentManager().addComponent({ - component: this, - weight: 1, - capabilities: [ - ComponentManager.Capability.AUTOHIDEABLE, - ComponentManager.Capability.DELETE_AREA, - ComponentManager.Capability.DRAG_TARGET, - ], - }); -}; - -/** - * Creates the DOM for the toolbox. - * @param {!WorkspaceSvg} workspace The workspace this toolbox is on. - * @return {!Element} The HTML container for the toolbox. - * @protected - */ -Toolbox.prototype.createDom_ = function(workspace) { - const svg = workspace.getParentSvg(); - - const container = this.createContainer_(); + addStyle(style) { + dom.addClass(/** @type {!Element} */ (this.HtmlDiv), style); + } - this.contentsDiv_ = this.createContentsContainer_(); - this.contentsDiv_.tabIndex = 0; - aria.setRole(this.contentsDiv_, aria.Role.TREE); - container.appendChild(this.contentsDiv_); + /** + * Removes a style from the toolbox. Usually used to change the cursor. + * @param {string} style The name of the class to remove. + * @package + */ + removeStyle(style) { + dom.removeClass(/** @type {!Element} */ (this.HtmlDiv), style); + } - svg.parentNode.insertBefore(container, svg); + /** + * Returns the bounding rectangle of the drag target area in pixel units + * relative to viewport. + * @return {?Rect} The component's bounding box. Null if drag + * target area should be ignored. + */ + getClientRect() { + if (!this.HtmlDiv || !this.isVisible_) { + return null; + } - this.attachEvents_(container, this.contentsDiv_); - return container; -}; + // BIG_NUM is offscreen padding so that blocks dragged beyond the toolbox + // area are still deleted. Must be smaller than Infinity, but larger than + // the largest screen size. + const BIG_NUM = 10000000; + const toolboxRect = this.HtmlDiv.getBoundingClientRect(); -/** - * Creates the container div for the toolbox. - * @return {!Element} The HTML container for the toolbox. - * @protected - */ -Toolbox.prototype.createContainer_ = function() { - const toolboxContainer = document.createElement('div'); - toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); - dom.addClass(toolboxContainer, 'blocklyToolboxDiv'); - dom.addClass(toolboxContainer, 'blocklyNonSelectable'); - toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); - return toolboxContainer; -}; + const top = toolboxRect.top; + const bottom = top + toolboxRect.height; + const left = toolboxRect.left; + const right = left + toolboxRect.width; -/** - * Creates the container for all the contents in the toolbox. - * @return {!Element} The HTML container for the toolbox contents. - * @protected - */ -Toolbox.prototype.createContentsContainer_ = function() { - const contentsContainer = document.createElement('div'); - dom.addClass(contentsContainer, 'blocklyToolboxContents'); - if (this.isHorizontal()) { - contentsContainer.style.flexDirection = 'row'; + // Assumes that the toolbox is on the SVG edge. If this changes + // (e.g. toolboxes in mutators) then this code will need to be more complex. + if (this.toolboxPosition === toolbox.Position.TOP) { + return new Rect(-BIG_NUM, bottom, -BIG_NUM, BIG_NUM); + } else if (this.toolboxPosition === toolbox.Position.BOTTOM) { + return new Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM); + } else if (this.toolboxPosition === toolbox.Position.LEFT) { + return new Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, right); + } else { // Right + return new Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM); + } } - return contentsContainer; -}; -/** - * Adds event listeners to the toolbox container div. - * @param {!Element} container The HTML container for the toolbox. - * @param {!Element} contentsContainer The HTML container for the contents - * of the toolbox. - * @protected - */ -Toolbox.prototype.attachEvents_ = function(container, contentsContainer) { - // Clicking on toolbox closes popups. - const clickEvent = browserEvents.conditionalBind( - container, 'click', this, this.onClick_, - /* opt_noCaptureIdentifier */ false, - /* opt_noPreventDefault */ true); - this.boundEvents_.push(clickEvent); - - const keyDownEvent = browserEvents.conditionalBind( - contentsContainer, 'keydown', this, this.onKeyDown_, - /* opt_noCaptureIdentifier */ false, - /* opt_noPreventDefault */ true); - this.boundEvents_.push(keyDownEvent); -}; - -/** - * Handles on click events for when the toolbox or toolbox items are clicked. - * @param {!Event} e Click event to handle. - * @protected - */ -Toolbox.prototype.onClick_ = function(e) { - if (browserEvents.isRightButton(e) || e.target === this.HtmlDiv) { - // Close flyout. - common.getMainWorkspace().hideChaff(false); - } else { - const targetElement = e.target; - const itemId = targetElement.getAttribute('id'); - if (itemId) { - const item = this.getToolboxItemById(itemId); - if (item.isSelectable()) { - this.setSelectedItem(item); - item.onClick(e); - } + /** + * Returns whether the provided block or bubble would be deleted if dropped on + * this area. + * This method should check if the element is deletable and is always called + * before onDragEnter/onDragOver/onDragExit. + * @param {!IDraggable} element The block or bubble currently being + * dragged. + * @param {boolean} _couldConnect Whether the element could could connect to + * another. + * @return {boolean} Whether the element provided would be deleted if dropped + * on this area. + * @override + */ + wouldDelete(element, _couldConnect) { + if (element instanceof BlockSvg) { + const block = /** @type {BlockSvg} */ (element); + // Prefer dragging to the toolbox over connecting to other blocks. + this.updateWouldDelete_(!block.getParent() && block.isDeletable()); + } else { + this.updateWouldDelete_(element.isDeletable()); } - // Just close popups. - common.getMainWorkspace().hideChaff(true); + return this.wouldDelete_; } - Touch.clearTouchIdentifier(); // Don't block future drags. -}; -/** - * Handles key down events for the toolbox. - * @param {!KeyboardEvent} e The key down event. - * @protected - */ -Toolbox.prototype.onKeyDown_ = function(e) { - let handled = false; - switch (e.keyCode) { - case KeyCodes.DOWN: - handled = this.selectNext_(); - break; - case KeyCodes.UP: - handled = this.selectPrevious_(); - break; - case KeyCodes.LEFT: - handled = this.selectParent_(); - break; - case KeyCodes.RIGHT: - handled = this.selectChild_(); - break; - case KeyCodes.ENTER: - case KeyCodes.SPACE: - if (this.selectedItem_ && this.selectedItem_.isCollapsible()) { - const collapsibleItem = - /** @type {!ICollapsibleToolboxItem} */ (this.selectedItem_); - collapsibleItem.toggleExpanded(); - handled = true; - } - break; - default: - handled = false; - break; - } - if (!handled && this.selectedItem_ && this.selectedItem_.onKeyDown) { - handled = this.selectedItem_.onKeyDown(e); + /** + * Handles when a cursor with a block or bubble enters this drag target. + * @param {!IDraggable} _dragElement The block or bubble currently being + * dragged. + * @override + */ + onDragEnter(_dragElement) { + this.updateCursorDeleteStyle_(true); } - if (handled) { - e.preventDefault(); + /** + * Handles when a cursor with a block or bubble exits this drag target. + * @param {!IDraggable} _dragElement The block or bubble currently being + * dragged. + * @override + */ + onDragExit(_dragElement) { + this.updateCursorDeleteStyle_(false); } -}; -/** - * Creates the flyout based on the toolbox layout. - * @return {!IFlyout} The flyout for the toolbox. - * @throws {Error} If missing a require for `Blockly.HorizontalFlyout`, - * `Blockly.VerticalFlyout`, and no flyout plugin is specified. - * @protected - */ -Toolbox.prototype.createFlyout_ = function() { - const workspace = this.workspace_; - // TODO (#4247): Look into adding a makeFlyout method to Blockly Options. - const workspaceOptions = new Options( - /** @type {!BlocklyOptions} */ - ({ - 'parentWorkspace': workspace, - 'rtl': workspace.RTL, - 'oneBasedIndex': workspace.options.oneBasedIndex, - 'horizontalLayout': workspace.horizontalLayout, - 'renderer': workspace.options.renderer, - 'rendererOverrides': workspace.options.rendererOverrides, - 'move': { - 'scrollbars': true, - }, - })); - // Options takes in either 'end' or 'start'. This has already been parsed to - // be either 0 or 1, so set it after. - workspaceOptions.toolboxPosition = workspace.options.toolboxPosition; - let FlyoutClass = null; - if (workspace.horizontalLayout) { - FlyoutClass = registry.getClassFromOptions( - registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, workspace.options, true); - } else { - FlyoutClass = registry.getClassFromOptions( - registry.Type.FLYOUTS_VERTICAL_TOOLBOX, workspace.options, true); + /** + * Handles when a block or bubble is dropped on this component. + * Should not handle delete here. + * @param {!IDraggable} _dragElement The block or bubble currently being + * dragged. + * @override + */ + onDrop(_dragElement) { + this.updateCursorDeleteStyle_(false); } - return new FlyoutClass(workspaceOptions); -}; -/** - * Fills the toolbox with new toolbox items and removes any old contents. - * @param {!toolbox.ToolboxInfo} toolboxDef Object holding information - * for creating a toolbox. - * @package - */ -Toolbox.prototype.render = function(toolboxDef) { - this.toolboxDef_ = toolboxDef; - for (let i = 0; i < this.contents_.length; i++) { - const toolboxItem = this.contents_[i]; - if (toolboxItem) { - toolboxItem.dispose(); + /** + * Updates the internal wouldDelete_ state. + * @param {boolean} wouldDelete The new value for the wouldDelete state. + * @protected + * @override + */ + updateWouldDelete_(wouldDelete) { + if (wouldDelete === this.wouldDelete_) { + return; } + // This logic handles updating the deleteStyle properly if the delete state + // changes while the block is over the Toolbox. This could happen if the + // implementation of wouldDeleteBlock depends on the couldConnect parameter + // or if the isDeletable property of the block currently being dragged + // changes during the drag. + this.updateCursorDeleteStyle_(false); + this.wouldDelete_ = wouldDelete; + this.updateCursorDeleteStyle_(true); } - this.contents_ = []; - this.contentMap_ = Object.create(null); - this.renderContents_(toolboxDef['contents']); - this.position(); - this.handleToolboxItemResize(); -}; -/** - * Adds all the toolbox items to the toolbox. - * @param {!Array} toolboxDef Array - * holding objects containing information on the contents of the toolbox. - * @protected - */ -Toolbox.prototype.renderContents_ = function(toolboxDef) { - // This is for performance reasons. By using document fragment we only have to - // add to the DOM once. - const fragment = document.createDocumentFragment(); - for (let i = 0; i < toolboxDef.length; i++) { - const toolboxItemDef = toolboxDef[i]; - this.createToolboxItem_(toolboxItemDef, fragment); - } - this.contentsDiv_.appendChild(fragment); -}; - -/** - * Creates and renders the toolbox item. - * @param {!toolbox.ToolboxItemInfo} toolboxItemDef Any information - * that can be used to create an item in the toolbox. - * @param {!DocumentFragment} fragment The document fragment to add the child - * toolbox elements to. - * @private - */ -Toolbox.prototype.createToolboxItem_ = function(toolboxItemDef, fragment) { - let registryName = toolboxItemDef['kind']; - - // Categories that are collapsible are created using a class registered under - // a different name. - if (registryName.toUpperCase() === 'CATEGORY' && - toolbox.isCategoryCollapsible( - /** @type {!toolbox.CategoryInfo} */ (toolboxItemDef))) { - registryName = CollapsibleToolboxCategory.registrationName; - } - - const ToolboxItemClass = - registry.getClass(registry.Type.TOOLBOX_ITEM, registryName.toLowerCase()); - if (ToolboxItemClass) { - const toolboxItem = new ToolboxItemClass(toolboxItemDef, this); - this.addToolboxItem_(toolboxItem); - toolboxItem.init(); - const toolboxItemDom = toolboxItem.getDiv(); - if (toolboxItemDom) { - fragment.appendChild(toolboxItemDom); - } - // Adds the ID to the HTML element that can receive a click. - // This is used in onClick_ to find the toolboxItem that was clicked. - if (toolboxItem.getClickTarget) { - toolboxItem.getClickTarget().setAttribute('id', toolboxItem.getId()); + /** + * Adds or removes the CSS style of the cursor over the toolbox based whether + * the block or bubble over it is expected to be deleted if dropped (using the + * internal this.wouldDelete_ property). + * @param {boolean} addStyle Whether the style should be added or removed. + * @protected + */ + updateCursorDeleteStyle_(addStyle) { + const style = + this.wouldDelete_ ? 'blocklyToolboxDelete' : 'blocklyToolboxGrab'; + if (addStyle) { + this.addStyle(style); + } else { + this.removeStyle(style); } } -}; -/** - * Adds an item to the toolbox. - * @param {!IToolboxItem} toolboxItem The item in the toolbox. - * @protected - */ -Toolbox.prototype.addToolboxItem_ = function(toolboxItem) { - this.contents_.push(toolboxItem); - this.contentMap_[toolboxItem.getId()] = toolboxItem; - if (toolboxItem.isCollapsible()) { - const collapsibleItem = /** @type {ICollapsibleToolboxItem} */ - (toolboxItem); - const childToolboxItems = collapsibleItem.getChildToolboxItems(); - for (let i = 0; i < childToolboxItems.length; i++) { - const child = childToolboxItems[i]; - this.addToolboxItem_(child); - } + /** + * Gets the toolbox item with the given ID. + * @param {string} id The ID of the toolbox item. + * @return {?IToolboxItem} The toolbox item with the given ID, or null + * if no item exists. + * @public + */ + getToolboxItemById(id) { + return this.contentMap_[id] || null; } -}; -/** - * Gets the items in the toolbox. - * @return {!Array} The list of items in the toolbox. - * @public - */ -Toolbox.prototype.getToolboxItems = function() { - return this.contents_; -}; - -/** - * Adds a style on the toolbox. Usually used to change the cursor. - * @param {string} style The name of the class to add. - * @package - */ -Toolbox.prototype.addStyle = function(style) { - dom.addClass(/** @type {!Element} */ (this.HtmlDiv), style); -}; - -/** - * Removes a style from the toolbox. Usually used to change the cursor. - * @param {string} style The name of the class to remove. - * @package - */ -Toolbox.prototype.removeStyle = function(style) { - dom.removeClass(/** @type {!Element} */ (this.HtmlDiv), style); -}; - -/** - * Returns the bounding rectangle of the drag target area in pixel units - * relative to viewport. - * @return {?Rect} The component's bounding box. Null if drag - * target area should be ignored. - */ -Toolbox.prototype.getClientRect = function() { - if (!this.HtmlDiv || !this.isVisible_) { - return null; + /** + * Gets the width of the toolbox. + * @return {number} The width of the toolbox. + * @public + */ + getWidth() { + return this.width_; } - // BIG_NUM is offscreen padding so that blocks dragged beyond the toolbox - // area are still deleted. Must be smaller than Infinity, but larger than - // the largest screen size. - const BIG_NUM = 10000000; - const toolboxRect = this.HtmlDiv.getBoundingClientRect(); - - const top = toolboxRect.top; - const bottom = top + toolboxRect.height; - const left = toolboxRect.left; - const right = left + toolboxRect.width; - - // Assumes that the toolbox is on the SVG edge. If this changes - // (e.g. toolboxes in mutators) then this code will need to be more complex. - if (this.toolboxPosition === toolbox.Position.TOP) { - return new Rect(-BIG_NUM, bottom, -BIG_NUM, BIG_NUM); - } else if (this.toolboxPosition === toolbox.Position.BOTTOM) { - return new Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM); - } else if (this.toolboxPosition === toolbox.Position.LEFT) { - return new Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, right); - } else { // Right - return new Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM); + /** + * Gets the height of the toolbox. + * @return {number} The width of the toolbox. + * @public + */ + getHeight() { + return this.height_; } -}; -/** - * Returns whether the provided block or bubble would be deleted if dropped on - * this area. - * This method should check if the element is deletable and is always called - * before onDragEnter/onDragOver/onDragExit. - * @param {!IDraggable} element The block or bubble currently being - * dragged. - * @param {boolean} _couldConnect Whether the element could could connect to - * another. - * @return {boolean} Whether the element provided would be deleted if dropped on - * this area. - * @override - */ -Toolbox.prototype.wouldDelete = function(element, _couldConnect) { - if (element instanceof BlockSvg) { - const block = /** @type {BlockSvg} */ (element); - // Prefer dragging to the toolbox over connecting to other blocks. - this.updateWouldDelete_(!block.getParent() && block.isDeletable()); - } else { - this.updateWouldDelete_(element.isDeletable()); + /** + * Gets the toolbox flyout. + * @return {?IFlyout} The toolbox flyout. + * @public + */ + getFlyout() { + return this.flyout_; } - return this.wouldDelete_; -}; - -/** - * Handles when a cursor with a block or bubble enters this drag target. - * @param {!IDraggable} _dragElement The block or bubble currently being - * dragged. - * @override - */ -Toolbox.prototype.onDragEnter = function(_dragElement) { - this.updateCursorDeleteStyle_(true); -}; - -/** - * Handles when a cursor with a block or bubble exits this drag target. - * @param {!IDraggable} _dragElement The block or bubble currently being - * dragged. - * @override - */ -Toolbox.prototype.onDragExit = function(_dragElement) { - this.updateCursorDeleteStyle_(false); -}; - -/** - * Handles when a block or bubble is dropped on this component. - * Should not handle delete here. - * @param {!IDraggable} _dragElement The block or bubble currently being - * dragged. - * @override - */ -Toolbox.prototype.onDrop = function(_dragElement) { - this.updateCursorDeleteStyle_(false); -}; - -/** - * Updates the internal wouldDelete_ state. - * @param {boolean} wouldDelete The new value for the wouldDelete state. - * @protected - * @override - */ -Toolbox.prototype.updateWouldDelete_ = function(wouldDelete) { - if (wouldDelete === this.wouldDelete_) { - return; + /** + * Gets the workspace for the toolbox. + * @return {!WorkspaceSvg} The parent workspace for the toolbox. + * @public + */ + getWorkspace() { + return this.workspace_; } - // This logic handles updating the deleteStyle properly if the delete state - // changes while the block is over the Toolbox. This could happen if the - // implementation of wouldDeleteBlock depends on the couldConnect parameter - // or if the isDeletable property of the block currently being dragged - // changes during the drag. - this.updateCursorDeleteStyle_(false); - this.wouldDelete_ = wouldDelete; - this.updateCursorDeleteStyle_(true); -}; -/** - * Adds or removes the CSS style of the cursor over the toolbox based whether - * the block or bubble over it is expected to be deleted if dropped (using the - * internal this.wouldDelete_ property). - * @param {boolean} addStyle Whether the style should be added or removed. - * @protected - */ -Toolbox.prototype.updateCursorDeleteStyle_ = function(addStyle) { - const style = - this.wouldDelete_ ? 'blocklyToolboxDelete' : 'blocklyToolboxGrab'; - if (addStyle) { - this.addStyle(style); - } else { - this.removeStyle(style); + /** + * Gets the selected item. + * @return {?ISelectableToolboxItem} The selected item, or null if no item is + * currently selected. + * @public + */ + getSelectedItem() { + return this.selectedItem_; } -}; - -/** - * Gets the toolbox item with the given ID. - * @param {string} id The ID of the toolbox item. - * @return {?IToolboxItem} The toolbox item with the given ID, or null - * if no item exists. - * @public - */ -Toolbox.prototype.getToolboxItemById = function(id) { - return this.contentMap_[id] || null; -}; - -/** - * Gets the width of the toolbox. - * @return {number} The width of the toolbox. - * @public - */ -Toolbox.prototype.getWidth = function() { - return this.width_; -}; - -/** - * Gets the height of the toolbox. - * @return {number} The width of the toolbox. - * @public - */ -Toolbox.prototype.getHeight = function() { - return this.height_; -}; - -/** - * Gets the toolbox flyout. - * @return {?IFlyout} The toolbox flyout. - * @public - */ -Toolbox.prototype.getFlyout = function() { - return this.flyout_; -}; -/** - * Gets the workspace for the toolbox. - * @return {!WorkspaceSvg} The parent workspace for the toolbox. - * @public - */ -Toolbox.prototype.getWorkspace = function() { - return this.workspace_; -}; - -/** - * Gets the selected item. - * @return {?ISelectableToolboxItem} The selected item, or null if no item is - * currently selected. - * @public - */ -Toolbox.prototype.getSelectedItem = function() { - return this.selectedItem_; -}; - -/** - * Gets the previously selected item. - * @return {?ISelectableToolboxItem} The previously selected item, or null if no - * item was previously selected. - * @public - */ -Toolbox.prototype.getPreviouslySelectedItem = function() { - return this.previouslySelectedItem_; -}; - -/** - * Gets whether or not the toolbox is horizontal. - * @return {boolean} True if the toolbox is horizontal, false if the toolbox is - * vertical. - * @public - */ -Toolbox.prototype.isHorizontal = function() { - return this.horizontalLayout_; -}; + /** + * Gets the previously selected item. + * @return {?ISelectableToolboxItem} The previously selected item, or null if + * no item was previously selected. + * @public + */ + getPreviouslySelectedItem() { + return this.previouslySelectedItem_; + } -/** - * Positions the toolbox based on whether it is a horizontal toolbox and whether - * the workspace is in rtl. - * @public - */ -Toolbox.prototype.position = function() { - const workspaceMetrics = this.workspace_.getMetrics(); - const toolboxDiv = this.HtmlDiv; - if (!toolboxDiv) { - // Not initialized yet. - return; + /** + * Gets whether or not the toolbox is horizontal. + * @return {boolean} True if the toolbox is horizontal, false if the toolbox + * is vertical. + * @public + */ + isHorizontal() { + return this.horizontalLayout_; } - if (this.horizontalLayout_) { - toolboxDiv.style.left = '0'; - toolboxDiv.style.height = 'auto'; - toolboxDiv.style.width = '100%'; - this.height_ = toolboxDiv.offsetHeight; - this.width_ = workspaceMetrics.viewWidth; - if (this.toolboxPosition === toolbox.Position.TOP) { - toolboxDiv.style.top = '0'; - } else { // Bottom - toolboxDiv.style.bottom = '0'; + /** + * Positions the toolbox based on whether it is a horizontal toolbox and + * whether the workspace is in rtl. + * @public + */ + position() { + const workspaceMetrics = this.workspace_.getMetrics(); + const toolboxDiv = this.HtmlDiv; + if (!toolboxDiv) { + // Not initialized yet. + return; } - } else { - if (this.toolboxPosition === toolbox.Position.RIGHT) { - toolboxDiv.style.right = '0'; - } else { // Left + + if (this.horizontalLayout_) { toolboxDiv.style.left = '0'; + toolboxDiv.style.height = 'auto'; + toolboxDiv.style.width = '100%'; + this.height_ = toolboxDiv.offsetHeight; + this.width_ = workspaceMetrics.viewWidth; + if (this.toolboxPosition === toolbox.Position.TOP) { + toolboxDiv.style.top = '0'; + } else { // Bottom + toolboxDiv.style.bottom = '0'; + } + } else { + if (this.toolboxPosition === toolbox.Position.RIGHT) { + toolboxDiv.style.right = '0'; + } else { // Left + toolboxDiv.style.left = '0'; + } + toolboxDiv.style.height = '100%'; + this.width_ = toolboxDiv.offsetWidth; + this.height_ = workspaceMetrics.viewHeight; } - toolboxDiv.style.height = '100%'; - this.width_ = toolboxDiv.offsetWidth; - this.height_ = workspaceMetrics.viewHeight; + this.flyout_.position(); } - this.flyout_.position(); -}; -/** - * Handles resizing the toolbox when a toolbox item resizes. - * @package - */ -Toolbox.prototype.handleToolboxItemResize = function() { - // Reposition the workspace so that (0,0) is in the correct position relative - // to the new absolute edge (ie toolbox edge). - const workspace = this.workspace_; - const rect = this.HtmlDiv.getBoundingClientRect(); - const newX = this.toolboxPosition === toolbox.Position.LEFT ? - workspace.scrollX + rect.width : - workspace.scrollX; - const newY = this.toolboxPosition === toolbox.Position.TOP ? - workspace.scrollY + rect.height : - workspace.scrollY; - workspace.translate(newX, newY); - - // Even though the div hasn't changed size, the visible workspace - // surface of the workspace has, so we may need to reposition everything. - common.svgResize(workspace); -}; - -/** - * Unhighlights any previously selected item. - * @public - */ -Toolbox.prototype.clearSelection = function() { - this.setSelectedItem(null); -}; -/** - * Updates the category colours and background colour of selected categories. - * @package - */ -Toolbox.prototype.refreshTheme = function() { - for (let i = 0; i < this.contents_.length; i++) { - const child = this.contents_[i]; - if (child.refreshTheme) { - child.refreshTheme(); - } + /** + * Handles resizing the toolbox when a toolbox item resizes. + * @package + */ + handleToolboxItemResize() { + // Reposition the workspace so that (0,0) is in the correct position + // relative to the new absolute edge (ie toolbox edge). + const workspace = this.workspace_; + const rect = this.HtmlDiv.getBoundingClientRect(); + const newX = this.toolboxPosition === toolbox.Position.LEFT ? + workspace.scrollX + rect.width : + workspace.scrollX; + const newY = this.toolboxPosition === toolbox.Position.TOP ? + workspace.scrollY + rect.height : + workspace.scrollY; + workspace.translate(newX, newY); + + // Even though the div hasn't changed size, the visible workspace + // surface of the workspace has, so we may need to reposition everything. + common.svgResize(workspace); } -}; -/** - * Updates the flyout's content without closing it. Should be used in response - * to a change in one of the dynamic categories, such as variables or - * procedures. - * @public - */ -Toolbox.prototype.refreshSelection = function() { - if (this.selectedItem_ && this.selectedItem_.isSelectable() && - this.selectedItem_.getContents().length) { - this.flyout_.show(this.selectedItem_.getContents()); + /** + * Unhighlights any previously selected item. + * @public + */ + clearSelection() { + this.setSelectedItem(null); } -}; -/** - * Shows or hides the toolbox. - * @param {boolean} isVisible True if toolbox should be visible. - * @public - */ -Toolbox.prototype.setVisible = function(isVisible) { - if (this.isVisible_ === isVisible) { - return; + /** + * Updates the category colours and background colour of selected categories. + * @package + */ + refreshTheme() { + for (let i = 0; i < this.contents_.length; i++) { + const child = this.contents_[i]; + if (child.refreshTheme) { + child.refreshTheme(); + } + } } - this.HtmlDiv.style.display = isVisible ? 'block' : 'none'; - this.isVisible_ = isVisible; - // Invisible toolbox is ignored as drag targets and must have the drag target - // updated. - this.workspace_.recordDragTargets(); -}; - -/** - * Hides the component. Called in WorkspaceSvg.hideChaff. - * @param {boolean} onlyClosePopups Whether only popups should be closed. - * Flyouts should not be closed if this is true. - */ -Toolbox.prototype.autoHide = function(onlyClosePopups) { - if (!onlyClosePopups && this.flyout_ && this.flyout_.autoClose) { - this.clearSelection(); + /** + * Updates the flyout's content without closing it. Should be used in + * response to a change in one of the dynamic categories, such as variables or + * procedures. + * @public + */ + refreshSelection() { + if (this.selectedItem_ && this.selectedItem_.isSelectable() && + this.selectedItem_.getContents().length) { + this.flyout_.show(this.selectedItem_.getContents()); + } } -}; - -/** - * Sets the given item as selected. - * No-op if the item is not selectable. - * @param {?IToolboxItem} newItem The toolbox item to select. - * @public - */ -Toolbox.prototype.setSelectedItem = function(newItem) { - const oldItem = this.selectedItem_; - if ((!newItem && !oldItem) || (newItem && !newItem.isSelectable())) { - return; - } - newItem = /** @type {ISelectableToolboxItem} */ (newItem); + /** + * Shows or hides the toolbox. + * @param {boolean} isVisible True if toolbox should be visible. + * @public + */ + setVisible(isVisible) { + if (this.isVisible_ === isVisible) { + return; + } - if (this.shouldDeselectItem_(oldItem, newItem) && oldItem !== null) { - this.deselectItem_(oldItem); + this.HtmlDiv.style.display = isVisible ? 'block' : 'none'; + this.isVisible_ = isVisible; + // Invisible toolbox is ignored as drag targets and must have the drag + // target updated. + this.workspace_.recordDragTargets(); } - if (this.shouldSelectItem_(oldItem, newItem) && newItem !== null) { - this.selectItem_(oldItem, newItem); + /** + * Hides the component. Called in WorkspaceSvg.hideChaff. + * @param {boolean} onlyClosePopups Whether only popups should be closed. + * Flyouts should not be closed if this is true. + */ + autoHide(onlyClosePopups) { + if (!onlyClosePopups && this.flyout_ && this.flyout_.autoClose) { + this.clearSelection(); + } } - this.updateFlyout_(oldItem, newItem); - this.fireSelectEvent_(oldItem, newItem); -}; + /** + * Sets the given item as selected. + * No-op if the item is not selectable. + * @param {?IToolboxItem} newItem The toolbox item to select. + * @public + */ + setSelectedItem(newItem) { + const oldItem = this.selectedItem_; -/** - * Decides whether the old item should be deselected. - * @param {?ISelectableToolboxItem} oldItem The previously selected - * toolbox item. - * @param {?ISelectableToolboxItem} newItem The newly selected toolbox - * item. - * @return {boolean} True if the old item should be deselected, false otherwise. - * @protected - */ -Toolbox.prototype.shouldDeselectItem_ = function(oldItem, newItem) { - // Deselect the old item unless the old item is collapsible and has been - // previously clicked on. - return oldItem !== null && (!oldItem.isCollapsible() || oldItem !== newItem); -}; + if ((!newItem && !oldItem) || (newItem && !newItem.isSelectable())) { + return; + } + newItem = /** @type {ISelectableToolboxItem} */ (newItem); -/** - * Decides whether the new item should be selected. - * @param {?ISelectableToolboxItem} oldItem The previously selected - * toolbox item. - * @param {?ISelectableToolboxItem} newItem The newly selected toolbox - * item. - * @return {boolean} True if the new item should be selected, false otherwise. - * @protected - */ -Toolbox.prototype.shouldSelectItem_ = function(oldItem, newItem) { - // Select the new item unless the old item equals the new item. - return newItem !== null && newItem !== oldItem; -}; + if (this.shouldDeselectItem_(oldItem, newItem) && oldItem !== null) { + this.deselectItem_(oldItem); + } -/** - * Deselects the given item, marks it as unselected, and updates aria state. - * @param {!ISelectableToolboxItem} item The previously selected - * toolbox item which should be deselected. - * @protected - */ -Toolbox.prototype.deselectItem_ = function(item) { - this.selectedItem_ = null; - this.previouslySelectedItem_ = item; - item.setSelected(false); - aria.setState( - /** @type {!Element} */ (this.contentsDiv_), aria.State.ACTIVEDESCENDANT, - ''); -}; + if (this.shouldSelectItem_(oldItem, newItem) && newItem !== null) { + this.selectItem_(oldItem, newItem); + } -/** - * Selects the given item, marks it selected, and updates aria state. - * @param {?ISelectableToolboxItem} oldItem The previously selected - * toolbox item. - * @param {!ISelectableToolboxItem} newItem The newly selected toolbox - * item. - * @protected - */ -Toolbox.prototype.selectItem_ = function(oldItem, newItem) { - this.selectedItem_ = newItem; - this.previouslySelectedItem_ = oldItem; - newItem.setSelected(true); - aria.setState( - /** @type {!Element} */ (this.contentsDiv_), aria.State.ACTIVEDESCENDANT, - newItem.getId()); -}; + this.updateFlyout_(oldItem, newItem); + this.fireSelectEvent_(oldItem, newItem); + } -/** - * Selects the toolbox item by its position in the list of toolbox items. - * @param {number} position The position of the item to select. - * @public - */ -Toolbox.prototype.selectItemByPosition = function(position) { - if (position > -1 && position < this.contents_.length) { - const item = this.contents_[position]; - if (item.isSelectable()) { - this.setSelectedItem(item); - } + /** + * Decides whether the old item should be deselected. + * @param {?ISelectableToolboxItem} oldItem The previously selected + * toolbox item. + * @param {?ISelectableToolboxItem} newItem The newly selected toolbox + * item. + * @return {boolean} True if the old item should be deselected, false + * otherwise. + * @protected + */ + shouldDeselectItem_(oldItem, newItem) { + // Deselect the old item unless the old item is collapsible and has been + // previously clicked on. + return oldItem !== null && + (!oldItem.isCollapsible() || oldItem !== newItem); } -}; -/** - * Decides whether to hide or show the flyout depending on the selected item. - * @param {?ISelectableToolboxItem} oldItem The previously selected toolbox - * item. - * @param {?ISelectableToolboxItem} newItem The newly selected toolbox item. - * @protected - */ -Toolbox.prototype.updateFlyout_ = function(oldItem, newItem) { - if (!newItem || (oldItem === newItem && !newItem.isCollapsible()) || - !newItem.getContents().length) { - this.flyout_.hide(); - } else { - this.flyout_.show(newItem.getContents()); - this.flyout_.scrollToStart(); + /** + * Decides whether the new item should be selected. + * @param {?ISelectableToolboxItem} oldItem The previously selected + * toolbox item. + * @param {?ISelectableToolboxItem} newItem The newly selected toolbox + * item. + * @return {boolean} True if the new item should be selected, false otherwise. + * @protected + */ + shouldSelectItem_(oldItem, newItem) { + // Select the new item unless the old item equals the new item. + return newItem !== null && newItem !== oldItem; } -}; -/** - * Emits an event when a new toolbox item is selected. - * @param {?ISelectableToolboxItem} oldItem The previously selected - * toolbox item. - * @param {?ISelectableToolboxItem} newItem The newly selected toolbox - * item. - * @private - */ -Toolbox.prototype.fireSelectEvent_ = function(oldItem, newItem) { - const oldElement = oldItem && oldItem.getName(); - let newElement = newItem && newItem.getName(); - // In this case the toolbox closes, so the newElement should be null. - if (oldItem === newItem) { - newElement = null; + /** + * Deselects the given item, marks it as unselected, and updates aria state. + * @param {!ISelectableToolboxItem} item The previously selected + * toolbox item which should be deselected. + * @protected + */ + deselectItem_(item) { + this.selectedItem_ = null; + this.previouslySelectedItem_ = item; + item.setSelected(false); + aria.setState( + /** @type {!Element} */ (this.contentsDiv_), + aria.State.ACTIVEDESCENDANT, ''); } - const event = new (eventUtils.get(eventUtils.TOOLBOX_ITEM_SELECT))( - oldElement, newElement, this.workspace_.id); - eventUtils.fire(event); -}; -/** - * Closes the current item if it is expanded, or selects the parent. - * @return {boolean} True if a parent category was selected, false otherwise. - * @private - */ -Toolbox.prototype.selectParent_ = function() { - if (!this.selectedItem_) { - return false; + /** + * Selects the given item, marks it selected, and updates aria state. + * @param {?ISelectableToolboxItem} oldItem The previously selected + * toolbox item. + * @param {!ISelectableToolboxItem} newItem The newly selected toolbox + * item. + * @protected + */ + selectItem_(oldItem, newItem) { + this.selectedItem_ = newItem; + this.previouslySelectedItem_ = oldItem; + newItem.setSelected(true); + aria.setState( + /** @type {!Element} */ (this.contentsDiv_), + aria.State.ACTIVEDESCENDANT, newItem.getId()); } - if (this.selectedItem_.isCollapsible() && this.selectedItem_.isExpanded()) { - const collapsibleItem = - /** @type {!ICollapsibleToolboxItem} */ (this.selectedItem_); - collapsibleItem.setExpanded(false); - return true; - } else if ( - this.selectedItem_.getParent() && - this.selectedItem_.getParent().isSelectable()) { - this.setSelectedItem(this.selectedItem_.getParent()); - return true; + /** + * Selects the toolbox item by its position in the list of toolbox items. + * @param {number} position The position of the item to select. + * @public + */ + selectItemByPosition(position) { + if (position > -1 && position < this.contents_.length) { + const item = this.contents_[position]; + if (item.isSelectable()) { + this.setSelectedItem(item); + } + } } - return false; -}; -/** - * Selects the first child of the currently selected item, or nothing if the - * toolbox item has no children. - * @return {boolean} True if a child category was selected, false otherwise. - * @private - */ -Toolbox.prototype.selectChild_ = function() { - if (!this.selectedItem_ || !this.selectedItem_.isCollapsible()) { - return false; + /** + * Decides whether to hide or show the flyout depending on the selected item. + * @param {?ISelectableToolboxItem} oldItem The previously selected toolbox + * item. + * @param {?ISelectableToolboxItem} newItem The newly selected toolbox item. + * @protected + */ + updateFlyout_(oldItem, newItem) { + if (!newItem || (oldItem === newItem && !newItem.isCollapsible()) || + !newItem.getContents().length) { + this.flyout_.hide(); + } else { + this.flyout_.show(newItem.getContents()); + this.flyout_.scrollToStart(); + } } - const collapsibleItem = /** @type {ICollapsibleToolboxItem} */ - (this.selectedItem_); - if (!collapsibleItem.isExpanded()) { - collapsibleItem.setExpanded(true); - return true; - } else { - this.selectNext_(); - return true; + + /** + * Emits an event when a new toolbox item is selected. + * @param {?ISelectableToolboxItem} oldItem The previously selected + * toolbox item. + * @param {?ISelectableToolboxItem} newItem The newly selected toolbox + * item. + * @private + */ + fireSelectEvent_(oldItem, newItem) { + const oldElement = oldItem && oldItem.getName(); + let newElement = newItem && newItem.getName(); + // In this case the toolbox closes, so the newElement should be null. + if (oldItem === newItem) { + newElement = null; + } + const event = new (eventUtils.get(eventUtils.TOOLBOX_ITEM_SELECT))( + oldElement, newElement, this.workspace_.id); + eventUtils.fire(event); } -}; -/** - * Selects the next visible toolbox item. - * @return {boolean} True if a next category was selected, false otherwise. - * @private - */ -Toolbox.prototype.selectNext_ = function() { - if (!this.selectedItem_) { + /** + * Closes the current item if it is expanded, or selects the parent. + * @return {boolean} True if a parent category was selected, false otherwise. + * @private + */ + selectParent_() { + if (!this.selectedItem_) { + return false; + } + + if (this.selectedItem_.isCollapsible() && this.selectedItem_.isExpanded()) { + const collapsibleItem = + /** @type {!ICollapsibleToolboxItem} */ (this.selectedItem_); + collapsibleItem.setExpanded(false); + return true; + } else if ( + this.selectedItem_.getParent() && + this.selectedItem_.getParent().isSelectable()) { + this.setSelectedItem(this.selectedItem_.getParent()); + return true; + } return false; } - let nextItemIdx = this.contents_.indexOf(this.selectedItem_) + 1; - if (nextItemIdx > -1 && nextItemIdx < this.contents_.length) { - let nextItem = this.contents_[nextItemIdx]; - while (nextItem && !nextItem.isSelectable()) { - nextItem = this.contents_[++nextItemIdx]; + /** + * Selects the first child of the currently selected item, or nothing if the + * toolbox item has no children. + * @return {boolean} True if a child category was selected, false otherwise. + * @private + */ + selectChild_() { + if (!this.selectedItem_ || !this.selectedItem_.isCollapsible()) { + return false; } - if (nextItem && nextItem.isSelectable()) { - this.setSelectedItem(nextItem); + const collapsibleItem = /** @type {ICollapsibleToolboxItem} */ + (this.selectedItem_); + if (!collapsibleItem.isExpanded()) { + collapsibleItem.setExpanded(true); + return true; + } else { + this.selectNext_(); return true; } } - return false; -}; -/** - * Selects the previous visible toolbox item. - * @return {boolean} True if a previous category was selected, false otherwise. - * @private - */ -Toolbox.prototype.selectPrevious_ = function() { - if (!this.selectedItem_) { + /** + * Selects the next visible toolbox item. + * @return {boolean} True if a next category was selected, false otherwise. + * @private + */ + selectNext_() { + if (!this.selectedItem_) { + return false; + } + + let nextItemIdx = this.contents_.indexOf(this.selectedItem_) + 1; + if (nextItemIdx > -1 && nextItemIdx < this.contents_.length) { + let nextItem = this.contents_[nextItemIdx]; + while (nextItem && !nextItem.isSelectable()) { + nextItem = this.contents_[++nextItemIdx]; + } + if (nextItem && nextItem.isSelectable()) { + this.setSelectedItem(nextItem); + return true; + } + } return false; } - let prevItemIdx = this.contents_.indexOf(this.selectedItem_) - 1; - if (prevItemIdx > -1 && prevItemIdx < this.contents_.length) { - let prevItem = this.contents_[prevItemIdx]; - while (prevItem && !prevItem.isSelectable()) { - prevItem = this.contents_[--prevItemIdx]; + /** + * Selects the previous visible toolbox item. + * @return {boolean} True if a previous category was selected, false + * otherwise. + * @private + */ + selectPrevious_() { + if (!this.selectedItem_) { + return false; } - if (prevItem && prevItem.isSelectable()) { - this.setSelectedItem(prevItem); - return true; + + let prevItemIdx = this.contents_.indexOf(this.selectedItem_) - 1; + if (prevItemIdx > -1 && prevItemIdx < this.contents_.length) { + let prevItem = this.contents_[prevItemIdx]; + while (prevItem && !prevItem.isSelectable()) { + prevItem = this.contents_[--prevItemIdx]; + } + if (prevItem && prevItem.isSelectable()) { + this.setSelectedItem(prevItem); + return true; + } } + return false; } - return false; -}; -/** - * Disposes of this toolbox. - * @public - */ -Toolbox.prototype.dispose = function() { - this.workspace_.getComponentManager().removeComponent('toolbox'); - this.flyout_.dispose(); - for (let i = 0; i < this.contents_.length; i++) { - const toolboxItem = this.contents_[i]; - toolboxItem.dispose(); - } + /** + * Disposes of this toolbox. + * @public + */ + dispose() { + this.workspace_.getComponentManager().removeComponent('toolbox'); + this.flyout_.dispose(); + for (let i = 0; i < this.contents_.length; i++) { + const toolboxItem = this.contents_[i]; + toolboxItem.dispose(); + } - for (let j = 0; j < this.boundEvents_.length; j++) { - browserEvents.unbind(this.boundEvents_[j]); - } - this.boundEvents_ = []; - this.contents_ = []; + for (let j = 0; j < this.boundEvents_.length; j++) { + browserEvents.unbind(this.boundEvents_[j]); + } + this.boundEvents_ = []; + this.contents_ = []; - this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv); - dom.removeNode(this.HtmlDiv); -}; + this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv); + dom.removeNode(this.HtmlDiv); + } +} /** * CSS for Toolbox. See css.js for use. diff --git a/core/utils/svg.js b/core/utils/svg.js index 1e3338ba7d6..ca6edf6d6bc 100644 --- a/core/utils/svg.js +++ b/core/utils/svg.js @@ -20,28 +20,30 @@ goog.module('Blockly.utils.Svg'); /** * A name with the type of the SVG element stored in the generic. - * @param {string} tagName The SVG element tag name. - * @constructor * @template T - * @private - * @alias Blockly.utils.Svg */ -const Svg = function(tagName) { +class Svg { /** - * @type {string} - * @private + * @param {string} tagName The SVG element tag name. + * @package + * @alias Blockly.utils.Svg */ - this.tagName_ = tagName; -}; + constructor(tagName) { + /** + * @type {string} + * @private + */ + this.tagName_ = tagName; + } -/** - * Returns the SVG element tag name. - * @return {string} The name. - * @override - */ -Svg.prototype.toString = function() { - return this.tagName_; -}; + /** + * Returns the SVG element tag name. + * @return {string} The name. + */ + toString() { + return this.tagName_; + } +} /** * @type {!Svg} diff --git a/scripts/gulpfiles/chunks.json b/scripts/gulpfiles/chunks.json index 46b36a9c61d..be0e54d8c2c 100644 --- a/scripts/gulpfiles/chunks.json +++ b/scripts/gulpfiles/chunks.json @@ -81,8 +81,8 @@ "./core/warning.js", "./core/events/events_bubble_open.js", "./core/comment.js", - "./core/events/events_block_move.js", "./core/events/events_block_drag.js", + "./core/events/events_block_move.js", "./core/bump_objects.js", "./core/block_dragger.js", "./core/workspace_dragger.js", @@ -258,6 +258,7 @@ "./core/events/events_var_create.js", "./core/variable_model.js", "./core/variables.js", + "./core/utils/object.js", "./core/events/events_abstract.js", "./core/events/utils.js", "./core/xml.js", diff --git a/tests/deps.js b/tests/deps.js index 95cca7b5866..c8b90c28d26 100644 --- a/tests/deps.js +++ b/tests/deps.js @@ -33,21 +33,21 @@ goog.addDependency('../../core/contextmenu.js', ['Blockly.ContextMenu'], ['Block goog.addDependency('../../core/contextmenu_items.js', ['Blockly.ContextMenuItems'], ['Blockly.ContextMenuRegistry', 'Blockly.Events', 'Blockly.Events.utils', 'Blockly.Msg', 'Blockly.clipboard', 'Blockly.dialog', 'Blockly.inputTypes', 'Blockly.utils.idGenerator', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/contextmenu_registry.js', ['Blockly.ContextMenuRegistry'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/css.js', ['Blockly.Css'], ['Blockly.utils.deprecation'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/delete_area.js', ['Blockly.DeleteArea'], ['Blockly.BlockSvg', 'Blockly.DragTarget', 'Blockly.IDeleteArea', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/delete_area.js', ['Blockly.DeleteArea'], ['Blockly.BlockSvg', 'Blockly.DragTarget', 'Blockly.IDeleteArea'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/dialog.js', ['Blockly.dialog'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/drag_target.js', ['Blockly.DragTarget'], ['Blockly.IDragTarget'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/dropdowndiv.js', ['Blockly.DropDownDiv'], ['Blockly.common', 'Blockly.utils.Rect', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.style'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events.js', ['Blockly.Events'], ['Blockly.Events.Abstract', 'Blockly.Events.BlockBase', 'Blockly.Events.BlockChange', 'Blockly.Events.BlockCreate', 'Blockly.Events.BlockDelete', 'Blockly.Events.BlockDrag', 'Blockly.Events.BlockMove', 'Blockly.Events.BubbleOpen', 'Blockly.Events.Click', 'Blockly.Events.CommentBase', 'Blockly.Events.CommentChange', 'Blockly.Events.CommentCreate', 'Blockly.Events.CommentDelete', 'Blockly.Events.CommentMove', 'Blockly.Events.FinishedLoading', 'Blockly.Events.MarkerMove', 'Blockly.Events.Selected', 'Blockly.Events.ThemeChange', 'Blockly.Events.ToolboxItemSelect', 'Blockly.Events.TrashcanOpen', 'Blockly.Events.Ui', 'Blockly.Events.UiBase', 'Blockly.Events.VarBase', 'Blockly.Events.VarCreate', 'Blockly.Events.VarDelete', 'Blockly.Events.VarRename', 'Blockly.Events.ViewportChange', 'Blockly.Events.utils', 'Blockly.utils.deprecation'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_abstract.js', ['Blockly.Events.Abstract'], ['Blockly.Events.utils'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/events/events_block_base.js', ['Blockly.Events.BlockBase'], ['Blockly.Events.Abstract', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/events/events_block_base.js', ['Blockly.Events.BlockBase'], ['Blockly.Events.Abstract'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_block_change.js', ['Blockly.Events.BlockChange'], ['Blockly.Events.BlockBase', 'Blockly.Events.utils', 'Blockly.Xml', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_block_create.js', ['Blockly.Events.BlockCreate'], ['Blockly.Events.BlockBase', 'Blockly.Events.utils', 'Blockly.Xml', 'Blockly.registry', 'Blockly.serialization.blocks'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_block_delete.js', ['Blockly.Events.BlockDelete'], ['Blockly.Events.BlockBase', 'Blockly.Events.utils', 'Blockly.Xml', 'Blockly.registry', 'Blockly.serialization.blocks'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_block_drag.js', ['Blockly.Events.BlockDrag'], ['Blockly.Events.UiBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_block_move.js', ['Blockly.Events.BlockMove'], ['Blockly.ConnectionType', 'Blockly.Events.BlockBase', 'Blockly.Events.utils', 'Blockly.registry', 'Blockly.utils.Coordinate'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/events/events_bubble_open.js', ['Blockly.Events.BubbleOpen'], ['Blockly.Events.UiBase', 'Blockly.Events.utils', 'Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/events/events_bubble_open.js', ['Blockly.Events.BubbleOpen'], ['Blockly.Events.UiBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_click.js', ['Blockly.Events.Click'], ['Blockly.Events.UiBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/events/events_comment_base.js', ['Blockly.Events.CommentBase'], ['Blockly.Events.Abstract', 'Blockly.Events.utils', 'Blockly.Xml', 'Blockly.utils.object', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/events/events_comment_base.js', ['Blockly.Events.CommentBase'], ['Blockly.Events.Abstract', 'Blockly.Events.utils', 'Blockly.Xml', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_comment_change.js', ['Blockly.Events.CommentChange'], ['Blockly.Events.CommentBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_comment_create.js', ['Blockly.Events.CommentCreate'], ['Blockly.Events.CommentBase', 'Blockly.Events.utils', 'Blockly.Xml', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_comment_delete.js', ['Blockly.Events.CommentDelete'], ['Blockly.Events.CommentBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); @@ -58,8 +58,8 @@ goog.addDependency('../../core/events/events_theme_change.js', ['Blockly.Events. goog.addDependency('../../core/events/events_toolbox_item_select.js', ['Blockly.Events.ToolboxItemSelect'], ['Blockly.Events.UiBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_trashcan_open.js', ['Blockly.Events.TrashcanOpen'], ['Blockly.Events.UiBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_ui.js', ['Blockly.Events.Ui'], ['Blockly.Events.UiBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/events/events_ui_base.js', ['Blockly.Events.UiBase'], ['Blockly.Events.Abstract', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/events/events_var_base.js', ['Blockly.Events.VarBase'], ['Blockly.Events.Abstract', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/events/events_ui_base.js', ['Blockly.Events.UiBase'], ['Blockly.Events.Abstract'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/events/events_var_base.js', ['Blockly.Events.VarBase'], ['Blockly.Events.Abstract'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_var_create.js', ['Blockly.Events.VarCreate'], ['Blockly.Events.VarBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_var_delete.js', ['Blockly.Events.VarDelete'], ['Blockly.Events.VarBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_var_rename.js', ['Blockly.Events.VarRename'], ['Blockly.Events.VarBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); @@ -124,10 +124,10 @@ goog.addDependency('../../core/interfaces/i_toolbox.js', ['Blockly.IToolbox'], [ goog.addDependency('../../core/interfaces/i_toolbox_item.js', ['Blockly.IToolboxItem'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/internal_constants.js', ['Blockly.internalConstants'], ['Blockly.ConnectionType'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/keyboard_nav/ast_node.js', ['Blockly.ASTNode'], ['Blockly.ConnectionType', 'Blockly.utils.Coordinate'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/keyboard_nav/basic_cursor.js', ['Blockly.BasicCursor'], ['Blockly.ASTNode', 'Blockly.Cursor', 'Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/keyboard_nav/cursor.js', ['Blockly.Cursor'], ['Blockly.ASTNode', 'Blockly.Marker', 'Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/keyboard_nav/basic_cursor.js', ['Blockly.BasicCursor'], ['Blockly.ASTNode', 'Blockly.Cursor', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/keyboard_nav/cursor.js', ['Blockly.Cursor'], ['Blockly.ASTNode', 'Blockly.Marker', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/keyboard_nav/marker.js', ['Blockly.Marker'], [], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/keyboard_nav/tab_navigate_cursor.js', ['Blockly.TabNavigateCursor'], ['Blockly.ASTNode', 'Blockly.BasicCursor', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/keyboard_nav/tab_navigate_cursor.js', ['Blockly.TabNavigateCursor'], ['Blockly.ASTNode', 'Blockly.BasicCursor'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/marker_manager.js', ['Blockly.MarkerManager'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/menu.js', ['Blockly.Menu'], ['Blockly.browserEvents', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.style'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/menuitem.js', ['Blockly.MenuItem'], ['Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.idGenerator'], {'lang': 'es6', 'module': 'goog'}); @@ -219,7 +219,7 @@ goog.addDependency('../../core/theme_manager.js', ['Blockly.ThemeManager'], ['Bl goog.addDependency('../../core/toolbox/category.js', ['Blockly.ToolboxCategory'], ['Blockly.Css', 'Blockly.ISelectableToolboxItem', 'Blockly.ToolboxItem', 'Blockly.registry', 'Blockly.utils.aria', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/toolbox/collapsible_category.js', ['Blockly.CollapsibleToolboxCategory'], ['Blockly.ICollapsibleToolboxItem', 'Blockly.ToolboxCategory', 'Blockly.ToolboxSeparator', 'Blockly.registry', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/toolbox/separator.js', ['Blockly.ToolboxSeparator'], ['Blockly.Css', 'Blockly.ToolboxItem', 'Blockly.registry', 'Blockly.utils.dom', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/toolbox/toolbox.js', ['Blockly.Toolbox'], ['Blockly.BlockSvg', 'Blockly.CollapsibleToolboxCategory', 'Blockly.ComponentManager', 'Blockly.Css', 'Blockly.DeleteArea', 'Blockly.Events.ToolboxItemSelect', 'Blockly.Events.utils', 'Blockly.IAutoHideable', 'Blockly.IKeyboardAccessible', 'Blockly.IStyleable', 'Blockly.IToolbox', 'Blockly.Options', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.common', 'Blockly.registry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Rect', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/toolbox/toolbox.js', ['Blockly.Toolbox'], ['Blockly.BlockSvg', 'Blockly.CollapsibleToolboxCategory', 'Blockly.ComponentManager', 'Blockly.Css', 'Blockly.DeleteArea', 'Blockly.Events.ToolboxItemSelect', 'Blockly.Events.utils', 'Blockly.IAutoHideable', 'Blockly.IKeyboardAccessible', 'Blockly.IStyleable', 'Blockly.IToolbox', 'Blockly.Options', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.common', 'Blockly.registry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Rect', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/toolbox/toolbox_item.js', ['Blockly.ToolboxItem'], ['Blockly.IToolboxItem', 'Blockly.utils.idGenerator'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/tooltip.js', ['Blockly.Tooltip'], ['Blockly.browserEvents', 'Blockly.common', 'Blockly.utils.deprecation', 'Blockly.utils.string'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/touch.js', ['Blockly.Touch'], ['Blockly.utils.global', 'Blockly.utils.string'], {'lang': 'es6', 'module': 'goog'});