From df2eafb8ddb5bf4e1f6091b4199f9de600ea5582 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 6 Jan 2022 13:13:40 -0800 Subject: [PATCH] refactor: move properties into constructors and convert to classes (#5822) * refactor: move properties to constructor in block_drag_surface.js * refactor: move properties to constructor in block_svg.js * refactor: move properties to constructor in block.js * refactor: move properties to constructor in bubble.js * refactor: move properties to constructor in connection.js * refactor: move properties to constructor in flyout_base.js * refactor: move properties to constructor in flyout_button.js * refactor: move properties to constructor in generator.js * refactor: move properties to constructor in grid.js * refactor: move properties to constructor in input.js * refactor: move properties to constructor in mutator.js * refactor: move properties to constructor in scrollbar.js * refactor: move properties to constructor in trashcan.js * refactor: move properties to constructor in warning.js * refactor: move properties to constructor in workspace_audio.js * refactor: move properties to constructor in workspace_drag_surface_svg.js * refactor: move properties to constructor in workspace_svg.js * refactor: move properties to constructor in workspace.js * refactor: move properties to constructor in zoom_controls.js * chore: rebuild * refactor: convert zoom_controls.js to es6 class and format * refactor: convert workspace_audio.js to es6 class and format * refactor: convert workspace_dragger.js to es6 class and format * refactor: convert workspace_drag_surface_svg.js to es6 class and format * refactor: convert variable_model.js to es6 class and format * refactor: convert variable_map.js to es6 class and format * refactor: convert theme.js to es6 class and format * chore: remove bad comment --- core/block.js | 191 +++---- core/block_drag_surface.js | 90 ++- core/block_svg.js | 161 +++--- core/bubble.js | 98 ++-- core/connection.js | 83 ++- core/flyout_base.js | 178 +++--- core/flyout_button.js | 24 +- core/generator.js | 106 ++-- core/grid.js | 16 +- core/input.js | 25 +- core/mutator.js | 40 +- core/scrollbar.js | 118 ++-- core/theme.js | 274 +++++---- core/trashcan.js | 127 ++--- core/variable_map.js | 704 ++++++++++++----------- core/variable_model.js | 111 ++-- core/warning.js | 11 +- core/workspace.js | 53 +- core/workspace_audio.js | 242 ++++---- core/workspace_drag_surface_svg.js | 279 +++++----- core/workspace_dragger.js | 151 +++-- core/workspace_svg.js | 544 +++++++++--------- core/zoom_controls.js | 864 ++++++++++++++--------------- scripts/gulpfiles/chunks.json | 4 +- 24 files changed, 2242 insertions(+), 2252 deletions(-) diff --git a/core/block.js b/core/block.js index cda2e39a7..0b0226035 100644 --- a/core/block.js +++ b/core/block.js @@ -82,6 +82,94 @@ const Block = function(workspace, prototypeName, opt_id) { '" conflicts with Blockly.Generator members.'); } + /** + * Optional text data that round-trips between blocks and XML. + * Has no effect. May be used by 3rd parties for meta information. + * @type {?string} + */ + this.data = null; + + /** + * Has this block been disposed of? + * @type {boolean} + * @package + */ + this.disposed = false; + + /** + * Colour of the block as HSV hue value (0-360) + * This may be null if the block colour was not set via a hue number. + * @type {?number} + * @private + */ + this.hue_ = null; + + /** + * Colour of the block in '#RRGGBB' format. + * @type {string} + * @protected + */ + this.colour_ = '#000000'; + + /** + * Name of the block style. + * @type {string} + * @protected + */ + this.styleName_ = ''; + + /** + * An optional method called during initialization. + * @type {undefined|?function()} + */ + this.init = undefined; + + /** + * An optional serialization method for defining how to serialize the + * mutation state to XML. This must be coupled with defining `domToMutation`. + * @type {undefined|?function(...):!Element} + */ + this.mutationToDom = undefined; + + /** + * An optional deserialization method for defining how to deserialize the + * mutation state from XML. This must be coupled with defining + * `mutationToDom`. + * @type {undefined|?function(!Element)} + */ + this.domToMutation = undefined; + + /** + * An optional serialization method for defining how to serialize the block's + * extra state (eg mutation state) to something JSON compatible. This must be + * coupled with defining `loadExtraState`. + * @type {undefined|?function(): *} + */ + this.saveExtraState = undefined; + + /** + * An optional serialization method for defining how to deserialize the + * block's extra state (eg mutation state) from something JSON compatible. + * This must be coupled with defining `saveExtraState`. + * @type {undefined|?function(*)} + */ + this.loadExtraState = undefined; + + /** + * An optional property for suppressing adding STATEMENT_PREFIX and + * STATEMENT_SUFFIX to generated code. + * @type {?boolean} + */ + this.suppressPrefixSuffix = false; + + /** + * An optional property for declaring developer variables. Return a list of + * variable names for use by generators. Developer variables are never shown + * to the user, but are declared as global variables in the generated code. + * @type {undefined|?function():!Array} + */ + this.getDeveloperVariables = undefined; + /** @type {string} */ this.id = (opt_id && !workspace.getBlockById(opt_id)) ? opt_id : idGenerator.genUid(); @@ -271,6 +359,14 @@ const Block = function(workspace, prototypeName, opt_id) { */ Block.CommentModel; +/** + * An optional callback method to use whenever the block's parent workspace + * changes. This is usually only called from the constructor, the block type + * initializer function, or an extension initializer function. + * @type {undefined|?function(Abstract)} + */ +Block.prototype.onchange; + /** * The language-neutral ID given to the collapsed input. * @const {string} @@ -283,101 +379,6 @@ Block.COLLAPSED_INPUT_NAME = constants.COLLAPSED_INPUT_NAME; */ Block.COLLAPSED_FIELD_NAME = constants.COLLAPSED_FIELD_NAME; -/** - * Optional text data that round-trips between blocks and XML. - * Has no effect. May be used by 3rd parties for meta information. - * @type {?string} - */ -Block.prototype.data = null; - -/** - * Has this block been disposed of? - * @type {boolean} - * @package - */ -Block.prototype.disposed = false; - -/** - * Colour of the block as HSV hue value (0-360) - * This may be null if the block colour was not set via a hue number. - * @type {?number} - * @private - */ -Block.prototype.hue_ = null; - -/** - * Colour of the block in '#RRGGBB' format. - * @type {string} - * @protected - */ -Block.prototype.colour_ = '#000000'; - -/** - * Name of the block style. - * @type {string} - * @protected - */ -Block.prototype.styleName_ = ''; - -/** - * An optional method called during initialization. - * @type {?function()} - */ -Block.prototype.init; - -/** - * An optional callback method to use whenever the block's parent workspace - * changes. This is usually only called from the constructor, the block type - * initializer function, or an extension initializer function. - * @type {?function(Abstract)} - */ -Block.prototype.onchange; - -/** - * An optional serialization method for defining how to serialize the - * mutation state to XML. This must be coupled with defining `domToMutation`. - * @type {?function(...):!Element} - */ -Block.prototype.mutationToDom; - -/** - * An optional deserialization method for defining how to deserialize the - * mutation state from XML. This must be coupled with defining `mutationToDom`. - * @type {?function(!Element)} - */ -Block.prototype.domToMutation; - -/** - * An optional serialization method for defining how to serialize the block's - * extra state (eg mutation state) to something JSON compatible. This must be - * coupled with defining `loadExtraState`. - * @type {?function(): *} - */ -Block.prototype.saveExtraState; - -/** - * An optional serialization method for defining how to deserialize the block's - * extra state (eg mutation state) from something JSON compatible. This must be - * coupled with defining `saveExtraState`. - * @type {?function(*)} - */ -Block.prototype.loadExtraState; - -/** - * An optional property for suppressing adding STATEMENT_PREFIX and - * STATEMENT_SUFFIX to generated code. - * @type {?boolean} - */ -Block.prototype.suppressPrefixSuffix; - -/** - * An optional property for declaring developer variables. Return a list of - * variable names for use by generators. Developer variables are never shown to - * the user, but are declared as global variables in the generated code. - * @type {?function():!Array} - */ -Block.prototype.getDeveloperVariables; - /** * Dispose of this block. * @param {boolean} healStack If true, then try to heal any gap by connecting diff --git a/core/block_drag_surface.js b/core/block_drag_surface.js index ff619d390..7c7a64f17 100644 --- a/core/block_drag_surface.js +++ b/core/block_drag_surface.js @@ -41,60 +41,56 @@ const {Svg} = goog.require('Blockly.utils.Svg'); */ const BlockDragSurfaceSvg = function(container) { /** + * The SVG drag surface. Set once by BlockDragSurfaceSvg.createDom. + * @type {?SVGElement} + * @private + */ + this.SVG_ = null; + + /** + * This is where blocks live while they are being dragged if the drag surface + * is enabled. + * @type {?SVGElement} + * @private + */ + this.dragGroup_ = null; + + /** + * Containing HTML element; parent of the workspace and the drag surface. * @type {!Element} * @private */ this.container_ = container; + + /** + * Cached value for the scale of the drag surface. + * Used to set/get the correct translation during and after a drag. + * @type {number} + * @private + */ + this.scale_ = 1; + + /** + * Cached value for the translation of the drag surface. + * This translation is in pixel units, because the scale is applied to the + * drag group rather than the top-level SVG. + * @type {?Coordinate} + * @private + */ + this.surfaceXY_ = null; + + /** + * Cached value for the translation of the child drag surface in pixel units. + * Since the child drag surface tracks the translation of the workspace this + * is ultimately the translation of the workspace. + * @type {!Coordinate} + * @private + */ + this.childSurfaceXY_ = new Coordinate(0, 0); + this.createDom(); }; -/** - * The SVG drag surface. Set once by BlockDragSurfaceSvg.createDom. - * @type {?SVGElement} - * @private - */ -BlockDragSurfaceSvg.prototype.SVG_ = null; - -/** - * This is where blocks live while they are being dragged if the drag surface - * is enabled. - * @type {?SVGElement} - * @private - */ -BlockDragSurfaceSvg.prototype.dragGroup_ = null; - -/** - * Containing HTML element; parent of the workspace and the drag surface. - * @type {?Element} - * @private - */ -BlockDragSurfaceSvg.prototype.container_ = null; - -/** - * Cached value for the scale of the drag surface. - * Used to set/get the correct translation during and after a drag. - * @type {number} - * @private - */ -BlockDragSurfaceSvg.prototype.scale_ = 1; - -/** - * Cached value for the translation of the drag surface. - * This translation is in pixel units, because the scale is applied to the - * drag group rather than the top-level SVG. - * @type {?Coordinate} - * @private - */ -BlockDragSurfaceSvg.prototype.surfaceXY_ = null; - -/** - * Cached value for the translation of the child drag surface in pixel units. - * Since the child drag surface tracks the translation of the workspace this is - * ultimately the translation of the workspace. - * @type {!Coordinate} - * @private - */ -BlockDragSurfaceSvg.prototype.childSurfaceXY_ = new Coordinate(0, 0); /** * Create the drag surface and inject it into the container. diff --git a/core/block_svg.js b/core/block_svg.js index b3fcf2fc4..54dbdbefd 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -95,6 +95,88 @@ goog.require('Blockly.Touch'); * @alias Blockly.BlockSvg */ const BlockSvg = function(workspace, prototypeName, opt_id) { + /** + * An optional method called when a mutator dialog is first opened. + * This function must create and initialize a top-level block for the mutator + * dialog, and return it. This function should also populate this top-level + * block with any sub-blocks which are appropriate. This method must also be + * coupled with defining a `compose` method for the default mutation dialog + * button and UI to appear. + * @type {undefined|?function(WorkspaceSvg):!BlockSvg} + */ + this.decompose = undefined; + + /** + * An optional method called when a mutator dialog saves its content. + * This function is called to modify the original block according to new + * settings. This method must also be coupled with defining a `decompose` + * method for the default mutation dialog button and UI to appear. + * @type {undefined|?function(!BlockSvg)} + */ + this.compose = undefined; + + /** + * An optional method for defining custom block context menu items. + * @type {undefined|?function(!Array)} + */ + this.customContextMenu = undefined; + + /** + * An property used internally to reference the block's rendering debugger. + * @type {?BlockRenderingDebug} + * @package + */ + this.renderingDebugger = null; + + /** + * Height of this block, not including any statement blocks above or below. + * Height is in workspace units. + * @type {number} + */ + this.height = 0; + + /** + * Width of this block, including any connected value blocks. + * Width is in workspace units. + * @type {number} + */ + this.width = 0; + + /** + * Map from IDs for warnings text to PIDs of functions to apply them. + * Used to be able to maintain multiple warnings. + * @type {Object} + * @private + */ + this.warningTextDb_ = null; + + /** + * Block's mutator icon (if any). + * @type {?Mutator} + */ + this.mutator = null; + + /** + * Block's comment icon (if any). + * @type {?Comment} + * @deprecated August 2019. Use getCommentIcon instead. + */ + this.comment = null; + + /** + * Block's comment icon (if any). + * @type {?Comment} + * @private + */ + this.commentIcon_ = null; + + /** + * Block's warning icon (if any). + * @type {?Warning} + */ + this.warning = null; + + // Create core elements for the block. /** * @type {!SVGGElement} @@ -163,26 +245,6 @@ const BlockSvg = function(workspace, prototypeName, opt_id) { }; object.inherits(BlockSvg, Block); -/** - * Height of this block, not including any statement blocks above or below. - * Height is in workspace units. - */ -BlockSvg.prototype.height = 0; - -/** - * Width of this block, including any connected value blocks. - * Width is in workspace units. - */ -BlockSvg.prototype.width = 0; - -/** - * Map from IDs for warnings text to PIDs of functions to apply them. - * Used to be able to maintain multiple warnings. - * @type {Object} - * @private - */ -BlockSvg.prototype.warningTextDb_ = null; - /** * Constant for identifying rows that are to be rendered inline. * Don't collide with Blockly.inputTypes. @@ -199,39 +261,6 @@ BlockSvg.INLINE = -1; */ BlockSvg.COLLAPSED_WARNING_ID = 'TEMP_COLLAPSED_WARNING_'; -/** - * An optional method called when a mutator dialog is first opened. - * This function must create and initialize a top-level block for the mutator - * dialog, and return it. This function should also populate this top-level - * block with any sub-blocks which are appropriate. This method must also be - * coupled with defining a `compose` method for the default mutation dialog - * button and UI to appear. - * @type {?function(WorkspaceSvg):!BlockSvg} - */ -BlockSvg.prototype.decompose; - -/** - * An optional method called when a mutator dialog saves its content. - * This function is called to modify the original block according to new - * settings. This method must also be coupled with defining a `decompose` - * method for the default mutation dialog button and UI to appear. - * @type {?function(!BlockSvg)} - */ -BlockSvg.prototype.compose; - -/** - * An optional method for defining custom block context menu items. - * @type {?function(!Array)} - */ -BlockSvg.prototype.customContextMenu; - -/** - * An property used internally to reference the block's rendering debugger. - * @type {?BlockRenderingDebug} - * @package - */ -BlockSvg.prototype.renderingDebugger; - /** * Create and initialize the SVG representation of the block. * May be called more than once. @@ -323,32 +352,6 @@ BlockSvg.prototype.unselect = function() { this.removeSelect(); }; -/** - * Block's mutator icon (if any). - * @type {?Mutator} - */ -BlockSvg.prototype.mutator = null; - -/** - * Block's comment icon (if any). - * @type {?Comment} - * @deprecated August 2019. Use getCommentIcon instead. - */ -BlockSvg.prototype.comment = null; - -/** - * Block's comment icon (if any). - * @type {?Comment} - * @private - */ -BlockSvg.prototype.commentIcon_ = null; - -/** - * Block's warning icon (if any). - * @type {?Warning} - */ -BlockSvg.prototype.warning = null; - /** * Returns a list of mutator, comment, and warning icons. * @return {!Array} List of icons. diff --git a/core/bubble.js b/core/bubble.js index d9e6df9f0..78799c0ec 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -58,6 +58,58 @@ const Bubble = function( this.content_ = content; this.shape_ = shape; + /** + * Flag to stop incremental rendering during construction. + * @type {boolean} + * @private + */ + this.rendered_ = false; + + /** + * Absolute coordinate of anchor point, in workspace coordinates. + * @type {Coordinate} + * @private + */ + this.anchorXY_ = null; + + /** + * Relative X coordinate of bubble with respect to the anchor's centre, + * in workspace units. + * In RTL mode the initial value is negated. + * @type {number} + * @private + */ + this.relativeLeft_ = 0; + + /** + * Relative Y coordinate of bubble with respect to the anchor's centre, in + * workspace units. + * @type {number} + * @private + */ + this.relativeTop_ = 0; + + /** + * Width of bubble, in workspace units. + * @type {number} + * @private + */ + this.width_ = 0; + + /** + * Height of bubble, in workspace units. + * @type {number} + * @private + */ + this.height_ = 0; + + /** + * Automatically position and reposition the bubble. + * @type {boolean} + * @private + */ + this.autoLayout_ = true; + /** * Method to call on resize of bubble. * @type {?function()} @@ -182,52 +234,6 @@ Bubble.bubbleMouseUp_ = function(_e) { Bubble.unbindDragEvents_(); }; -/** - * Flag to stop incremental rendering during construction. - * @private - */ -Bubble.prototype.rendered_ = false; - -/** - * Absolute coordinate of anchor point, in workspace coordinates. - * @type {Coordinate} - * @private - */ -Bubble.prototype.anchorXY_ = null; - -/** - * Relative X coordinate of bubble with respect to the anchor's centre, - * in workspace units. - * In RTL mode the initial value is negated. - * @private - */ -Bubble.prototype.relativeLeft_ = 0; - -/** - * Relative Y coordinate of bubble with respect to the anchor's centre, in - * workspace units. - * @private - */ -Bubble.prototype.relativeTop_ = 0; - -/** - * Width of bubble, in workspace units. - * @private - */ -Bubble.prototype.width_ = 0; - -/** - * Height of bubble, in workspace units. - * @private - */ -Bubble.prototype.height_ = 0; - -/** - * Automatically position and reposition the bubble. - * @private - */ -Bubble.prototype.autoLayout_ = true; - /** * Create the bubble's DOM. * @param {!Element} content SVG content for the bubble. diff --git a/core/connection.js b/core/connection.js index 25f31bbe5..3273ab100 100644 --- a/core/connection.js +++ b/core/connection.js @@ -49,6 +49,47 @@ const Connection = function(source, type) { this.sourceBlock_ = source; /** @type {number} */ this.type = type; + + /** + * Connection this connection connects to. Null if not connected. + * @type {Connection} + */ + this.targetConnection = null; + + /** + * Has this connection been disposed of? + * @type {boolean} + * @package + */ + this.disposed = false; + + /** + * List of compatible value types. Null if all types are compatible. + * @type {Array} + * @private + */ + this.check_ = null; + + /** + * DOM representation of a shadow block, or null if none. + * @type {Element} + * @private + */ + this.shadowDom_ = null; + + /** + * Horizontal location of this connection. + * @type {number} + * @package + */ + this.x = 0; + + /** + * Vertical location of this connection. + * @type {number} + * @package + */ + this.y = 0; }; /** @@ -64,47 +105,6 @@ Connection.REASON_SHADOW_PARENT = 6; Connection.REASON_DRAG_CHECKS_FAILED = 7; Connection.REASON_PREVIOUS_AND_OUTPUT = 8; -/** - * Connection this connection connects to. Null if not connected. - * @type {Connection} - */ -Connection.prototype.targetConnection = null; - -/** - * Has this connection been disposed of? - * @type {boolean} - * @package - */ -Connection.prototype.disposed = false; - -/** - * List of compatible value types. Null if all types are compatible. - * @type {Array} - * @private - */ -Connection.prototype.check_ = null; - -/** - * DOM representation of a shadow block, or null if none. - * @type {Element} - * @private - */ -Connection.prototype.shadowDom_ = null; - -/** - * Horizontal location of this connection. - * @type {number} - * @package - */ -Connection.prototype.x = 0; - -/** - * Vertical location of this connection. - * @type {number} - * @package - */ -Connection.prototype.y = 0; - /** * Connect two connections together. This is the connection on the superior * block. @@ -163,7 +163,6 @@ Connection.prototype.connect_ = function(childConnection) { } }; - /** * Dispose of this connection and deal with connected blocks. * @package diff --git a/core/flyout_base.js b/core/flyout_base.js index d1484ffa8..5e8f1c6cc 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -167,98 +167,98 @@ const Flyout = function(workspaceOptions) { * @private */ this.recycledBlocks_ = []; + + /** + * Does the flyout automatically close when a block is created? + * @type {boolean} + */ + this.autoClose = true; + + /** + * Whether the flyout is visible. + * @type {boolean} + * @private + */ + this.isVisible_ = false; + + /** + * Whether the workspace containing this flyout is visible. + * @type {boolean} + * @private + */ + this.containerVisible_ = true; + + /** + * Corner radius of the flyout background. + * @type {number} + * @const + */ + this.CORNER_RADIUS = 8; + + /** + * Margin around the edges of the blocks in the flyout. + * @type {number} + * @const + */ + this.MARGIN = this.CORNER_RADIUS; + + // TODO: Move GAP_X and GAP_Y to their appropriate files. + + /** + * Gap between items in horizontal flyouts. Can be overridden with the "sep" + * element. + * @const {number} + */ + this.GAP_X = this.MARGIN * 3; + + /** + * Gap between items in vertical flyouts. Can be overridden with the "sep" + * element. + * @const {number} + */ + this.GAP_Y = this.MARGIN * 3; + + /** + * Top/bottom padding between scrollbar and edge of flyout background. + * @type {number} + * @const + */ + this.SCROLLBAR_MARGIN = 2.5; + + /** + * Width of flyout. + * @type {number} + * @protected + */ + this.width_ = 0; + + /** + * Height of flyout. + * @type {number} + * @protected + */ + this.height_ = 0; + + /** + * Range of a drag angle from a flyout considered "dragging toward workspace". + * Drags that are within the bounds of this many degrees from the orthogonal + * line to the flyout edge are considered to be "drags toward the workspace". + * Example: + * Flyout Edge Workspace + * [block] / <-within this angle, drags "toward workspace" | + * [block] ---- orthogonal to flyout boundary ---- | + * [block] \ | + * The angle is given in degrees from the orthogonal. + * + * This is used to know when to create a new block and when to scroll the + * flyout. Setting it to 360 means that all drags create a new block. + * @type {number} + * @protected + */ + this.dragAngleRange_ = 70; }; object.inherits(Flyout, DeleteArea); -/** - * Does the flyout automatically close when a block is created? - * @type {boolean} - */ -Flyout.prototype.autoClose = true; - -/** - * Whether the flyout is visible. - * @type {boolean} - * @private - */ -Flyout.prototype.isVisible_ = false; - -/** - * Whether the workspace containing this flyout is visible. - * @type {boolean} - * @private - */ -Flyout.prototype.containerVisible_ = true; - -/** - * Corner radius of the flyout background. - * @type {number} - * @const - */ -Flyout.prototype.CORNER_RADIUS = 8; - -/** - * Margin around the edges of the blocks in the flyout. - * @type {number} - * @const - */ -Flyout.prototype.MARGIN = Flyout.prototype.CORNER_RADIUS; - -// TODO: Move GAP_X and GAP_Y to their appropriate files. - -/** - * Gap between items in horizontal flyouts. Can be overridden with the "sep" - * element. - * @const {number} - */ -Flyout.prototype.GAP_X = Flyout.prototype.MARGIN * 3; - -/** - * Gap between items in vertical flyouts. Can be overridden with the "sep" - * element. - * @const {number} - */ -Flyout.prototype.GAP_Y = Flyout.prototype.MARGIN * 3; - -/** - * Top/bottom padding between scrollbar and edge of flyout background. - * @type {number} - * @const - */ -Flyout.prototype.SCROLLBAR_MARGIN = 2.5; - -/** - * Width of flyout. - * @type {number} - * @protected - */ -Flyout.prototype.width_ = 0; - -/** - * Height of flyout. - * @type {number} - * @protected - */ -Flyout.prototype.height_ = 0; - -/** - * Range of a drag angle from a flyout considered "dragging toward workspace". - * Drags that are within the bounds of this many degrees from the orthogonal - * line to the flyout edge are considered to be "drags toward the workspace". - * Example: - * Flyout Edge Workspace - * [block] / <-within this angle, drags "toward workspace" | - * [block] ---- orthogonal to flyout boundary ---- | - * [block] \ | - * The angle is given in degrees from the orthogonal. - * - * This is used to know when to create a new block and when to scroll the - * flyout. Setting it to 360 means that all drags create a new block. - * @type {number} - * @protected - */ -Flyout.prototype.dragAngleRange_ = 70; - /** * Creates the flyout's DOM. Only needs to be called once. The flyout can * either exist as its own SVG element or be a g element nested inside a diff --git a/core/flyout_button.js b/core/flyout_button.js index 6ade89e2d..a76b0fa85 100644 --- a/core/flyout_button.js +++ b/core/flyout_button.js @@ -102,6 +102,18 @@ const FlyoutButton = function(workspace, targetWorkspace, json, isLabel) { * @type {!toolbox.ButtonOrLabelInfo} */ this.info = json; + + /** + * The width of the button's rect. + * @type {number} + */ + this.width = 0; + + /** + * The height of the button's rect. + * @type {number} + */ + this.height = 0; }; /** @@ -114,18 +126,6 @@ FlyoutButton.MARGIN_X = 5; */ FlyoutButton.MARGIN_Y = 2; -/** - * The width of the button's rect. - * @type {number} - */ -FlyoutButton.prototype.width = 0; - -/** - * The height of the button's rect. - * @type {number} - */ -FlyoutButton.prototype.height = 0; - /** * Create the button elements. * @return {!SVGElement} The button's SVG group. diff --git a/core/generator.js b/core/generator.js index 6e8a7bfa7..55b7f9095 100644 --- a/core/generator.js +++ b/core/generator.js @@ -37,61 +37,61 @@ const Generator = function(name) { this.name_ = name; this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ = new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g'); + + /** + * Arbitrary code to inject into locations that risk causing infinite loops. + * Any instances of '%1' will be replaced by the block ID that failed. + * E.g. ' checkTimeout(%1);\n' + * @type {?string} + */ + this.INFINITE_LOOP_TRAP = null; + + /** + * Arbitrary code to inject before every statement. + * Any instances of '%1' will be replaced by the block ID of the statement. + * E.g. 'highlight(%1);\n' + * @type {?string} + */ + this.STATEMENT_PREFIX = null; + + /** + * Arbitrary code to inject after every statement. + * Any instances of '%1' will be replaced by the block ID of the statement. + * E.g. 'highlight(%1);\n' + * @type {?string} + */ + this.STATEMENT_SUFFIX = null; + + /** + * The method of indenting. Defaults to two spaces, but language generators + * may override this to increase indent or change to tabs. + * @type {string} + */ + this.INDENT = ' '; + + /** + * Maximum length for a comment before wrapping. Does not account for + * indenting level. + * @type {number} + */ + this.COMMENT_WRAP = 60; + + /** + * List of outer-inner pairings that do NOT require parentheses. + * @type {!Array>} + */ + this.ORDER_OVERRIDES = []; + + /** + * Whether the init method has been called. + * Generators that set this flag to false after creation and true in init + * will cause blockToCode to emit a warning if the generator has not been + * initialized. If this flag is untouched, it will have no effect. + * @type {?boolean} + */ + this.isInitialized = null; }; -/** - * Arbitrary code to inject into locations that risk causing infinite loops. - * Any instances of '%1' will be replaced by the block ID that failed. - * E.g. ' checkTimeout(%1);\n' - * @type {?string} - */ -Generator.prototype.INFINITE_LOOP_TRAP = null; - -/** - * Arbitrary code to inject before every statement. - * Any instances of '%1' will be replaced by the block ID of the statement. - * E.g. 'highlight(%1);\n' - * @type {?string} - */ -Generator.prototype.STATEMENT_PREFIX = null; - -/** - * Arbitrary code to inject after every statement. - * Any instances of '%1' will be replaced by the block ID of the statement. - * E.g. 'highlight(%1);\n' - * @type {?string} - */ -Generator.prototype.STATEMENT_SUFFIX = null; - -/** - * The method of indenting. Defaults to two spaces, but language generators - * may override this to increase indent or change to tabs. - * @type {string} - */ -Generator.prototype.INDENT = ' '; - -/** - * Maximum length for a comment before wrapping. Does not account for - * indenting level. - * @type {number} - */ -Generator.prototype.COMMENT_WRAP = 60; - -/** - * List of outer-inner pairings that do NOT require parentheses. - * @type {!Array>} - */ -Generator.prototype.ORDER_OVERRIDES = []; - -/** - * Whether the init method has been called. - * Generators that set this flag to false after creation and true in init - * will cause blockToCode to emit a warning if the generator has not been - * initialized. If this flag is untouched, it will have no effect. - * @type {?boolean} - */ -Generator.prototype.isInitialized = null; - /** * Generate code for all blocks in the workspace to the specified language. * @param {!Workspace=} workspace Workspace to generate code from. diff --git a/core/grid.js b/core/grid.js index a16d3f083..950175607 100644 --- a/core/grid.js +++ b/core/grid.js @@ -33,6 +33,14 @@ const {Svg} = goog.require('Blockly.utils.Svg'); * @alias Blockly.Grid */ const Grid = function(pattern, options) { + /** + * The scale of the grid, used to set stroke width on grid lines. + * This should always be the same as the workspace scale. + * @type {number} + * @private + */ + this.scale_ = 1; + /** * The grid's SVG pattern, created during injection. * @type {!SVGElement} @@ -77,14 +85,6 @@ const Grid = function(pattern, options) { this.snapToGrid_ = options['snap']; }; -/** - * The scale of the grid, used to set stroke width on grid lines. - * This should always be the same as the workspace scale. - * @type {number} - * @private - */ -Grid.prototype.scale_ = 1; - /** * Dispose of this grid and unlink from the DOM. * @package diff --git a/core/input.js b/core/input.js index edc0c5c62..318063022 100644 --- a/core/input.js +++ b/core/input.js @@ -69,20 +69,21 @@ const Input = function(type, name, block, connection) { this.connection = connection; /** @type {!Array} */ this.fieldRow = []; + + /** + * Alignment of input's fields (left, right or centre). + * @type {number} + */ + this.align = Align.LEFT; + + /** + * Is the input visible? + * @type {boolean} + * @private + */ + this.visible_ = true; }; -/** - * Alignment of input's fields (left, right or centre). - * @type {number} - */ -Input.prototype.align = Align.LEFT; - -/** - * Is the input visible? - * @type {boolean} - * @private - */ -Input.prototype.visible_ = true; /** * Get the source block for this input. diff --git a/core/mutator.js b/core/mutator.js index f8c95326c..bbadd41ea 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -57,28 +57,30 @@ goog.require('Blockly.Events.BubbleOpen'); const Mutator = function(quarkNames) { Mutator.superClass_.constructor.call(this, null); this.quarkNames_ = quarkNames; + + /** + * Workspace in the mutator's bubble. + * @type {?WorkspaceSvg} + * @private + */ + this.workspace_ = null; + + /** + * Width of workspace. + * @type {number} + * @private + */ + this.workspaceWidth_ = 0; + + /** + * Height of workspace. + * @type {number} + * @private + */ + this.workspaceHeight_ = 0; }; object.inherits(Mutator, Icon); -/** - * Workspace in the mutator's bubble. - * @type {?WorkspaceSvg} - * @private - */ -Mutator.prototype.workspace_ = null; - -/** - * Width of workspace. - * @private - */ -Mutator.prototype.workspaceWidth_ = 0; - -/** - * Height of workspace. - * @private - */ -Mutator.prototype.workspaceHeight_ = 0; - /** * Set the block this mutator is associated with. * @param {!BlockSvg} block The block associated with this mutator. diff --git a/core/scrollbar.js b/core/scrollbar.js index 42b7d9953..45f4fa5fe 100644 --- a/core/scrollbar.js +++ b/core/scrollbar.js @@ -87,6 +87,66 @@ const Scrollbar = function( */ this.ratio = null; + /** + * The location of the origin of the workspace that the scrollbar is in, + * measured in CSS pixels relative to the injection div origin. This is + * usually (0, 0). When the scrollbar is in a flyout it may have a different + * origin. + * @type {Coordinate} + * @private + */ + this.origin_ = new Coordinate(0, 0); + + /** + * The position of the mouse along this scrollbar's major axis at the start of + * the most recent drag. + * Units are CSS pixels, with (0, 0) at the top left of the browser window. + * For a horizontal scrollbar this is the x coordinate of the mouse down + * event; for a vertical scrollbar it's the y coordinate of the mouse down + * event. + * @type {number} + * @private + */ + this.startDragMouse_ = 0; + + /** + * The length of the scrollbars (including the handle and the background), in + * CSS pixels. This is equivalent to scrollbar background length and the area + * within which the scrollbar handle can move. + * @type {number} + * @private + */ + this.scrollbarLength_ = 0; + + /** + * The length of the scrollbar handle in CSS pixels. + * @type {number} + * @private + */ + this.handleLength_ = 0; + + /** + * The offset of the start of the handle from the scrollbar position, in CSS + * pixels. + * @type {number} + * @private + */ + this.handlePosition_ = 0; + + /** + * Whether the scrollbar handle is visible. + * @type {boolean} + * @private + */ + this.isVisible_ = true; + + /** + * Whether the workspace containing this scrollbar is visible. + * @type {boolean} + * @private + */ + this.containerVisible_ = true; + this.createDom_(opt_class); /** @@ -124,64 +184,6 @@ const Scrollbar = function( this.svgHandle_, 'mousedown', scrollbar, scrollbar.onMouseDownHandle_); }; -/** - * The location of the origin of the workspace that the scrollbar is in, - * measured in CSS pixels relative to the injection div origin. This is usually - * (0, 0). When the scrollbar is in a flyout it may have a different origin. - * @type {Coordinate} - * @private - */ -Scrollbar.prototype.origin_ = new Coordinate(0, 0); - -/** - * The position of the mouse along this scrollbar's major axis at the start of - * the most recent drag. - * Units are CSS pixels, with (0, 0) at the top left of the browser window. - * For a horizontal scrollbar this is the x coordinate of the mouse down event; - * for a vertical scrollbar it's the y coordinate of the mouse down event. - * @type {number} - * @private - */ -Scrollbar.prototype.startDragMouse_ = 0; - -/** - * The length of the scrollbars (including the handle and the background), in - * CSS pixels. This is equivalent to scrollbar background length and the area - * within which the scrollbar handle can move. - * @type {number} - * @private - */ -Scrollbar.prototype.scrollbarLength_ = 0; - -/** - * The length of the scrollbar handle in CSS pixels. - * @type {number} - * @private - */ -Scrollbar.prototype.handleLength_ = 0; - -/** - * The offset of the start of the handle from the scrollbar position, in CSS - * pixels. - * @type {number} - * @private - */ -Scrollbar.prototype.handlePosition_ = 0; - -/** - * Whether the scrollbar handle is visible. - * @type {boolean} - * @private - */ -Scrollbar.prototype.isVisible_ = true; - -/** - * Whether the workspace containing this scrollbar is visible. - * @type {boolean} - * @private - */ -Scrollbar.prototype.containerVisible_ = true; - /** * Width of vertical scrollbar or height of horizontal scrollbar in CSS pixels. * Scrollbars should be larger on touch devices. diff --git a/core/theme.js b/core/theme.js index fb78bde30..d156b1227 100644 --- a/core/theme.js +++ b/core/theme.js @@ -21,65 +21,157 @@ const registry = goog.require('Blockly.registry'); /** * Class for a theme. - * @param {string} name Theme name. - * @param {!Object=} opt_blockStyles A map - * from style names (strings) to objects with style attributes for blocks. - * @param {!Object=} opt_categoryStyles A - * map from style names (strings) to objects with style attributes for - * categories. - * @param {!Theme.ComponentStyle=} opt_componentStyles A map of Blockly - * component names to style value. - * @constructor * @alias Blockly.Theme */ -const Theme = function( - name, opt_blockStyles, opt_categoryStyles, opt_componentStyles) { +class Theme { /** - * The theme name. This can be used to reference a specific theme in CSS. - * @type {string} + * @param {string} name Theme name. + * @param {!Object=} opt_blockStyles A map + * from style names (strings) to objects with style attributes for blocks. + * @param {!Object=} opt_categoryStyles A + * map from style names (strings) to objects with style attributes for + * categories. + * @param {!Theme.ComponentStyle=} opt_componentStyles A map of Blockly + * component names to style value. */ - this.name = name; + constructor(name, opt_blockStyles, opt_categoryStyles, opt_componentStyles) { + /** + * The theme name. This can be used to reference a specific theme in CSS. + * @type {string} + */ + this.name = name; + /** + * The block styles map. + * @type {!Object} + * @package + */ + this.blockStyles = opt_blockStyles || Object.create(null); + + /** + * The category styles map. + * @type {!Object} + * @package + */ + this.categoryStyles = opt_categoryStyles || Object.create(null); + + /** + * The UI components styles map. + * @type {!Theme.ComponentStyle} + * @package + */ + this.componentStyles = opt_componentStyles || + (/** @type {Theme.ComponentStyle} */ (Object.create(null))); + + /** + * The font style. + * @type {!Theme.FontStyle} + * @package + */ + this.fontStyle = /** @type {Theme.FontStyle} */ (Object.create(null)); + + /** + * Whether or not to add a 'hat' on top of all blocks with no previous or + * output connections. + * @type {?boolean} + * @package + */ + this.startHats = null; + + // Register the theme by name. + registry.register(registry.Type.THEME, name, this); + } /** - * The block styles map. - * @type {!Object} + * Gets the class name that identifies this theme. + * @return {string} The CSS class name. * @package */ - this.blockStyles = opt_blockStyles || Object.create(null); - + getClassName() { + return this.name + '-theme'; + } /** - * The category styles map. - * @type {!Object} - * @package + * Overrides or adds a style to the blockStyles map. + * @param {string} blockStyleName The name of the block style. + * @param {Theme.BlockStyle} blockStyle The block style. */ - this.categoryStyles = opt_categoryStyles || Object.create(null); - + setBlockStyle(blockStyleName, blockStyle) { + this.blockStyles[blockStyleName] = blockStyle; + } /** - * The UI components styles map. - * @type {!Theme.ComponentStyle} - * @package + * Overrides or adds a style to the categoryStyles map. + * @param {string} categoryStyleName The name of the category style. + * @param {Theme.CategoryStyle} categoryStyle The category style. */ - this.componentStyles = opt_componentStyles || - (/** @type {Theme.ComponentStyle} */ (Object.create(null))); - + setCategoryStyle(categoryStyleName, categoryStyle) { + this.categoryStyles[categoryStyleName] = categoryStyle; + } /** - * The font style. - * @type {!Theme.FontStyle} - * @package + * Gets the style for a given Blockly UI component. If the style value is a + * string, we attempt to find the value of any named references. + * @param {string} componentName The name of the component. + * @return {?string} The style value. */ - this.fontStyle = /** @type {Theme.FontStyle} */ (Object.create(null)); - + getComponentStyle(componentName) { + const style = this.componentStyles[componentName]; + if (style && typeof style === 'string' && + this.getComponentStyle(/** @type {string} */ (style))) { + return this.getComponentStyle(/** @type {string} */ (style)); + } + return style ? String(style) : null; + } /** - * Whether or not to add a 'hat' on top of all blocks with no previous or - * output connections. - * @type {?boolean} - * @package + * Configure a specific Blockly UI component with a style value. + * @param {string} componentName The name of the component. + * @param {*} styleValue The style value. */ - this.startHats = null; + setComponentStyle(componentName, styleValue) { + this.componentStyles[componentName] = styleValue; + } + /** + * Configure a theme's font style. + * @param {Theme.FontStyle} fontStyle The font style. + */ + setFontStyle(fontStyle) { + this.fontStyle = fontStyle; + } + /** + * Configure a theme's start hats. + * @param {boolean} startHats True if the theme enables start hats, false + * otherwise. + */ + setStartHats(startHats) { + this.startHats = startHats; + } + /** + * Define a new Blockly theme. + * @param {string} name The name of the theme. + * @param {!Object} themeObj An object containing theme properties. + * @return {!Theme} A new Blockly theme. + */ + static defineTheme(name, themeObj) { + const theme = new Theme(name); + let base = themeObj['base']; + if (base) { + if (typeof base === 'string') { + base = registry.getObject(registry.Type.THEME, base); + } + if (base instanceof Theme) { + object.deepMerge(theme, base); + theme.name = name; + } + } - // Register the theme by name. - registry.register(registry.Type.THEME, name, this); -}; + object.deepMerge(theme.blockStyles, themeObj['blockStyles']); + object.deepMerge(theme.categoryStyles, themeObj['categoryStyles']); + object.deepMerge(theme.componentStyles, themeObj['componentStyles']); + object.deepMerge(theme.fontStyle, themeObj['fontStyle']); + if (themeObj['startHats'] !== null) { + theme.startHats = themeObj['startHats']; + } + + return theme; + } +} /** * A block style. @@ -133,102 +225,4 @@ Theme.ComponentStyle; */ Theme.FontStyle; -/** - * Gets the class name that identifies this theme. - * @return {string} The CSS class name. - * @package - */ -Theme.prototype.getClassName = function() { - return this.name + '-theme'; -}; - -/** - * Overrides or adds a style to the blockStyles map. - * @param {string} blockStyleName The name of the block style. - * @param {Theme.BlockStyle} blockStyle The block style. - */ -Theme.prototype.setBlockStyle = function(blockStyleName, blockStyle) { - this.blockStyles[blockStyleName] = blockStyle; -}; - -/** - * Overrides or adds a style to the categoryStyles map. - * @param {string} categoryStyleName The name of the category style. - * @param {Theme.CategoryStyle} categoryStyle The category style. - */ -Theme.prototype.setCategoryStyle = function(categoryStyleName, categoryStyle) { - this.categoryStyles[categoryStyleName] = categoryStyle; -}; - -/** - * Gets the style for a given Blockly UI component. If the style value is a - * string, we attempt to find the value of any named references. - * @param {string} componentName The name of the component. - * @return {?string} The style value. - */ -Theme.prototype.getComponentStyle = function(componentName) { - const style = this.componentStyles[componentName]; - if (style && typeof style === 'string' && - this.getComponentStyle(/** @type {string} */ (style))) { - return this.getComponentStyle(/** @type {string} */ (style)); - } - return style ? String(style) : null; -}; - -/** - * Configure a specific Blockly UI component with a style value. - * @param {string} componentName The name of the component. - * @param {*} styleValue The style value. - */ -Theme.prototype.setComponentStyle = function(componentName, styleValue) { - this.componentStyles[componentName] = styleValue; -}; - -/** - * Configure a theme's font style. - * @param {Theme.FontStyle} fontStyle The font style. - */ -Theme.prototype.setFontStyle = function(fontStyle) { - this.fontStyle = fontStyle; -}; - -/** - * Configure a theme's start hats. - * @param {boolean} startHats True if the theme enables start hats, false - * otherwise. - */ -Theme.prototype.setStartHats = function(startHats) { - this.startHats = startHats; -}; - -/** - * Define a new Blockly theme. - * @param {string} name The name of the theme. - * @param {!Object} themeObj An object containing theme properties. - * @return {!Theme} A new Blockly theme. - */ -Theme.defineTheme = function(name, themeObj) { - const theme = new Theme(name); - let base = themeObj['base']; - if (base) { - if (typeof base === 'string') { - base = registry.getObject(registry.Type.THEME, base); - } - if (base instanceof Theme) { - object.deepMerge(theme, base); - theme.name = name; - } - } - - object.deepMerge(theme.blockStyles, themeObj['blockStyles']); - object.deepMerge(theme.categoryStyles, themeObj['categoryStyles']); - object.deepMerge(theme.componentStyles, themeObj['componentStyles']); - object.deepMerge(theme.fontStyle, themeObj['fontStyle']); - if (themeObj['startHats'] !== null) { - theme.startHats = themeObj['startHats']; - } - - return theme; -}; - exports.Theme = Theme; diff --git a/core/trashcan.js b/core/trashcan.js index 698eb7a74..3fa25e433 100644 --- a/core/trashcan.js +++ b/core/trashcan.js @@ -93,6 +93,70 @@ const Trashcan = function(workspace) { if (this.workspace_.options.maxTrashcanContents <= 0) { return; } + + /** + * Current open/close state of the lid. + * @type {boolean} + */ + this.isLidOpen = false; + + /** + * The minimum openness of the lid. Used to indicate if the trashcan contains + * blocks. + * @type {number} + * @private + */ + this.minOpenness_ = 0; + + /** + * The SVG group containing the trash can. + * @type {SVGElement} + * @private + */ + this.svgGroup_ = null; + + /** + * The SVG image element of the trash can lid. + * @type {SVGElement} + * @private + */ + this.svgLid_ = null; + + /** + * Task ID of opening/closing animation. + * @type {number} + * @private + */ + this.lidTask_ = 0; + + /** + * Current state of lid opening (0.0 = closed, 1.0 = open). + * @type {number} + * @private + */ + this.lidOpen_ = 0; + + /** + * Left coordinate of the trash can. + * @type {number} + * @private + */ + this.left_ = 0; + + /** + * Top coordinate of the trash can. + * @type {number} + * @private + */ + this.top_ = 0; + + /** + * Whether this trash can has been initialized. + * @type {boolean} + * @private + */ + this.initialized_ = false; + // Create flyout options. const flyoutWorkspaceOptions = new Options( /** @type {!BlocklyOptions} */ @@ -202,69 +266,6 @@ const OPACITY_MAX = 0.8; */ const MAX_LID_ANGLE = 45; -/** - * Current open/close state of the lid. - * @type {boolean} - */ -Trashcan.prototype.isLidOpen = false; - -/** - * The minimum openness of the lid. Used to indicate if the trashcan contains - * blocks. - * @type {number} - * @private - */ -Trashcan.prototype.minOpenness_ = 0; - -/** - * The SVG group containing the trash can. - * @type {SVGElement} - * @private - */ -Trashcan.prototype.svgGroup_ = null; - -/** - * The SVG image element of the trash can lid. - * @type {SVGElement} - * @private - */ -Trashcan.prototype.svgLid_ = null; - -/** - * Task ID of opening/closing animation. - * @type {number} - * @private - */ -Trashcan.prototype.lidTask_ = 0; - -/** - * Current state of lid opening (0.0 = closed, 1.0 = open). - * @type {number} - * @private - */ -Trashcan.prototype.lidOpen_ = 0; - -/** - * Left coordinate of the trash can. - * @type {number} - * @private - */ -Trashcan.prototype.left_ = 0; - -/** - * Top coordinate of the trash can. - * @type {number} - * @private - */ -Trashcan.prototype.top_ = 0; - -/** - * Whether this has been initialized. - * @type {boolean} - * @private - */ -Trashcan.prototype.initialized_ = false; - /** * Create the trash can elements. * @return {!SVGElement} The trash can's SVG group. diff --git a/core/variable_map.js b/core/variable_map.js index 797d1a72f..68c5a2826 100644 --- a/core/variable_map.js +++ b/core/variable_map.js @@ -37,390 +37,374 @@ goog.require('Blockly.Events.VarRename'); * Class for a variable map. This contains a dictionary data structure with * variable types as keys and lists of variables as values. The list of * variables are the type indicated by the key. - * @param {!Workspace} workspace The workspace this map belongs to. - * @constructor * @alias Blockly.VariableMap */ -const VariableMap = function(workspace) { +class VariableMap { /** - * A map from variable type to list of variable names. The lists contain all - * of the named variables in the workspace, including variables - * that are not currently in use. - * @type {!Object>} - * @private + * @param {!Workspace} workspace The workspace this map belongs to. */ - this.variableMap_ = Object.create(null); + constructor(workspace) { + /** + * A map from variable type to list of variable names. The lists contain + * all of the named variables in the workspace, including variables that are + * not currently in use. + * @type {!Object>} + * @private + */ + this.variableMap_ = Object.create(null); + /** + * The workspace this map belongs to. + * @type {!Workspace} + */ + this.workspace = workspace; + } /** - * The workspace this map belongs to. - * @type {!Workspace} + * Clear the variable map. */ - this.workspace = workspace; -}; - -/** - * Clear the variable map. - */ -VariableMap.prototype.clear = function() { - this.variableMap_ = Object.create(null); -}; - -/* Begin functions for renaming variables. */ - -/** - * Rename the given variable by updating its name in the variable map. - * @param {!VariableModel} variable Variable to rename. - * @param {string} newName New variable name. - * @package - */ -VariableMap.prototype.renameVariable = function(variable, newName) { - const type = variable.type; - const conflictVar = this.getVariable(newName, type); - const blocks = this.workspace.getAllBlocks(false); - eventUtils.setGroup(true); - try { - // The IDs may match if the rename is a simple case change (name1 -> Name1). - if (!conflictVar || conflictVar.getId() === variable.getId()) { - this.renameVariableAndUses_(variable, newName, blocks); - } else { - this.renameVariableWithConflict_(variable, newName, conflictVar, blocks); - } - } finally { - eventUtils.setGroup(false); + clear() { + this.variableMap_ = Object.create(null); } -}; - -/** - * Rename a variable by updating its name in the variable map. Identify the - * variable to rename with the given ID. - * @param {string} id ID of the variable to rename. - * @param {string} newName New variable name. - */ -VariableMap.prototype.renameVariableById = function(id, newName) { - const variable = this.getVariableById(id); - if (!variable) { - throw Error('Tried to rename a variable that didn\'t exist. ID: ' + id); - } - - this.renameVariable(variable, newName); -}; - -/** - * Update the name of the given variable and refresh all references to it. - * The new name must not conflict with any existing variable names. - * @param {!VariableModel} variable Variable to rename. - * @param {string} newName New variable name. - * @param {!Array} blocks The list of all blocks in the - * workspace. - * @private - */ -VariableMap.prototype.renameVariableAndUses_ = function( - variable, newName, blocks) { - eventUtils.fire( - new (eventUtils.get(eventUtils.VAR_RENAME))(variable, newName)); - variable.name = newName; - for (let i = 0; i < blocks.length; i++) { - blocks[i].updateVarName(variable); - } -}; - -/** - * Update the name of the given variable to the same name as an existing - * variable. The two variables are coalesced into a single variable with the ID - * of the existing variable that was already using newName. - * Refresh all references to the variable. - * @param {!VariableModel} variable Variable to rename. - * @param {string} newName New variable name. - * @param {!VariableModel} conflictVar The variable that was already - * using newName. - * @param {!Array} blocks The list of all blocks in the - * workspace. - * @private - */ -VariableMap.prototype.renameVariableWithConflict_ = function( - variable, newName, conflictVar, blocks) { - const type = variable.type; - const oldCase = conflictVar.name; - - if (newName !== oldCase) { - // Simple rename to change the case and update references. - this.renameVariableAndUses_(conflictVar, newName, blocks); - } - - // These blocks now refer to a different variable. - // These will fire change events. - for (let i = 0; i < blocks.length; i++) { - blocks[i].renameVarById(variable.getId(), conflictVar.getId()); - } - - // Finally delete the original variable, which is now unreferenced. - eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable)); - // And remove it from the list. - arrayUtils.removeElem(this.variableMap_[type], variable); -}; - -/* End functions for renaming variables. */ - -/** - * Create a variable with a given name, optional type, and optional ID. - * @param {string} name The name of the variable. This must be unique across - * variables and procedures. - * @param {?string=} opt_type The type of the variable like 'int' or 'string'. - * Does not need to be unique. Field_variable can filter variables based on - * their type. This will default to '' which is a specific type. - * @param {?string=} opt_id The unique ID of the variable. This will default to - * a UUID. - * @return {!VariableModel} The newly created variable. - */ -VariableMap.prototype.createVariable = function(name, opt_type, opt_id) { - let variable = this.getVariable(name, opt_type); - if (variable) { - if (opt_id && variable.getId() !== opt_id) { - throw Error( - 'Variable "' + name + '" is already in use and its id is "' + - variable.getId() + '" which conflicts with the passed in ' + - 'id, "' + opt_id + '".'); - } - // The variable already exists and has the same ID. - return variable; - } - if (opt_id && this.getVariableById(opt_id)) { - throw Error('Variable id, "' + opt_id + '", is already in use.'); - } - const id = opt_id || idGenerator.genUid(); - const type = opt_type || ''; - variable = new VariableModel(this.workspace, name, type, id); - - const variables = this.variableMap_[type] || []; - variables.push(variable); - // Delete the list of variables of this type, and re-add it so that - // the most recent addition is at the end. - // This is used so the toolbox's set block is set to the most recent variable. - delete this.variableMap_[type]; - this.variableMap_[type] = variables; - - return variable; -}; - -/* Begin functions for variable deletion. */ - -/** - * Delete a variable. - * @param {!VariableModel} variable Variable to delete. - */ -VariableMap.prototype.deleteVariable = function(variable) { - const variableId = variable.getId(); - const variableList = this.variableMap_[variable.type]; - for (let i = 0; i < variableList.length; i++) { - const tempVar = variableList[i]; - if (tempVar.getId() === variableId) { - variableList.splice(i, 1); - eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable)); - return; - } - } -}; - -/** - * Delete a variables by the passed in ID and all of its uses from this - * workspace. May prompt the user for confirmation. - * @param {string} id ID of variable to delete. - */ -VariableMap.prototype.deleteVariableById = function(id) { - const variable = this.getVariableById(id); - if (variable) { - // Check whether this variable is a function parameter before deleting. - const variableName = variable.name; - const uses = this.getVariableUsesById(id); - for (let i = 0, block; (block = uses[i]); i++) { - if (block.type === 'procedures_defnoreturn' || - block.type === 'procedures_defreturn') { - const procedureName = String(block.getFieldValue('NAME')); - const deleteText = Msg['CANNOT_DELETE_VARIABLE_PROCEDURE'] - .replace('%1', variableName) - .replace('%2', procedureName); - dialog.alert(deleteText); - return; - } - } - - const map = this; - if (uses.length > 1) { - // Confirm before deleting multiple blocks. - const confirmText = Msg['DELETE_VARIABLE_CONFIRMATION'] - .replace('%1', String(uses.length)) - .replace('%2', variableName); - dialog.confirm(confirmText, function(ok) { - if (ok && variable) { - map.deleteVariableInternal(variable, uses); - } - }); - } else { - // No confirmation necessary for a single block. - map.deleteVariableInternal(variable, uses); - } - } else { - console.warn('Can\'t delete non-existent variable: ' + id); - } -}; - -/** - * Deletes a variable and all of its uses from this workspace without asking the - * user for confirmation. - * @param {!VariableModel} variable Variable to delete. - * @param {!Array} uses An array of uses of the variable. - * @package - */ -VariableMap.prototype.deleteVariableInternal = function(variable, uses) { - const existingGroup = eventUtils.getGroup(); - if (!existingGroup) { + /* Begin functions for renaming variables. */ + /** + * Rename the given variable by updating its name in the variable map. + * @param {!VariableModel} variable Variable to rename. + * @param {string} newName New variable name. + * @package + */ + renameVariable(variable, newName) { + const type = variable.type; + const conflictVar = this.getVariable(newName, type); + const blocks = this.workspace.getAllBlocks(false); eventUtils.setGroup(true); - } - try { - for (let i = 0; i < uses.length; i++) { - uses[i].dispose(true); - } - this.deleteVariable(variable); - } finally { - if (!existingGroup) { + try { + // The IDs may match if the rename is a simple case change (name1 -> + // Name1). + if (!conflictVar || conflictVar.getId() === variable.getId()) { + this.renameVariableAndUses_(variable, newName, blocks); + } else { + this.renameVariableWithConflict_( + variable, newName, conflictVar, blocks); + } + } finally { eventUtils.setGroup(false); } } -}; + /** + * Rename a variable by updating its name in the variable map. Identify the + * variable to rename with the given ID. + * @param {string} id ID of the variable to rename. + * @param {string} newName New variable name. + */ + renameVariableById(id, newName) { + const variable = this.getVariableById(id); + if (!variable) { + throw Error('Tried to rename a variable that didn\'t exist. ID: ' + id); + } -/* End functions for variable deletion. */ + this.renameVariable(variable, newName); + } + /** + * Update the name of the given variable and refresh all references to it. + * The new name must not conflict with any existing variable names. + * @param {!VariableModel} variable Variable to rename. + * @param {string} newName New variable name. + * @param {!Array} blocks The list of all blocks in the + * workspace. + * @private + */ + renameVariableAndUses_(variable, newName, blocks) { + eventUtils.fire( + new (eventUtils.get(eventUtils.VAR_RENAME))(variable, newName)); + variable.name = newName; + for (let i = 0; i < blocks.length; i++) { + blocks[i].updateVarName(variable); + } + } + /** + * Update the name of the given variable to the same name as an existing + * variable. The two variables are coalesced into a single variable with the + * ID of the existing variable that was already using newName. Refresh all + * references to the variable. + * @param {!VariableModel} variable Variable to rename. + * @param {string} newName New variable name. + * @param {!VariableModel} conflictVar The variable that was already + * using newName. + * @param {!Array} blocks The list of all blocks in the + * workspace. + * @private + */ + renameVariableWithConflict_(variable, newName, conflictVar, blocks) { + const type = variable.type; + const oldCase = conflictVar.name; -/** - * Find the variable by the given name and type and return it. Return null if - * it is not found. - * @param {string} name The name to check for. - * @param {?string=} opt_type The type of the variable. If not provided it - * defaults to the empty string, which is a specific type. - * @return {?VariableModel} The variable with the given name, or null if - * it was not found. - */ -VariableMap.prototype.getVariable = function(name, opt_type) { - const type = opt_type || ''; - const list = this.variableMap_[type]; - if (list) { - for (let j = 0, variable; (variable = list[j]); j++) { - if (Names.equals(variable.name, name)) { - return variable; + if (newName !== oldCase) { + // Simple rename to change the case and update references. + this.renameVariableAndUses_(conflictVar, newName, blocks); + } + + // These blocks now refer to a different variable. + // These will fire change events. + for (let i = 0; i < blocks.length; i++) { + blocks[i].renameVarById(variable.getId(), conflictVar.getId()); + } + + // Finally delete the original variable, which is now unreferenced. + eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable)); + // And remove it from the list. + arrayUtils.removeElem(this.variableMap_[type], variable); + } + /* End functions for renaming variables. */ + /** + * Create a variable with a given name, optional type, and optional ID. + * @param {string} name The name of the variable. This must be unique across + * variables and procedures. + * @param {?string=} opt_type The type of the variable like 'int' or 'string'. + * Does not need to be unique. Field_variable can filter variables based + * on their type. This will default to '' which is a specific type. + * @param {?string=} opt_id The unique ID of the variable. This will default + * to a UUID. + * @return {!VariableModel} The newly created variable. + */ + createVariable(name, opt_type, opt_id) { + let variable = this.getVariable(name, opt_type); + if (variable) { + if (opt_id && variable.getId() !== opt_id) { + throw Error( + 'Variable "' + name + '" is already in use and its id is "' + + variable.getId() + '" which conflicts with the passed in ' + + 'id, "' + opt_id + '".'); + } + // The variable already exists and has the same ID. + return variable; + } + if (opt_id && this.getVariableById(opt_id)) { + throw Error('Variable id, "' + opt_id + '", is already in use.'); + } + const id = opt_id || idGenerator.genUid(); + const type = opt_type || ''; + variable = new VariableModel(this.workspace, name, type, id); + + const variables = this.variableMap_[type] || []; + variables.push(variable); + // Delete the list of variables of this type, and re-add it so that + // the most recent addition is at the end. + // This is used so the toolbox's set block is set to the most recent + // variable. + delete this.variableMap_[type]; + this.variableMap_[type] = variables; + + return variable; + } + /* Begin functions for variable deletion. */ + /** + * Delete a variable. + * @param {!VariableModel} variable Variable to delete. + */ + deleteVariable(variable) { + const variableId = variable.getId(); + const variableList = this.variableMap_[variable.type]; + for (let i = 0; i < variableList.length; i++) { + const tempVar = variableList[i]; + if (tempVar.getId() === variableId) { + variableList.splice(i, 1); + eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable)); + return; } } } - return null; -}; + /** + * Delete a variables by the passed in ID and all of its uses from this + * workspace. May prompt the user for confirmation. + * @param {string} id ID of variable to delete. + */ + deleteVariableById(id) { + const variable = this.getVariableById(id); + if (variable) { + // Check whether this variable is a function parameter before deleting. + const variableName = variable.name; + const uses = this.getVariableUsesById(id); + for (let i = 0, block; (block = uses[i]); i++) { + if (block.type === 'procedures_defnoreturn' || + block.type === 'procedures_defreturn') { + const procedureName = String(block.getFieldValue('NAME')); + const deleteText = Msg['CANNOT_DELETE_VARIABLE_PROCEDURE'] + .replace('%1', variableName) + .replace('%2', procedureName); + dialog.alert(deleteText); + return; + } + } -/** - * Find the variable by the given ID and return it. Return null if not found. - * @param {string} id The ID to check for. - * @return {?VariableModel} The variable with the given ID. - */ -VariableMap.prototype.getVariableById = function(id) { - const keys = Object.keys(this.variableMap_); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - for (let j = 0, variable; (variable = this.variableMap_[key][j]); j++) { - if (variable.getId() === id) { - return variable; + const map = this; + if (uses.length > 1) { + // Confirm before deleting multiple blocks. + const confirmText = Msg['DELETE_VARIABLE_CONFIRMATION'] + .replace('%1', String(uses.length)) + .replace('%2', variableName); + dialog.confirm(confirmText, function(ok) { + if (ok && variable) { + map.deleteVariableInternal(variable, uses); + } + }); + } else { + // No confirmation necessary for a single block. + map.deleteVariableInternal(variable, uses); + } + } else { + console.warn('Can\'t delete non-existent variable: ' + id); + } + } + /** + * Deletes a variable and all of its uses from this workspace without asking + * the user for confirmation. + * @param {!VariableModel} variable Variable to delete. + * @param {!Array} uses An array of uses of the variable. + * @package + */ + deleteVariableInternal(variable, uses) { + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } + try { + for (let i = 0; i < uses.length; i++) { + uses[i].dispose(true); + } + this.deleteVariable(variable); + } finally { + if (!existingGroup) { + eventUtils.setGroup(false); } } } - return null; -}; - -/** - * Get a list containing all of the variables of a specified type. If type is - * null, return list of variables with empty string type. - * @param {?string} type Type of the variables to find. - * @return {!Array} The sought after variables of the - * passed in type. An empty array if none are found. - */ -VariableMap.prototype.getVariablesOfType = function(type) { - type = type || ''; - const variableList = this.variableMap_[type]; - if (variableList) { - return variableList.slice(); - } - return []; -}; - -/** - * Return all variable and potential variable types. This list always contains - * the empty string. - * @param {?Workspace} ws The workspace used to look for potential - * variables. This can be different than the workspace stored on this object - * if the passed in ws is a flyout workspace. - * @return {!Array} List of variable types. - * @package - */ -VariableMap.prototype.getVariableTypes = function(ws) { - const variableMap = {}; - object.mixin(variableMap, this.variableMap_); - if (ws && ws.getPotentialVariableMap()) { - object.mixin(variableMap, ws.getPotentialVariableMap().variableMap_); - } - const types = Object.keys(variableMap); - let hasEmpty = false; - for (let i = 0; i < types.length; i++) { - if (types[i] === '') { - hasEmpty = true; - } - } - if (!hasEmpty) { - types.push(''); - } - return types; -}; - -/** - * Return all variables of all types. - * @return {!Array} List of variable models. - */ -VariableMap.prototype.getAllVariables = function() { - let allVariables = []; - for (const key in this.variableMap_) { - allVariables = allVariables.concat(this.variableMap_[key]); - } - return allVariables; -}; - -/** - * Returns all of the variable names of all types. - * @return {!Array} All of the variable names of all types. - */ -VariableMap.prototype.getAllVariableNames = function() { - const allNames = []; - for (const key in this.variableMap_) { - const variables = this.variableMap_[key]; - for (let i = 0, variable; (variable = variables[i]); i++) { - allNames.push(variable.name); - } - } - return allNames; -}; - -/** - * Find all the uses of a named variable. - * @param {string} id ID of the variable to find. - * @return {!Array} Array of block usages. - */ -VariableMap.prototype.getVariableUsesById = function(id) { - const uses = []; - const blocks = this.workspace.getAllBlocks(false); - // Iterate through every block and check the name. - for (let i = 0; i < blocks.length; i++) { - const blockVariables = blocks[i].getVarModels(); - if (blockVariables) { - for (let j = 0; j < blockVariables.length; j++) { - if (blockVariables[j].getId() === id) { - uses.push(blocks[i]); + /* End functions for variable deletion. */ + /** + * Find the variable by the given name and type and return it. Return null if + * it is not found. + * @param {string} name The name to check for. + * @param {?string=} opt_type The type of the variable. If not provided it + * defaults to the empty string, which is a specific type. + * @return {?VariableModel} The variable with the given name, or null if + * it was not found. + */ + getVariable(name, opt_type) { + const type = opt_type || ''; + const list = this.variableMap_[type]; + if (list) { + for (let j = 0, variable; (variable = list[j]); j++) { + if (Names.equals(variable.name, name)) { + return variable; } } } + return null; } - return uses; -}; + /** + * Find the variable by the given ID and return it. Return null if not found. + * @param {string} id The ID to check for. + * @return {?VariableModel} The variable with the given ID. + */ + getVariableById(id) { + const keys = Object.keys(this.variableMap_); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + for (let j = 0, variable; (variable = this.variableMap_[key][j]); j++) { + if (variable.getId() === id) { + return variable; + } + } + } + return null; + } + /** + * Get a list containing all of the variables of a specified type. If type is + * null, return list of variables with empty string type. + * @param {?string} type Type of the variables to find. + * @return {!Array} The sought after variables of the + * passed in type. An empty array if none are found. + */ + getVariablesOfType(type) { + type = type || ''; + const variableList = this.variableMap_[type]; + if (variableList) { + return variableList.slice(); + } + return []; + } + /** + * Return all variable and potential variable types. This list always + * contains the empty string. + * @param {?Workspace} ws The workspace used to look for potential + * variables. This can be different than the workspace stored on this object + * if the passed in ws is a flyout workspace. + * @return {!Array} List of variable types. + * @package + */ + getVariableTypes(ws) { + const variableMap = {}; + object.mixin(variableMap, this.variableMap_); + if (ws && ws.getPotentialVariableMap()) { + object.mixin(variableMap, ws.getPotentialVariableMap().variableMap_); + } + const types = Object.keys(variableMap); + let hasEmpty = false; + for (let i = 0; i < types.length; i++) { + if (types[i] === '') { + hasEmpty = true; + } + } + if (!hasEmpty) { + types.push(''); + } + return types; + } + /** + * Return all variables of all types. + * @return {!Array} List of variable models. + */ + getAllVariables() { + let allVariables = []; + for (const key in this.variableMap_) { + allVariables = allVariables.concat(this.variableMap_[key]); + } + return allVariables; + } + /** + * Returns all of the variable names of all types. + * @return {!Array} All of the variable names of all types. + */ + getAllVariableNames() { + const allNames = []; + for (const key in this.variableMap_) { + const variables = this.variableMap_[key]; + for (let i = 0, variable; (variable = variables[i]); i++) { + allNames.push(variable.name); + } + } + return allNames; + } + /** + * Find all the uses of a named variable. + * @param {string} id ID of the variable to find. + * @return {!Array} Array of block usages. + */ + getVariableUsesById(id) { + const uses = []; + const blocks = this.workspace.getAllBlocks(false); + // Iterate through every block and check the name. + for (let i = 0; i < blocks.length; i++) { + const blockVariables = blocks[i].getVarModels(); + if (blockVariables) { + for (let j = 0; j < blockVariables.length; j++) { + if (blockVariables[j].getId() === id) { + uses.push(blocks[i]); + } + } + } + } + return uses; + } +} exports.VariableMap = VariableMap; diff --git a/core/variable_model.js b/core/variable_model.js index d169b4a49..b88fe65e7 100644 --- a/core/variable_model.js +++ b/core/variable_model.js @@ -26,70 +26,71 @@ goog.require('Blockly.Events.VarCreate'); /** * Class for a variable model. * Holds information for the variable including name, ID, and type. - * @param {!Workspace} workspace The variable's workspace. - * @param {string} name The name of the variable. This is the user-visible name - * (e.g. 'my var' or '私の変数'), not the generated name. - * @param {string=} opt_type The type of the variable like 'int' or 'string'. - * Does not need to be unique. Field_variable can filter variables based on - * their type. This will default to '' which is a specific type. - * @param {string=} opt_id The unique ID of the variable. This will default to - * a UUID. * @see {Blockly.FieldVariable} - * @constructor * @alias Blockly.VariableModel */ -const VariableModel = function(workspace, name, opt_type, opt_id) { +class VariableModel { /** - * The workspace the variable is in. - * @type {!Workspace} + * @param {!Workspace} workspace The variable's workspace. + * @param {string} name The name of the variable. This is the user-visible + * name (e.g. 'my var' or '私の変数'), not the generated name. + * @param {string=} opt_type The type of the variable like 'int' or 'string'. + * Does not need to be unique. Field_variable can filter variables based + * on their type. This will default to '' which is a specific type. + * @param {string=} opt_id The unique ID of the variable. This will default to + * a UUID. */ - this.workspace = workspace; + constructor(workspace, name, opt_type, opt_id) { + /** + * The workspace the variable is in. + * @type {!Workspace} + */ + this.workspace = workspace; + /** + * The name of the variable, typically defined by the user. It may be + * changed by the user. + * @type {string} + */ + this.name = name; + + /** + * The type of the variable, such as 'int' or 'sound_effect'. This may be + * used to build a list of variables of a specific type. By default this is + * the empty string '', which is a specific type. + * @see {Blockly.FieldVariable} + * @type {string} + */ + this.type = opt_type || ''; + + /** + * A unique ID for the variable. This should be defined at creation and + * not change, even if the name changes. In most cases this should be a + * UUID. + * @type {string} + * @private + */ + this.id_ = opt_id || idGenerator.genUid(); + + eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(this)); + } /** - * The name of the variable, typically defined by the user. It may be - * changed by the user. - * @type {string} + * @return {string} The ID for the variable. */ - this.name = name; - + getId() { + return this.id_; + } /** - * The type of the variable, such as 'int' or 'sound_effect'. This may be - * used to build a list of variables of a specific type. By default this is - * the empty string '', which is a specific type. - * @see {Blockly.FieldVariable} - * @type {string} + * A custom compare function for the VariableModel objects. + * @param {VariableModel} var1 First variable to compare. + * @param {VariableModel} var2 Second variable to compare. + * @return {number} -1 if name of var1 is less than name of var2, 0 if equal, + * and 1 if greater. + * @package */ - this.type = opt_type || ''; - - /** - * A unique ID for the variable. This should be defined at creation and - * not change, even if the name changes. In most cases this should be a - * UUID. - * @type {string} - * @private - */ - this.id_ = opt_id || idGenerator.genUid(); - - eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(this)); -}; - -/** - * @return {string} The ID for the variable. - */ -VariableModel.prototype.getId = function() { - return this.id_; -}; - -/** - * A custom compare function for the VariableModel objects. - * @param {VariableModel} var1 First variable to compare. - * @param {VariableModel} var2 Second variable to compare. - * @return {number} -1 if name of var1 is less than name of var2, 0 if equal, - * and 1 if greater. - * @package - */ -VariableModel.compareByName = function(var1, var2) { - return var1.name.localeCompare(var2.name, undefined, {sensitivity: 'base'}); -}; + static compareByName(var1, var2) { + return var1.name.localeCompare(var2.name, undefined, {sensitivity: 'base'}); + } +} exports.VariableModel = VariableModel; diff --git a/core/warning.js b/core/warning.js index e58807839..e34e3258f 100644 --- a/core/warning.js +++ b/core/warning.js @@ -43,14 +43,15 @@ const Warning = function(block) { this.createIcon(); // The text_ object can contain multiple warnings. this.text_ = Object.create(null); + + /** + * Does this icon get hidden when the block is collapsed? + * @type {boolean} + */ + this.collapseHidden = false; }; object.inherits(Warning, Icon); -/** - * Does this icon get hidden when the block is collapsed. - */ -Warning.prototype.collapseHidden = false; - /** * Draw the warning icon. * @param {!Element} group The icon group. diff --git a/core/workspace.js b/core/workspace.js index 41d2a1c91..3a4af4359 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -72,6 +72,33 @@ const Workspace = function(opt_options) { /** @type {toolbox.Position} */ this.toolboxPosition = this.options.toolboxPosition; + /** + * Returns `true` if the workspace is visible and `false` if it's headless. + * @type {boolean} + */ + this.rendered = false; + + /** + * Returns `true` if the workspace is currently in the process of a bulk + * clear. + * @type {boolean} + * @package + */ + this.isClearing = false; + + /** + * Maximum number of undo events in stack. `0` turns off undo, `Infinity` sets + * it to unlimited. + * @type {number} + */ + this.MAX_UNDO = 1024; + + /** + * Set of databases for rapid lookup of connection locations. + * @type {Array} + */ + this.connectionDBList = null; + const connectionCheckerClass = registry.getClassFromOptions( registry.Type.CONNECTION_CHECKER, this.options, true); /** @@ -143,32 +170,6 @@ const Workspace = function(opt_options) { this.potentialVariableMap_ = null; }; -/** - * Returns `true` if the workspace is visible and `false` if it's headless. - * @type {boolean} - */ -Workspace.prototype.rendered = false; - -/** - * Returns `true` if the workspace is currently in the process of a bulk clear. - * @type {boolean} - * @package - */ -Workspace.prototype.isClearing = false; - -/** - * Maximum number of undo events in stack. `0` turns off undo, `Infinity` sets - * it to unlimited. - * @type {number} - */ -Workspace.prototype.MAX_UNDO = 1024; - -/** - * Set of databases for rapid lookup of connection locations. - * @type {Array} - */ -Workspace.prototype.connectionDBList = null; - /** * Dispose of this workspace. * Unlink from all DOM elements to prevent memory leaks. diff --git a/core/workspace_audio.js b/core/workspace_audio.js index 5fd0eff4f..1be8bb5b7 100644 --- a/core/workspace_audio.js +++ b/core/workspace_audio.js @@ -26,138 +26,138 @@ const {globalThis} = goog.require('Blockly.utils.global'); /** * Class for loading, storing, and playing audio for a workspace. - * @param {WorkspaceSvg} parentWorkspace The parent of the workspace - * this audio object belongs to, or null. - * @constructor * @alias Blockly.WorkspaceAudio */ -const WorkspaceAudio = function(parentWorkspace) { +class WorkspaceAudio { /** - * The parent of the workspace this object belongs to, or null. May be - * checked for sounds that this object can't find. - * @type {WorkspaceSvg} - * @private + * @param {WorkspaceSvg} parentWorkspace The parent of the workspace + * this audio object belongs to, or null. */ - this.parentWorkspace_ = parentWorkspace; + constructor(parentWorkspace) { + /** + * The parent of the workspace this object belongs to, or null. May be + * checked for sounds that this object can't find. + * @type {WorkspaceSvg} + * @private + */ + this.parentWorkspace_ = parentWorkspace; + /** + * Database of pre-loaded sounds. + * @private + */ + this.SOUNDS_ = Object.create(null); + + /** + * Time that the last sound was played. + * @type {Date} + * @private + */ + this.lastSound_ = null; + } /** - * Database of pre-loaded sounds. - * @private + * Dispose of this audio manager. + * @package */ - this.SOUNDS_ = Object.create(null); -}; - -/** - * Time that the last sound was played. - * @type {Date} - * @private - */ -WorkspaceAudio.prototype.lastSound_ = null; - -/** - * Dispose of this audio manager. - * @package - */ -WorkspaceAudio.prototype.dispose = function() { - this.parentWorkspace_ = null; - this.SOUNDS_ = null; -}; - -/** - * Load an audio file. Cache it, ready for instantaneous playing. - * @param {!Array} filenames List of file types in decreasing order of - * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav'] - * Filenames include path from Blockly's root. File extensions matter. - * @param {string} name Name of sound. - */ -WorkspaceAudio.prototype.load = function(filenames, name) { - if (!filenames.length) { - return; + dispose() { + this.parentWorkspace_ = null; + this.SOUNDS_ = null; } - let audioTest; - try { - audioTest = new globalThis['Audio'](); - } catch (e) { - // No browser support for Audio. - // IE can throw an error even if the Audio object exists. - return; - } - let sound; - for (let i = 0; i < filenames.length; i++) { - const filename = filenames[i]; - const ext = filename.match(/\.(\w+)$/); - if (ext && audioTest.canPlayType('audio/' + ext[1])) { - // Found an audio format we can play. - sound = new globalThis['Audio'](filename); - break; - } - } - if (sound && sound.play) { - this.SOUNDS_[name] = sound; - } -}; - -/** - * Preload all the audio files so that they play quickly when asked for. - * @package - */ -WorkspaceAudio.prototype.preload = function() { - for (const name in this.SOUNDS_) { - const sound = this.SOUNDS_[name]; - sound.volume = 0.01; - const playPromise = sound.play(); - // Edge does not return a promise, so we need to check. - if (playPromise !== undefined) { - // If we don't wait for the play request to complete before calling - // pause() we will get an exception: (DOMException: The play() request was - // interrupted) See more: - // https://developers.google.com/web/updates/2017/06/play-request-was-interrupted - playPromise.then(sound.pause).catch(function() { - // Play without user interaction was prevented. - }); - } else { - sound.pause(); - } - - // iOS can only process one sound at a time. Trying to load more than one - // corrupts the earlier ones. Just load one and leave the others uncached. - if (userAgent.IPAD || userAgent.IPHONE) { - break; - } - } -}; - -/** - * Play a named sound at specified volume. If volume is not specified, - * use full volume (1). - * @param {string} name Name of sound. - * @param {number=} opt_volume Volume of sound (0-1). - */ -WorkspaceAudio.prototype.play = function(name, opt_volume) { - const sound = this.SOUNDS_[name]; - if (sound) { - // Don't play one sound on top of another. - const now = new Date; - if (this.lastSound_ !== null && - now - this.lastSound_ < internalConstants.SOUND_LIMIT) { + /** + * Load an audio file. Cache it, ready for instantaneous playing. + * @param {!Array} filenames List of file types in decreasing order of + * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav'] + * Filenames include path from Blockly's root. File extensions matter. + * @param {string} name Name of sound. + */ + load(filenames, name) { + if (!filenames.length) { return; } - this.lastSound_ = now; - let mySound; - if (userAgent.IPAD || userAgent.ANDROID) { - // Creating a new audio node causes lag in Android and iPad. Android - // refetches the file from the server, iPad uses a singleton audio - // node which must be deleted and recreated for each new audio tag. - mySound = sound; - } else { - mySound = sound.cloneNode(); + let audioTest; + try { + audioTest = new globalThis['Audio'](); + } catch (e) { + // No browser support for Audio. + // IE can throw an error even if the Audio object exists. + return; + } + let sound; + for (let i = 0; i < filenames.length; i++) { + const filename = filenames[i]; + const ext = filename.match(/\.(\w+)$/); + if (ext && audioTest.canPlayType('audio/' + ext[1])) { + // Found an audio format we can play. + sound = new globalThis['Audio'](filename); + break; + } + } + if (sound && sound.play) { + this.SOUNDS_[name] = sound; } - mySound.volume = (opt_volume === undefined ? 1 : opt_volume); - mySound.play(); - } else if (this.parentWorkspace_) { - // Maybe a workspace on a lower level knows about this sound. - this.parentWorkspace_.getAudioManager().play(name, opt_volume); } -}; + /** + * Preload all the audio files so that they play quickly when asked for. + * @package + */ + preload() { + for (const name in this.SOUNDS_) { + const sound = this.SOUNDS_[name]; + sound.volume = 0.01; + const playPromise = sound.play(); + // Edge does not return a promise, so we need to check. + if (playPromise !== undefined) { + // If we don't wait for the play request to complete before calling + // pause() we will get an exception: (DOMException: The play() request + // was interrupted) See more: + // https://developers.google.com/web/updates/2017/06/play-request-was-interrupted + playPromise.then(sound.pause).catch(function() { + // Play without user interaction was prevented. + }); + } else { + sound.pause(); + } + + // iOS can only process one sound at a time. Trying to load more than one + // corrupts the earlier ones. Just load one and leave the others + // uncached. + if (userAgent.IPAD || userAgent.IPHONE) { + break; + } + } + } + /** + * Play a named sound at specified volume. If volume is not specified, + * use full volume (1). + * @param {string} name Name of sound. + * @param {number=} opt_volume Volume of sound (0-1). + */ + play(name, opt_volume) { + const sound = this.SOUNDS_[name]; + if (sound) { + // Don't play one sound on top of another. + const now = new Date; + if (this.lastSound_ !== null && + now - this.lastSound_ < internalConstants.SOUND_LIMIT) { + return; + } + this.lastSound_ = now; + let mySound; + if (userAgent.IPAD || userAgent.ANDROID) { + // Creating a new audio node causes lag in Android and iPad. Android + // refetches the file from the server, iPad uses a singleton audio + // node which must be deleted and recreated for each new audio tag. + mySound = sound; + } else { + mySound = sound.cloneNode(); + } + mySound.volume = (opt_volume === undefined ? 1 : opt_volume); + mySound.play(); + } else if (this.parentWorkspace_) { + // Maybe a workspace on a lower level knows about this sound. + this.parentWorkspace_.getAudioManager().play(name, opt_volume); + } + } +} exports.WorkspaceAudio = WorkspaceAudio; diff --git a/core/workspace_drag_surface_svg.js b/core/workspace_drag_surface_svg.js index 586e9676f..51f9a901d 100644 --- a/core/workspace_drag_surface_svg.js +++ b/core/workspace_drag_surface_svg.js @@ -33,155 +33,162 @@ const {Svg} = goog.require('Blockly.utils.Svg'); * Blocks are moved into this SVG during a drag, improving performance. * The entire SVG is translated using CSS transforms instead of SVG so the * blocks are never repainted during drag improving performance. - * @param {!Element} container Containing element. - * @constructor * @alias Blockly.WorkspaceDragSurfaceSvg */ -const WorkspaceDragSurfaceSvg = function(container) { - this.container_ = container; - this.createDom(); -}; - -/** - * The SVG drag surface. Set once by WorkspaceDragSurfaceSvg.createDom. - * @type {SVGElement} - * @private - */ -WorkspaceDragSurfaceSvg.prototype.SVG_ = null; - -/** - * Containing HTML element; parent of the workspace and the drag surface. - * @type {Element} - * @private - */ -WorkspaceDragSurfaceSvg.prototype.container_ = null; - -/** - * Create the drag surface and inject it into the container. - */ -WorkspaceDragSurfaceSvg.prototype.createDom = function() { - if (this.SVG_) { - return; // Already created. - } - +class WorkspaceDragSurfaceSvg { /** - * Dom structure when the workspace is being dragged. If there is no drag in - * progress, the SVG is empty and display: none. - * - * - * /g> - * + * @param {!Element} container Containing element. */ - this.SVG_ = dom.createSvgElement( - Svg.SVG, { - 'xmlns': dom.SVG_NS, - 'xmlns:html': dom.HTML_NS, - 'xmlns:xlink': dom.XLINK_NS, - 'version': '1.1', - 'class': 'blocklyWsDragSurface blocklyOverflowVisible', - }, - null); - this.container_.appendChild(this.SVG_); -}; + constructor(container) { + /** + * The SVG drag surface. Set once by WorkspaceDragSurfaceSvg.createDom. + * @type {SVGElement} + * @private + */ + this.SVG_ = null; -/** - * Translate the entire drag surface during a drag. - * We translate the drag surface instead of the blocks inside the surface - * so that the browser avoids repainting the SVG. - * Because of this, the drag coordinates must be adjusted by scale. - * @param {number} x X translation for the entire surface - * @param {number} y Y translation for the entire surface - * @package - */ -WorkspaceDragSurfaceSvg.prototype.translateSurface = function(x, y) { - // This is a work-around to prevent a the blocks from rendering - // fuzzy while they are being moved on the drag surface. - const fixedX = x.toFixed(0); - const fixedY = y.toFixed(0); + /** + * Containing HTML element; parent of the workspace and the drag surface. + * @type {Element} + * @private + */ + this.container_ = container; - this.SVG_.style.display = 'block'; - dom.setCssTransform( - this.SVG_, 'translate3d(' + fixedX + 'px, ' + fixedY + 'px, 0)'); -}; + /** + * The element to insert the block canvas and bubble canvas after when it + * goes back in the DOM at the end of a drag. + * @type {Element} + * @private + */ + this.previousSibling_ = null; -/** - * Reports the surface translation in scaled workspace coordinates. - * Use this when finishing a drag to return blocks to the correct position. - * @return {!Coordinate} Current translation of the surface - * @package - */ -WorkspaceDragSurfaceSvg.prototype.getSurfaceTranslation = function() { - return svgMath.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_)); -}; - -/** - * Move the blockCanvas and bubbleCanvas out of the surface SVG and on to - * newSurface. - * @param {SVGElement} newSurface The element to put the drag surface contents - * into. - * @package - */ -WorkspaceDragSurfaceSvg.prototype.clearAndHide = function(newSurface) { - if (!newSurface) { - throw Error( - 'Couldn\'t clear and hide the drag surface: missing new surface.'); - } - const blockCanvas = /** @type {!Element} */ (this.SVG_.childNodes[0]); - const bubbleCanvas = /** @type {!Element} */ (this.SVG_.childNodes[1]); - if (!blockCanvas || !bubbleCanvas || - !dom.hasClass(blockCanvas, 'blocklyBlockCanvas') || - !dom.hasClass(bubbleCanvas, 'blocklyBubbleCanvas')) { - throw Error( - 'Couldn\'t clear and hide the drag surface. A node was missing.'); + this.createDom(); } + /** + * Create the drag surface and inject it into the container. + */ + createDom() { + if (this.SVG_) { + return; // Already created. + } - // If there is a previous sibling, put the blockCanvas back right afterwards, - // otherwise insert it as the first child node in newSurface. - if (this.previousSibling_ !== null) { - dom.insertAfter(blockCanvas, this.previousSibling_); - } else { - newSurface.insertBefore(blockCanvas, newSurface.firstChild); + /** + * Dom structure when the workspace is being dragged. If there is no drag in + * progress, the SVG is empty and display: none. + * + * + * /g> + * + */ + this.SVG_ = dom.createSvgElement( + Svg.SVG, { + 'xmlns': dom.SVG_NS, + 'xmlns:html': dom.HTML_NS, + 'xmlns:xlink': dom.XLINK_NS, + 'version': '1.1', + 'class': 'blocklyWsDragSurface blocklyOverflowVisible', + }, + null); + this.container_.appendChild(this.SVG_); } + /** + * Translate the entire drag surface during a drag. + * We translate the drag surface instead of the blocks inside the surface + * so that the browser avoids repainting the SVG. + * Because of this, the drag coordinates must be adjusted by scale. + * @param {number} x X translation for the entire surface + * @param {number} y Y translation for the entire surface + * @package + */ + translateSurface(x, y) { + // This is a work-around to prevent a the blocks from rendering + // fuzzy while they are being moved on the drag surface. + const fixedX = x.toFixed(0); + const fixedY = y.toFixed(0); - // Reattach the bubble canvas after the blockCanvas. - dom.insertAfter(bubbleCanvas, blockCanvas); - // Hide the drag surface. - this.SVG_.style.display = 'none'; - if (this.SVG_.childNodes.length) { - throw Error('Drag surface was not cleared.'); + this.SVG_.style.display = 'block'; + dom.setCssTransform( + this.SVG_, 'translate3d(' + fixedX + 'px, ' + fixedY + 'px, 0)'); } - dom.setCssTransform(this.SVG_, ''); - this.previousSibling_ = null; -}; + /** + * Reports the surface translation in scaled workspace coordinates. + * Use this when finishing a drag to return blocks to the correct position. + * @return {!Coordinate} Current translation of the surface + * @package + */ + getSurfaceTranslation() { + return svgMath.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_)); + } + /** + * Move the blockCanvas and bubbleCanvas out of the surface SVG and on to + * newSurface. + * @param {SVGElement} newSurface The element to put the drag surface contents + * into. + * @package + */ + clearAndHide(newSurface) { + if (!newSurface) { + throw Error( + 'Couldn\'t clear and hide the drag surface: missing new surface.'); + } + const blockCanvas = /** @type {!Element} */ (this.SVG_.childNodes[0]); + const bubbleCanvas = /** @type {!Element} */ (this.SVG_.childNodes[1]); + if (!blockCanvas || !bubbleCanvas || + !dom.hasClass(blockCanvas, 'blocklyBlockCanvas') || + !dom.hasClass(bubbleCanvas, 'blocklyBubbleCanvas')) { + throw Error( + 'Couldn\'t clear and hide the drag surface. A node was missing.'); + } -/** - * Set the SVG to have the block canvas and bubble canvas in it and then - * show the surface. - * @param {!SVGElement} blockCanvas The block canvas element from the - * workspace. - * @param {!SVGElement} bubbleCanvas The element that contains the bubbles. - * @param {Element} previousSibling The element to insert the block canvas and - bubble canvas after when it goes back in the DOM at the end of a drag. - * @param {number} width The width of the workspace SVG element. - * @param {number} height The height of the workspace SVG element. - * @param {number} scale The scale of the workspace being dragged. - * @package - */ -WorkspaceDragSurfaceSvg.prototype.setContentsAndShow = function( - blockCanvas, bubbleCanvas, previousSibling, width, height, scale) { - if (this.SVG_.childNodes.length) { - throw Error('Already dragging a block.'); + // If there is a previous sibling, put the blockCanvas back right + // afterwards, otherwise insert it as the first child node in newSurface. + if (this.previousSibling_ !== null) { + dom.insertAfter(blockCanvas, this.previousSibling_); + } else { + newSurface.insertBefore(blockCanvas, newSurface.firstChild); + } + + // Reattach the bubble canvas after the blockCanvas. + dom.insertAfter(bubbleCanvas, blockCanvas); + // Hide the drag surface. + this.SVG_.style.display = 'none'; + if (this.SVG_.childNodes.length) { + throw Error('Drag surface was not cleared.'); + } + dom.setCssTransform(this.SVG_, ''); + this.previousSibling_ = null; } - this.previousSibling_ = previousSibling; - // Make sure the blocks and bubble canvas are scaled appropriately. - blockCanvas.setAttribute('transform', 'translate(0, 0) scale(' + scale + ')'); - bubbleCanvas.setAttribute( - 'transform', 'translate(0, 0) scale(' + scale + ')'); - this.SVG_.setAttribute('width', width); - this.SVG_.setAttribute('height', height); - this.SVG_.appendChild(blockCanvas); - this.SVG_.appendChild(bubbleCanvas); - this.SVG_.style.display = 'block'; -}; + /** + * Set the SVG to have the block canvas and bubble canvas in it and then + * show the surface. + * @param {!SVGElement} blockCanvas The block canvas element from the + * workspace. + * @param {!SVGElement} bubbleCanvas The element that contains the + bubbles. + * @param {Element} previousSibling The element to insert the block canvas and + bubble canvas after when it goes back in the DOM at the end of a drag. + * @param {number} width The width of the workspace SVG element. + * @param {number} height The height of the workspace SVG element. + * @param {number} scale The scale of the workspace being dragged. + * @package + */ + setContentsAndShow( + blockCanvas, bubbleCanvas, previousSibling, width, height, scale) { + if (this.SVG_.childNodes.length) { + throw Error('Already dragging a block.'); + } + this.previousSibling_ = previousSibling; + // Make sure the blocks and bubble canvas are scaled appropriately. + blockCanvas.setAttribute( + 'transform', 'translate(0, 0) scale(' + scale + ')'); + bubbleCanvas.setAttribute( + 'transform', 'translate(0, 0) scale(' + scale + ')'); + this.SVG_.setAttribute('width', width); + this.SVG_.setAttribute('height', height); + this.SVG_.appendChild(blockCanvas); + this.SVG_.appendChild(bubbleCanvas); + this.SVG_.style.display = 'block'; + } +} exports.WorkspaceDragSurfaceSvg = WorkspaceDragSurfaceSvg; diff --git a/core/workspace_dragger.js b/core/workspace_dragger.js index f888f3514..ce2e5ba26 100644 --- a/core/workspace_dragger.js +++ b/core/workspace_dragger.js @@ -27,90 +27,89 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg'); * Note that the workspace itself manages whether or not it has a drag surface * and how to do translations based on that. This simply passes the right * commands based on events. - * @param {!WorkspaceSvg} workspace The workspace to drag. - * @constructor * @alias Blockly.WorkspaceDragger */ -const WorkspaceDragger = function(workspace) { +class WorkspaceDragger { /** - * @type {!WorkspaceSvg} - * @private + * @param {!WorkspaceSvg} workspace The workspace to drag. */ - this.workspace_ = workspace; + constructor(workspace) { + /** + * @type {!WorkspaceSvg} + * @private + */ + this.workspace_ = workspace; - /** - * Whether horizontal scroll is enabled. - * @type {boolean} - * @private - */ - this.horizontalScrollEnabled_ = this.workspace_.isMovableHorizontally(); + /** + * Whether horizontal scroll is enabled. + * @type {boolean} + * @private + */ + this.horizontalScrollEnabled_ = this.workspace_.isMovableHorizontally(); - /** - * Whether vertical scroll is enabled. - * @type {boolean} - * @private - */ - this.verticalScrollEnabled_ = this.workspace_.isMovableVertically(); + /** + * Whether vertical scroll is enabled. + * @type {boolean} + * @private + */ + this.verticalScrollEnabled_ = this.workspace_.isMovableVertically(); - /** - * The scroll position of the workspace at the beginning of the drag. - * Coordinate system: pixel coordinates. - * @type {!Coordinate} - * @protected - */ - this.startScrollXY_ = new Coordinate(workspace.scrollX, workspace.scrollY); -}; - -/** - * Sever all links from this object. - * @package - * @suppress {checkTypes} - */ -WorkspaceDragger.prototype.dispose = function() { - this.workspace_ = null; -}; - -/** - * Start dragging the workspace. - * @package - */ -WorkspaceDragger.prototype.startDrag = function() { - if (common.getSelected()) { - common.getSelected().unselect(); + /** + * The scroll position of the workspace at the beginning of the drag. + * Coordinate system: pixel coordinates. + * @type {!Coordinate} + * @protected + */ + this.startScrollXY_ = new Coordinate(workspace.scrollX, workspace.scrollY); } - this.workspace_.setupDragSurface(); -}; - -/** - * Finish dragging the workspace and put everything back where it belongs. - * @param {!Coordinate} currentDragDeltaXY How far the pointer has - * moved from the position at the start of the drag, in pixel coordinates. - * @package - */ -WorkspaceDragger.prototype.endDrag = function(currentDragDeltaXY) { - // Make sure everything is up to date. - this.drag(currentDragDeltaXY); - this.workspace_.resetDragSurface(); -}; - -/** - * Move the workspace based on the most recent mouse movements. - * @param {!Coordinate} currentDragDeltaXY How far the pointer has - * moved from the position at the start of the drag, in pixel coordinates. - * @package - */ -WorkspaceDragger.prototype.drag = function(currentDragDeltaXY) { - const newXY = Coordinate.sum(this.startScrollXY_, currentDragDeltaXY); - - if (this.horizontalScrollEnabled_ && this.verticalScrollEnabled_) { - this.workspace_.scroll(newXY.x, newXY.y); - } else if (this.horizontalScrollEnabled_) { - this.workspace_.scroll(newXY.x, this.workspace_.scrollY); - } else if (this.verticalScrollEnabled_) { - this.workspace_.scroll(this.workspace_.scrollX, newXY.y); - } else { - throw new TypeError('Invalid state.'); + /** + * Sever all links from this object. + * @package + * @suppress {checkTypes} + */ + dispose() { + this.workspace_ = null; } -}; + /** + * Start dragging the workspace. + * @package + */ + startDrag() { + if (common.getSelected()) { + common.getSelected().unselect(); + } + this.workspace_.setupDragSurface(); + } + /** + * Finish dragging the workspace and put everything back where it belongs. + * @param {!Coordinate} currentDragDeltaXY How far the pointer has + * moved from the position at the start of the drag, in pixel coordinates. + * @package + */ + endDrag(currentDragDeltaXY) { + // Make sure everything is up to date. + this.drag(currentDragDeltaXY); + this.workspace_.resetDragSurface(); + } + /** + * Move the workspace based on the most recent mouse movements. + * @param {!Coordinate} currentDragDeltaXY How far the pointer has + * moved from the position at the start of the drag, in pixel coordinates. + * @package + */ + drag(currentDragDeltaXY) { + const newXY = Coordinate.sum(this.startScrollXY_, currentDragDeltaXY); + + if (this.horizontalScrollEnabled_ && this.verticalScrollEnabled_) { + this.workspace_.scroll(newXY.x, newXY.y); + } else if (this.horizontalScrollEnabled_) { + this.workspace_.scroll(newXY.x, this.workspace_.scrollY); + } else if (this.verticalScrollEnabled_) { + this.workspace_.scroll(this.workspace_.scrollX, newXY.y); + } else { + throw new TypeError('Invalid state.'); + } + } +} exports.WorkspaceDragger = WorkspaceDragger; diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 28b3c8eca..b13d0e56e 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -130,6 +130,278 @@ const WorkspaceSvg = function( options, opt_blockDragSurface, opt_wsDragSurface) { WorkspaceSvg.superClass_.constructor.call(this, options); + /** + * A wrapper function called when a resize event occurs. + * You can pass the result to `eventHandling.unbind`. + * @type {?browserEvents.Data} + * @private + */ + this.resizeHandlerWrapper_ = null; + + /** + * The render status of an SVG workspace. + * Returns `false` for headless workspaces and true for instances of + * `WorkspaceSvg`. + * @type {boolean} + */ + this.rendered = true; + + /** + * Whether the workspace is visible. False if the workspace has been hidden + * by calling `setVisible(false)`. + * @type {boolean} + * @private + */ + this.isVisible_ = true; + + /** + * Is this workspace the surface for a flyout? + * @type {boolean} + */ + this.isFlyout = false; + + /** + * Is this workspace the surface for a mutator? + * @type {boolean} + * @package + */ + this.isMutator = false; + + /** + * Whether this workspace has resizes enabled. + * Disable during batch operations for a performance improvement. + * @type {boolean} + * @private + */ + this.resizesEnabled_ = true; + + /** + * Current horizontal scrolling offset in pixel units, relative to the + * workspace origin. + * + * It is useful to think about a view, and a canvas moving beneath that + * view. As the canvas moves right, this value becomes more positive, and + * the view is now "seeing" the left side of the canvas. As the canvas moves + * left, this value becomes more negative, and the view is now "seeing" the + * right side of the canvas. + * + * The confusing thing about this value is that it does not, and must not + * include the absoluteLeft offset. This is because it is used to calculate + * the viewLeft value. + * + * The viewLeft is relative to the workspace origin (although in pixel + * units). The workspace origin is the top-left corner of the workspace (at + * least when it is enabled). It is shifted from the top-left of the + * blocklyDiv so as not to be beneath the toolbox. + * + * When the workspace is enabled the viewLeft and workspace origin are at + * the same X location. As the canvas slides towards the right beneath the + * view this value (scrollX) becomes more positive, and the viewLeft becomes + * more negative relative to the workspace origin (imagine the workspace + * origin as a dot on the canvas sliding to the right as the canvas moves). + * + * So if the scrollX were to include the absoluteLeft this would in a way + * "unshift" the workspace origin. This means that the viewLeft would be + * representing the left edge of the blocklyDiv, rather than the left edge + * of the workspace. + * + * @type {number} + */ + this.scrollX = 0; + + /** + * Current vertical scrolling offset in pixel units, relative to the + * workspace origin. + * + * It is useful to think about a view, and a canvas moving beneath that + * view. As the canvas moves down, this value becomes more positive, and the + * view is now "seeing" the upper part of the canvas. As the canvas moves + * up, this value becomes more negative, and the view is "seeing" the lower + * part of the canvas. + * + * This confusing thing about this value is that it does not, and must not + * include the absoluteTop offset. This is because it is used to calculate + * the viewTop value. + * + * The viewTop is relative to the workspace origin (although in pixel + * units). The workspace origin is the top-left corner of the workspace (at + * least when it is enabled). It is shifted from the top-left of the + * blocklyDiv so as not to be beneath the toolbox. + * + * When the workspace is enabled the viewTop and workspace origin are at the + * same Y location. As the canvas slides towards the bottom this value + * (scrollY) becomes more positive, and the viewTop becomes more negative + * relative to the workspace origin (image in the workspace origin as a dot + * on the canvas sliding downwards as the canvas moves). + * + * So if the scrollY were to include the absoluteTop this would in a way + * "unshift" the workspace origin. This means that the viewTop would be + * representing the top edge of the blocklyDiv, rather than the top edge of + * the workspace. + * + * @type {number} + */ + this.scrollY = 0; + + /** + * Horizontal scroll value when scrolling started in pixel units. + * @type {number} + */ + this.startScrollX = 0; + + /** + * Vertical scroll value when scrolling started in pixel units. + * @type {number} + */ + this.startScrollY = 0; + + /** + * Distance from mouse to object being dragged. + * @type {Coordinate} + * @private + */ + this.dragDeltaXY_ = null; + + /** + * Current scale. + * @type {number} + */ + this.scale = 1; + + /** + * Cached scale value. Used to detect changes in viewport. + * @type {number} + * @private + */ + this.oldScale_ = 1; + + /** + * Cached viewport top value. Used to detect changes in viewport. + * @type {number} + * @private + */ + this.oldTop_ = 0; + + /** + * Cached viewport left value. Used to detect changes in viewport. + * @type {number} + * @private + */ + this.oldLeft_ = 0; + + /** + * The workspace's trashcan (if any). + * @type {Trashcan} + */ + this.trashcan = null; + + /** + * This workspace's scrollbars, if they exist. + * @type {ScrollbarPair} + */ + this.scrollbar = null; + + /** + * Fixed flyout providing blocks which may be dragged into this workspace. + * @type {IFlyout} + * @private + */ + this.flyout_ = null; + + /** + * Category-based toolbox providing blocks which may be dragged into this + * workspace. + * @type {IToolbox} + * @private + */ + this.toolbox_ = null; + + /** + * The current gesture in progress on this workspace, if any. + * @type {TouchGesture} + * @private + */ + this.currentGesture_ = null; + + /** + * This workspace's surface for dragging blocks, if it exists. + * @type {BlockDragSurfaceSvg} + * @private + */ + this.blockDragSurface_ = null; + + /** + * This workspace's drag surface, if it exists. + * @type {WorkspaceDragSurfaceSvg} + * @private + */ + this.workspaceDragSurface_ = null; + + /** + * Whether to move workspace to the drag surface when it is dragged. + * True if it should move, false if it should be translated directly. + * @type {boolean} + * @private + */ + this.useWorkspaceDragSurface_ = false; + + /** + * Whether the drag surface is actively in use. When true, calls to + * translate will translate the drag surface instead of the translating the + * workspace directly. + * This is set to true in setupDragSurface and to false in resetDragSurface. + * @type {boolean} + * @private + */ + this.isDragSurfaceActive_ = false; + + /** + * The first parent div with 'injectionDiv' in the name, or null if not set. + * Access this with getInjectionDiv. + * @type {Element} + * @private + */ + this.injectionDiv_ = null; + + /** + * Last known position of the page scroll. + * This is used to determine whether we have recalculated screen coordinate + * stuff since the page scrolled. + * @type {Coordinate} + * @private + */ + this.lastRecordedPageScroll_ = null; + + /** + * Developers may define this function to add custom menu options to the + * workspace's context menu or edit the workspace-created set of menu options. + * @param {!Array} options List of menu options to add to. + * @param {!Event} e The right-click event that triggered the context menu. + */ + this.configureContextMenu; + + /** + * In a flyout, the target workspace where blocks should be placed after a + * drag. Otherwise null. + * @type {WorkspaceSvg} + * @package + */ + this.targetWorkspace = null; + + /** + * Inverted screen CTM, for use in mouseToSvg. + * @type {?SVGMatrix} + * @private + */ + this.inverseScreenCTM_ = null; + + /** + * Inverted screen CTM is dirty, recalculate it. + * @type {boolean} + * @private + */ + this.inverseScreenCTMDirty_ = true; + const MetricsManagerClass = registry.getClassFromOptions( registry.Type.METRICS_MANAGER, options, true); /** @@ -302,278 +574,6 @@ const WorkspaceSvg = function( }; object.inherits(WorkspaceSvg, Workspace); -/** - * A wrapper function called when a resize event occurs. - * You can pass the result to `eventHandling.unbind`. - * @type {?browserEvents.Data} - * @private - */ -WorkspaceSvg.prototype.resizeHandlerWrapper_ = null; - -/** - * The render status of an SVG workspace. - * Returns `false` for headless workspaces and true for instances of - * `WorkspaceSvg`. - * @type {boolean} - */ -WorkspaceSvg.prototype.rendered = true; - -/** - * Whether the workspace is visible. False if the workspace has been hidden - * by calling `setVisible(false)`. - * @type {boolean} - * @private - */ -WorkspaceSvg.prototype.isVisible_ = true; - -/** - * Is this workspace the surface for a flyout? - * @type {boolean} - */ -WorkspaceSvg.prototype.isFlyout = false; - -/** - * Is this workspace the surface for a mutator? - * @type {boolean} - * @package - */ -WorkspaceSvg.prototype.isMutator = false; - -/** - * Whether this workspace has resizes enabled. - * Disable during batch operations for a performance improvement. - * @type {boolean} - * @private - */ -WorkspaceSvg.prototype.resizesEnabled_ = true; - -/** - * Current horizontal scrolling offset in pixel units, relative to the - * workspace origin. - * - * It is useful to think about a view, and a canvas moving beneath that - * view. As the canvas moves right, this value becomes more positive, and - * the view is now "seeing" the left side of the canvas. As the canvas moves - * left, this value becomes more negative, and the view is now "seeing" the - * right side of the canvas. - * - * The confusing thing about this value is that it does not, and must not - * include the absoluteLeft offset. This is because it is used to calculate - * the viewLeft value. - * - * The viewLeft is relative to the workspace origin (although in pixel - * units). The workspace origin is the top-left corner of the workspace (at - * least when it is enabled). It is shifted from the top-left of the blocklyDiv - * so as not to be beneath the toolbox. - * - * When the workspace is enabled the viewLeft and workspace origin are at - * the same X location. As the canvas slides towards the right beneath the view - * this value (scrollX) becomes more positive, and the viewLeft becomes more - * negative relative to the workspace origin (imagine the workspace origin - * as a dot on the canvas sliding to the right as the canvas moves). - * - * So if the scrollX were to include the absoluteLeft this would in a way - * "unshift" the workspace origin. This means that the viewLeft would be - * representing the left edge of the blocklyDiv, rather than the left edge - * of the workspace. - * - * @type {number} - */ -WorkspaceSvg.prototype.scrollX = 0; - -/** - * Current vertical scrolling offset in pixel units, relative to the - * workspace origin. - * - * It is useful to think about a view, and a canvas moving beneath that - * view. As the canvas moves down, this value becomes more positive, and the - * view is now "seeing" the upper part of the canvas. As the canvas moves - * up, this value becomes more negative, and the view is "seeing" the lower - * part of the canvas. - * - * This confusing thing about this value is that it does not, and must not - * include the absoluteTop offset. This is because it is used to calculate - * the viewTop value. - * - * The viewTop is relative to the workspace origin (although in pixel - * units). The workspace origin is the top-left corner of the workspace (at - * least when it is enabled). It is shifted from the top-left of the - * blocklyDiv so as not to be beneath the toolbox. - * - * When the workspace is enabled the viewTop and workspace origin are at the - * same Y location. As the canvas slides towards the bottom this value - * (scrollY) becomes more positive, and the viewTop becomes more negative - * relative to the workspace origin (image in the workspace origin as a dot - * on the canvas sliding downwards as the canvas moves). - * - * So if the scrollY were to include the absoluteTop this would in a way - * "unshift" the workspace origin. This means that the viewTop would be - * representing the top edge of the blocklyDiv, rather than the top edge of - * the workspace. - * - * @type {number} - */ -WorkspaceSvg.prototype.scrollY = 0; - -/** - * Horizontal scroll value when scrolling started in pixel units. - * @type {number} - */ -WorkspaceSvg.prototype.startScrollX = 0; - -/** - * Vertical scroll value when scrolling started in pixel units. - * @type {number} - */ -WorkspaceSvg.prototype.startScrollY = 0; - -/** - * Distance from mouse to object being dragged. - * @type {Coordinate} - * @private - */ -WorkspaceSvg.prototype.dragDeltaXY_ = null; - -/** - * Current scale. - * @type {number} - */ -WorkspaceSvg.prototype.scale = 1; - -/** - * Cached scale value. Used to detect changes in viewport. - * @type {number} - * @private - */ -WorkspaceSvg.prototype.oldScale_ = 1; - -/** - * Cached viewport top value. Used to detect changes in viewport. - * @type {number} - * @private - */ -WorkspaceSvg.prototype.oldTop_ = 0; - -/** - * Cached viewport left value. Used to detect changes in viewport. - * @type {number} - * @private - */ -WorkspaceSvg.prototype.oldLeft_ = 0; - -/** - * The workspace's trashcan (if any). - * @type {Trashcan} - */ -WorkspaceSvg.prototype.trashcan = null; - -/** - * This workspace's scrollbars, if they exist. - * @type {ScrollbarPair} - */ -WorkspaceSvg.prototype.scrollbar = null; - -/** - * Fixed flyout providing blocks which may be dragged into this workspace. - * @type {IFlyout} - * @private - */ -WorkspaceSvg.prototype.flyout_ = null; - -/** - * Category-based toolbox providing blocks which may be dragged into this - * workspace. - * @type {IToolbox} - * @private - */ -WorkspaceSvg.prototype.toolbox_ = null; - -/** - * The current gesture in progress on this workspace, if any. - * @type {TouchGesture} - * @private - */ -WorkspaceSvg.prototype.currentGesture_ = null; - -/** - * This workspace's surface for dragging blocks, if it exists. - * @type {BlockDragSurfaceSvg} - * @private - */ -WorkspaceSvg.prototype.blockDragSurface_ = null; - -/** - * This workspace's drag surface, if it exists. - * @type {WorkspaceDragSurfaceSvg} - * @private - */ -WorkspaceSvg.prototype.workspaceDragSurface_ = null; - -/** - * Whether to move workspace to the drag surface when it is dragged. - * True if it should move, false if it should be translated directly. - * @type {boolean} - * @private - */ -WorkspaceSvg.prototype.useWorkspaceDragSurface_ = false; - -/** - * Whether the drag surface is actively in use. When true, calls to - * translate will translate the drag surface instead of the translating the - * workspace directly. - * This is set to true in setupDragSurface and to false in resetDragSurface. - * @type {boolean} - * @private - */ -WorkspaceSvg.prototype.isDragSurfaceActive_ = false; - -/** - * The first parent div with 'injectionDiv' in the name, or null if not set. - * Access this with getInjectionDiv. - * @type {Element} - * @private - */ -WorkspaceSvg.prototype.injectionDiv_ = null; - -/** - * Last known position of the page scroll. - * This is used to determine whether we have recalculated screen coordinate - * stuff since the page scrolled. - * @type {Coordinate} - * @private - */ -WorkspaceSvg.prototype.lastRecordedPageScroll_ = null; - -/** - * Developers may define this function to add custom menu options to the - * workspace's context menu or edit the workspace-created set of menu options. - * @param {!Array} options List of menu options to add to. - * @param {!Event} e The right-click event that triggered the context menu. - */ -WorkspaceSvg.prototype.configureContextMenu; - -/** - * In a flyout, the target workspace where blocks should be placed after a drag. - * Otherwise null. - * @type {WorkspaceSvg} - * @package - */ -WorkspaceSvg.prototype.targetWorkspace = null; - -/** - * Inverted screen CTM, for use in mouseToSvg. - * @type {?SVGMatrix} - * @private - */ -WorkspaceSvg.prototype.inverseScreenCTM_ = null; - -/** - * Inverted screen CTM is dirty, recalculate it. - * @type {boolean} - * @private - */ -WorkspaceSvg.prototype.inverseScreenCTMDirty_ = true; - /** * Get the marker manager for this workspace. * @return {!MarkerManager} The marker manager. diff --git a/core/zoom_controls.js b/core/zoom_controls.js index 28b2b5c32..3235a8e98 100644 --- a/core/zoom_controls.js +++ b/core/zoom_controls.js @@ -38,462 +38,454 @@ goog.require('Blockly.Events.Click'); /** * Class for a zoom controls. - * @param {!WorkspaceSvg} workspace The workspace to sit in. - * @constructor * @implements {IPositionable} * @alias Blockly.ZoomControls */ -const ZoomControls = function(workspace) { +class ZoomControls { /** - * @type {!WorkspaceSvg} - * @private + * @param {!WorkspaceSvg} workspace The workspace to sit in. */ - this.workspace_ = workspace; + constructor(workspace) { + /** + * @type {!WorkspaceSvg} + * @private + */ + this.workspace_ = workspace; + /** + * The unique id for this component that is used to register with the + * ComponentManager. + * @type {string} + */ + this.id = 'zoomControls'; + + /** + * A handle to use to unbind the mouse down event handler for zoom reset + * button. Opaque data returned from browserEvents.conditionalBind. + * @type {?browserEvents.Data} + * @private + */ + this.onZoomResetWrapper_ = null; + + /** + * A handle to use to unbind the mouse down event handler for zoom in + * button. Opaque data returned from browserEvents.conditionalBind. + * @type {?browserEvents.Data} + * @private + */ + this.onZoomInWrapper_ = null; + + /** + * A handle to use to unbind the mouse down event handler for zoom out + * button. Opaque data returned from browserEvents.conditionalBind. + * @type {?browserEvents.Data} + * @private + */ + this.onZoomOutWrapper_ = null; + + /** + * The zoom in svg element. + * @type {SVGGElement} + * @private + */ + this.zoomInGroup_ = null; + + /** + * The zoom out svg element. + * @type {SVGGElement} + * @private + */ + this.zoomOutGroup_ = null; + + /** + * The zoom reset svg element. + * @type {SVGGElement} + * @private + */ + this.zoomResetGroup_ = null; + + /** + * Width of the zoom controls. + * @type {number} + * @const + * @private + */ + this.WIDTH_ = 32; + + /** + * Height of each zoom control. + * @type {number} + * @const + * @private + */ + this.HEIGHT_ = 32; + + /** + * Small spacing used between the zoom in and out control, in pixels. + * @type {number} + * @const + * @private + */ + this.SMALL_SPACING_ = 2; + + /** + * Large spacing used between the zoom in and reset control, in pixels. + * @type {number} + * @const + * @private + */ + this.LARGE_SPACING_ = 11; + + /** + * Distance between zoom controls and bottom or top edge of workspace. + * @type {number} + * @const + * @private + */ + this.MARGIN_VERTICAL_ = 20; + + /** + * Distance between zoom controls and right or left edge of workspace. + * @type {number} + * @private + */ + this.MARGIN_HORIZONTAL_ = 20; + + /** + * The SVG group containing the zoom controls. + * @type {SVGElement} + * @private + */ + this.svgGroup_ = null; + + /** + * Left coordinate of the zoom controls. + * @type {number} + * @private + */ + this.left_ = 0; + + /** + * Top coordinate of the zoom controls. + * @type {number} + * @private + */ + this.top_ = 0; + + /** + * Whether this has been initialized. + * @type {boolean} + * @private + */ + this.initialized_ = false; + } /** - * The unique id for this component that is used to register with the - * ComponentManager. - * @type {string} + * Create the zoom controls. + * @return {!SVGElement} The zoom controls SVG group. */ - this.id = 'zoomControls'; + createDom() { + this.svgGroup_ = dom.createSvgElement(Svg.G, {}, null); - /** - * A handle to use to unbind the mouse down event handler for zoom reset - * button. Opaque data returned from browserEvents.conditionalBind. - * @type {?browserEvents.Data} - * @private - */ - this.onZoomResetWrapper_ = null; - - /** - * A handle to use to unbind the mouse down event handler for zoom in button. - * Opaque data returned from browserEvents.conditionalBind. - * @type {?browserEvents.Data} - * @private - */ - this.onZoomInWrapper_ = null; - - /** - * A handle to use to unbind the mouse down event handler for zoom out button. - * Opaque data returned from browserEvents.conditionalBind. - * @type {?browserEvents.Data} - * @private - */ - this.onZoomOutWrapper_ = null; - - /** - * The zoom in svg element. - * @type {SVGGElement} - * @private - */ - this.zoomInGroup_ = null; - - /** - * The zoom out svg element. - * @type {SVGGElement} - * @private - */ - this.zoomOutGroup_ = null; - - /** - * The zoom reset svg element. - * @type {SVGGElement} - * @private - */ - this.zoomResetGroup_ = null; -}; - -/** - * Width of the zoom controls. - * @type {number} - * @const - * @private - */ -ZoomControls.prototype.WIDTH_ = 32; - -/** - * Height of each zoom control. - * @type {number} - * @const - * @private - */ -ZoomControls.prototype.HEIGHT_ = 32; - -/** - * Small spacing used between the zoom in and out control, in pixels. - * @type {number} - * @const - * @private - */ -ZoomControls.prototype.SMALL_SPACING_ = 2; - -/** - * Large spacing used between the zoom in and reset control, in pixels. - * @type {number} - * @const - * @private - */ -ZoomControls.prototype.LARGE_SPACING_ = 11; - -/** - * Distance between zoom controls and bottom or top edge of workspace. - * @type {number} - * @const - * @private - */ -ZoomControls.prototype.MARGIN_VERTICAL_ = 20; - -/** - * Distance between zoom controls and right or left edge of workspace. - * @type {number} - * @private - */ -ZoomControls.prototype.MARGIN_HORIZONTAL_ = 20; - -/** - * The SVG group containing the zoom controls. - * @type {SVGElement} - * @private - */ -ZoomControls.prototype.svgGroup_ = null; - -/** - * Left coordinate of the zoom controls. - * @type {number} - * @private - */ -ZoomControls.prototype.left_ = 0; - -/** - * Top coordinate of the zoom controls. - * @type {number} - * @private - */ -ZoomControls.prototype.top_ = 0; - -/** - * Whether this has been initialized. - * @type {boolean} - * @private - */ -ZoomControls.prototype.initialized_ = false; - -/** - * Create the zoom controls. - * @return {!SVGElement} The zoom controls SVG group. - */ -ZoomControls.prototype.createDom = function() { - this.svgGroup_ = dom.createSvgElement(Svg.G, {}, null); - - // Each filter/pattern needs a unique ID for the case of multiple Blockly - // instances on a page. Browser behaviour becomes undefined otherwise. - // https://neil.fraser.name/news/2015/11/01/ - const rnd = String(Math.random()).substring(2); - this.createZoomOutSvg_(rnd); - this.createZoomInSvg_(rnd); - if (this.workspace_.isMovable()) { - // If we zoom to the center and the workspace isn't movable we could - // loose blocks at the edges of the workspace. - this.createZoomResetSvg_(rnd); - } - return this.svgGroup_; -}; - -/** - * Initializes the zoom controls. - */ -ZoomControls.prototype.init = function() { - this.workspace_.getComponentManager().addComponent({ - component: this, - weight: 2, - capabilities: [ComponentManager.Capability.POSITIONABLE], - }); - this.initialized_ = true; -}; - -/** - * Disposes of this zoom controls. - * Unlink from all DOM elements to prevent memory leaks. - */ -ZoomControls.prototype.dispose = function() { - this.workspace_.getComponentManager().removeComponent('zoomControls'); - if (this.svgGroup_) { - dom.removeNode(this.svgGroup_); - } - if (this.onZoomResetWrapper_) { - browserEvents.unbind(this.onZoomResetWrapper_); - } - if (this.onZoomInWrapper_) { - browserEvents.unbind(this.onZoomInWrapper_); - } - if (this.onZoomOutWrapper_) { - browserEvents.unbind(this.onZoomOutWrapper_); - } -}; - -/** - * Returns the bounding rectangle of the UI element in pixel units relative to - * the Blockly injection div. - * @return {?Rect} The UI elements's bounding box. Null if - * bounding box should be ignored by other UI elements. - */ -ZoomControls.prototype.getBoundingRectangle = function() { - let height = this.SMALL_SPACING_ + 2 * this.HEIGHT_; - if (this.zoomResetGroup_) { - height += this.LARGE_SPACING_ + this.HEIGHT_; - } - const bottom = this.top_ + height; - const right = this.left_ + this.WIDTH_; - return new Rect(this.top_, bottom, this.left_, right); -}; - - -/** - * Positions the zoom controls. - * It is positioned in the opposite corner to the corner the - * categories/toolbox starts at. - * @param {!MetricsManager.UiMetrics} metrics The workspace metrics. - * @param {!Array} savedPositions List of rectangles that - * are already on the workspace. - */ -ZoomControls.prototype.position = function(metrics, savedPositions) { - // Not yet initialized. - if (!this.initialized_) { - return; - } - - const cornerPosition = - uiPosition.getCornerOppositeToolbox(this.workspace_, metrics); - let height = this.SMALL_SPACING_ + 2 * this.HEIGHT_; - if (this.zoomResetGroup_) { - height += this.LARGE_SPACING_ + this.HEIGHT_; - } - const startRect = uiPosition.getStartPositionRect( - cornerPosition, new Size(this.WIDTH_, height), this.MARGIN_HORIZONTAL_, - this.MARGIN_VERTICAL_, metrics, this.workspace_); - - const verticalPosition = cornerPosition.vertical; - const bumpDirection = verticalPosition === uiPosition.verticalPosition.TOP ? - uiPosition.bumpDirection.DOWN : - uiPosition.bumpDirection.UP; - const positionRect = uiPosition.bumpPositionRect( - startRect, this.MARGIN_VERTICAL_, bumpDirection, savedPositions); - - if (verticalPosition === uiPosition.verticalPosition.TOP) { - const zoomInTranslateY = this.SMALL_SPACING_ + this.HEIGHT_; - this.zoomInGroup_.setAttribute( - 'transform', 'translate(0, ' + zoomInTranslateY + ')'); - if (this.zoomResetGroup_) { - const zoomResetTranslateY = - zoomInTranslateY + this.LARGE_SPACING_ + this.HEIGHT_; - this.zoomResetGroup_.setAttribute( - 'transform', 'translate(0, ' + zoomResetTranslateY + ')'); + // Each filter/pattern needs a unique ID for the case of multiple Blockly + // instances on a page. Browser behaviour becomes undefined otherwise. + // https://neil.fraser.name/news/2015/11/01/ + const rnd = String(Math.random()).substring(2); + this.createZoomOutSvg_(rnd); + this.createZoomInSvg_(rnd); + if (this.workspace_.isMovable()) { + // If we zoom to the center and the workspace isn't movable we could + // loose blocks at the edges of the workspace. + this.createZoomResetSvg_(rnd); } - } else { - const zoomInTranslateY = - this.zoomResetGroup_ ? this.LARGE_SPACING_ + this.HEIGHT_ : 0; - this.zoomInGroup_.setAttribute( - 'transform', 'translate(0, ' + zoomInTranslateY + ')'); - const zoomOutTranslateY = - zoomInTranslateY + this.SMALL_SPACING_ + this.HEIGHT_; - this.zoomOutGroup_.setAttribute( - 'transform', 'translate(0, ' + zoomOutTranslateY + ')'); + return this.svgGroup_; } + /** + * Initializes the zoom controls. + */ + init() { + this.workspace_.getComponentManager().addComponent({ + component: this, + weight: 2, + capabilities: [ComponentManager.Capability.POSITIONABLE], + }); + this.initialized_ = true; + } + /** + * Disposes of this zoom controls. + * Unlink from all DOM elements to prevent memory leaks. + */ + dispose() { + this.workspace_.getComponentManager().removeComponent('zoomControls'); + if (this.svgGroup_) { + dom.removeNode(this.svgGroup_); + } + if (this.onZoomResetWrapper_) { + browserEvents.unbind(this.onZoomResetWrapper_); + } + if (this.onZoomInWrapper_) { + browserEvents.unbind(this.onZoomInWrapper_); + } + if (this.onZoomOutWrapper_) { + browserEvents.unbind(this.onZoomOutWrapper_); + } + } + /** + * Returns the bounding rectangle of the UI element in pixel units relative to + * the Blockly injection div. + * @return {?Rect} The UI elements's bounding box. Null if + * bounding box should be ignored by other UI elements. + */ + getBoundingRectangle() { + let height = this.SMALL_SPACING_ + 2 * this.HEIGHT_; + if (this.zoomResetGroup_) { + height += this.LARGE_SPACING_ + this.HEIGHT_; + } + const bottom = this.top_ + height; + const right = this.left_ + this.WIDTH_; + return new Rect(this.top_, bottom, this.left_, right); + } + /** + * Positions the zoom controls. + * It is positioned in the opposite corner to the corner the + * categories/toolbox starts at. + * @param {!MetricsManager.UiMetrics} metrics The workspace metrics. + * @param {!Array} savedPositions List of rectangles that + * are already on the workspace. + */ + position(metrics, savedPositions) { + // Not yet initialized. + if (!this.initialized_) { + return; + } - this.top_ = positionRect.top; - this.left_ = positionRect.left; - this.svgGroup_.setAttribute( - 'transform', 'translate(' + this.left_ + ',' + this.top_ + ')'); -}; + const cornerPosition = + uiPosition.getCornerOppositeToolbox(this.workspace_, metrics); + let height = this.SMALL_SPACING_ + 2 * this.HEIGHT_; + if (this.zoomResetGroup_) { + height += this.LARGE_SPACING_ + this.HEIGHT_; + } + const startRect = uiPosition.getStartPositionRect( + cornerPosition, new Size(this.WIDTH_, height), this.MARGIN_HORIZONTAL_, + this.MARGIN_VERTICAL_, metrics, this.workspace_); -/** - * Create the zoom in icon and its event handler. - * @param {string} rnd The random string to use as a suffix in the clip path's - * ID. These IDs must be unique in case there are multiple Blockly - * instances on the same page. - * @private - */ -ZoomControls.prototype.createZoomOutSvg_ = function(rnd) { - /* This markup will be generated and added to the .svgGroup_: - - - - - */ - this.zoomOutGroup_ = - dom.createSvgElement(Svg.G, {'class': 'blocklyZoom'}, this.svgGroup_); - const clip = dom.createSvgElement( - Svg.CLIPPATH, {'id': 'blocklyZoomoutClipPath' + rnd}, this.zoomOutGroup_); - dom.createSvgElement( - Svg.RECT, { - 'width': 32, - 'height': 32, - }, - clip); - const zoomoutSvg = dom.createSvgElement( - Svg.IMAGE, { - 'width': internalConstants.SPRITE.width, - 'height': internalConstants.SPRITE.height, - 'x': -64, - 'y': -92, - 'clip-path': 'url(#blocklyZoomoutClipPath' + rnd + ')', - }, - this.zoomOutGroup_); - zoomoutSvg.setAttributeNS( - dom.XLINK_NS, 'xlink:href', - this.workspace_.options.pathToMedia + internalConstants.SPRITE.url); + const verticalPosition = cornerPosition.vertical; + const bumpDirection = verticalPosition === uiPosition.verticalPosition.TOP ? + uiPosition.bumpDirection.DOWN : + uiPosition.bumpDirection.UP; + const positionRect = uiPosition.bumpPositionRect( + startRect, this.MARGIN_VERTICAL_, bumpDirection, savedPositions); - // Attach listener. - this.onZoomOutWrapper_ = browserEvents.conditionalBind( - this.zoomOutGroup_, 'mousedown', null, this.zoom_.bind(this, -1)); -}; + if (verticalPosition === uiPosition.verticalPosition.TOP) { + const zoomInTranslateY = this.SMALL_SPACING_ + this.HEIGHT_; + this.zoomInGroup_.setAttribute( + 'transform', 'translate(0, ' + zoomInTranslateY + ')'); + if (this.zoomResetGroup_) { + const zoomResetTranslateY = + zoomInTranslateY + this.LARGE_SPACING_ + this.HEIGHT_; + this.zoomResetGroup_.setAttribute( + 'transform', 'translate(0, ' + zoomResetTranslateY + ')'); + } + } else { + const zoomInTranslateY = + this.zoomResetGroup_ ? this.LARGE_SPACING_ + this.HEIGHT_ : 0; + this.zoomInGroup_.setAttribute( + 'transform', 'translate(0, ' + zoomInTranslateY + ')'); + const zoomOutTranslateY = + zoomInTranslateY + this.SMALL_SPACING_ + this.HEIGHT_; + this.zoomOutGroup_.setAttribute( + 'transform', 'translate(0, ' + zoomOutTranslateY + ')'); + } -/** - * Create the zoom out icon and its event handler. - * @param {string} rnd The random string to use as a suffix in the clip path's - * ID. These IDs must be unique in case there are multiple Blockly - * instances on the same page. - * @private - */ -ZoomControls.prototype.createZoomInSvg_ = function(rnd) { - /* This markup will be generated and added to the .svgGroup_: - - - - - - - */ - this.zoomInGroup_ = - dom.createSvgElement(Svg.G, {'class': 'blocklyZoom'}, this.svgGroup_); - const clip = dom.createSvgElement( - Svg.CLIPPATH, {'id': 'blocklyZoominClipPath' + rnd}, this.zoomInGroup_); - dom.createSvgElement( - Svg.RECT, { - 'width': 32, - 'height': 32, - }, - clip); - const zoominSvg = dom.createSvgElement( - Svg.IMAGE, { - 'width': internalConstants.SPRITE.width, - 'height': internalConstants.SPRITE.height, - 'x': -32, - 'y': -92, - 'clip-path': 'url(#blocklyZoominClipPath' + rnd + ')', - }, - this.zoomInGroup_); - zoominSvg.setAttributeNS( - dom.XLINK_NS, 'xlink:href', - this.workspace_.options.pathToMedia + internalConstants.SPRITE.url); + this.top_ = positionRect.top; + this.left_ = positionRect.left; + this.svgGroup_.setAttribute( + 'transform', 'translate(' + this.left_ + ',' + this.top_ + ')'); + } + /** + * Create the zoom in icon and its event handler. + * @param {string} rnd The random string to use as a suffix in the clip path's + * ID. These IDs must be unique in case there are multiple Blockly + * instances on the same page. + * @private + */ + createZoomOutSvg_(rnd) { + /* This markup will be generated and added to the .svgGroup_: + + + + + */ + this.zoomOutGroup_ = + dom.createSvgElement(Svg.G, {'class': 'blocklyZoom'}, this.svgGroup_); + const clip = dom.createSvgElement( + Svg.CLIPPATH, {'id': 'blocklyZoomoutClipPath' + rnd}, + this.zoomOutGroup_); + dom.createSvgElement( + Svg.RECT, { + 'width': 32, + 'height': 32, + }, + clip); + const zoomoutSvg = dom.createSvgElement( + Svg.IMAGE, { + 'width': internalConstants.SPRITE.width, + 'height': internalConstants.SPRITE.height, + 'x': -64, + 'y': -92, + 'clip-path': 'url(#blocklyZoomoutClipPath' + rnd + ')', + }, + this.zoomOutGroup_); + zoomoutSvg.setAttributeNS( + dom.XLINK_NS, 'xlink:href', + this.workspace_.options.pathToMedia + internalConstants.SPRITE.url); - // Attach listener. - this.onZoomInWrapper_ = browserEvents.conditionalBind( - this.zoomInGroup_, 'mousedown', null, this.zoom_.bind(this, 1)); -}; + // Attach listener. + this.onZoomOutWrapper_ = browserEvents.conditionalBind( + this.zoomOutGroup_, 'mousedown', null, this.zoom_.bind(this, -1)); + } + /** + * Create the zoom out icon and its event handler. + * @param {string} rnd The random string to use as a suffix in the clip path's + * ID. These IDs must be unique in case there are multiple Blockly + * instances on the same page. + * @private + */ + createZoomInSvg_(rnd) { + /* This markup will be generated and added to the .svgGroup_: + + + + + + + */ + this.zoomInGroup_ = + dom.createSvgElement(Svg.G, {'class': 'blocklyZoom'}, this.svgGroup_); + const clip = dom.createSvgElement( + Svg.CLIPPATH, {'id': 'blocklyZoominClipPath' + rnd}, this.zoomInGroup_); + dom.createSvgElement( + Svg.RECT, { + 'width': 32, + 'height': 32, + }, + clip); + const zoominSvg = dom.createSvgElement( + Svg.IMAGE, { + 'width': internalConstants.SPRITE.width, + 'height': internalConstants.SPRITE.height, + 'x': -32, + 'y': -92, + 'clip-path': 'url(#blocklyZoominClipPath' + rnd + ')', + }, + this.zoomInGroup_); + zoominSvg.setAttributeNS( + dom.XLINK_NS, 'xlink:href', + this.workspace_.options.pathToMedia + internalConstants.SPRITE.url); -/** - * Handles a mouse down event on the zoom in or zoom out buttons on the - * workspace. - * @param {number} amount Amount of zooming. Negative amount values zoom out, - * and positive amount values zoom in. - * @param {!Event} e A mouse down event. - * @private - */ -ZoomControls.prototype.zoom_ = function(amount, e) { - this.workspace_.markFocused(); - this.workspace_.zoomCenter(amount); - this.fireZoomEvent_(); - Touch.clearTouchIdentifier(); // Don't block future drags. - e.stopPropagation(); // Don't start a workspace scroll. - e.preventDefault(); // Stop double-clicking from selecting text. -}; + // Attach listener. + this.onZoomInWrapper_ = browserEvents.conditionalBind( + this.zoomInGroup_, 'mousedown', null, this.zoom_.bind(this, 1)); + } + /** + * Handles a mouse down event on the zoom in or zoom out buttons on the + * workspace. + * @param {number} amount Amount of zooming. Negative amount values zoom out, + * and positive amount values zoom in. + * @param {!Event} e A mouse down event. + * @private + */ + zoom_(amount, e) { + this.workspace_.markFocused(); + this.workspace_.zoomCenter(amount); + this.fireZoomEvent_(); + Touch.clearTouchIdentifier(); // Don't block future drags. + e.stopPropagation(); // Don't start a workspace scroll. + e.preventDefault(); // Stop double-clicking from selecting text. + } + /** + * Create the zoom reset icon and its event handler. + * @param {string} rnd The random string to use as a suffix in the clip path's + * ID. These IDs must be unique in case there are multiple Blockly + * instances on the same page. + * @private + */ + createZoomResetSvg_(rnd) { + /* This markup will be generated and added to the .svgGroup_: + + + + + + + */ + this.zoomResetGroup_ = + dom.createSvgElement(Svg.G, {'class': 'blocklyZoom'}, this.svgGroup_); + const clip = dom.createSvgElement( + Svg.CLIPPATH, {'id': 'blocklyZoomresetClipPath' + rnd}, + this.zoomResetGroup_); + dom.createSvgElement(Svg.RECT, {'width': 32, 'height': 32}, clip); + const zoomresetSvg = dom.createSvgElement( + Svg.IMAGE, { + 'width': internalConstants.SPRITE.width, + 'height': internalConstants.SPRITE.height, + 'y': -92, + 'clip-path': 'url(#blocklyZoomresetClipPath' + rnd + ')', + }, + this.zoomResetGroup_); + zoomresetSvg.setAttributeNS( + dom.XLINK_NS, 'xlink:href', + this.workspace_.options.pathToMedia + internalConstants.SPRITE.url); -/** - * Create the zoom reset icon and its event handler. - * @param {string} rnd The random string to use as a suffix in the clip path's - * ID. These IDs must be unique in case there are multiple Blockly - * instances on the same page. - * @private - */ -ZoomControls.prototype.createZoomResetSvg_ = function(rnd) { - /* This markup will be generated and added to the .svgGroup_: - - - - - - - */ - this.zoomResetGroup_ = - dom.createSvgElement(Svg.G, {'class': 'blocklyZoom'}, this.svgGroup_); - const clip = dom.createSvgElement( - Svg.CLIPPATH, {'id': 'blocklyZoomresetClipPath' + rnd}, - this.zoomResetGroup_); - dom.createSvgElement(Svg.RECT, {'width': 32, 'height': 32}, clip); - const zoomresetSvg = dom.createSvgElement( - Svg.IMAGE, { - 'width': internalConstants.SPRITE.width, - 'height': internalConstants.SPRITE.height, - 'y': -92, - 'clip-path': 'url(#blocklyZoomresetClipPath' + rnd + ')', - }, - this.zoomResetGroup_); - zoomresetSvg.setAttributeNS( - dom.XLINK_NS, 'xlink:href', - this.workspace_.options.pathToMedia + internalConstants.SPRITE.url); + // Attach event listeners. + this.onZoomResetWrapper_ = browserEvents.conditionalBind( + this.zoomResetGroup_, 'mousedown', null, this.resetZoom_.bind(this)); + } + /** + * Handles a mouse down event on the reset zoom button on the workspace. + * @param {!Event} e A mouse down event. + * @private + */ + resetZoom_(e) { + this.workspace_.markFocused(); - // Attach event listeners. - this.onZoomResetWrapper_ = browserEvents.conditionalBind( - this.zoomResetGroup_, 'mousedown', null, this.resetZoom_.bind(this)); -}; + // zoom is passed amount and computes the new scale using the formula: + // targetScale = currentScale * Math.pow(speed, amount) + const targetScale = this.workspace_.options.zoomOptions.startScale; + const currentScale = this.workspace_.scale; + const speed = this.workspace_.options.zoomOptions.scaleSpeed; + // To compute amount: + // amount = log(speed, (targetScale / currentScale)) + // Math.log computes natural logarithm (ln), to change the base, use + // formula: log(base, value) = ln(value) / ln(base) + const amount = Math.log(targetScale / currentScale) / Math.log(speed); + this.workspace_.beginCanvasTransition(); + this.workspace_.zoomCenter(amount); + this.workspace_.scrollCenter(); -/** - * Handles a mouse down event on the reset zoom button on the workspace. - * @param {!Event} e A mouse down event. - * @private - */ -ZoomControls.prototype.resetZoom_ = function(e) { - this.workspace_.markFocused(); - - // zoom is passed amount and computes the new scale using the formula: - // targetScale = currentScale * Math.pow(speed, amount) - const targetScale = this.workspace_.options.zoomOptions.startScale; - const currentScale = this.workspace_.scale; - const speed = this.workspace_.options.zoomOptions.scaleSpeed; - // To compute amount: - // amount = log(speed, (targetScale / currentScale)) - // Math.log computes natural logarithm (ln), to change the base, use formula: - // log(base, value) = ln(value) / ln(base) - const amount = Math.log(targetScale / currentScale) / Math.log(speed); - this.workspace_.beginCanvasTransition(); - this.workspace_.zoomCenter(amount); - this.workspace_.scrollCenter(); - - setTimeout(this.workspace_.endCanvasTransition.bind(this.workspace_), 500); - this.fireZoomEvent_(); - Touch.clearTouchIdentifier(); // Don't block future drags. - e.stopPropagation(); // Don't start a workspace scroll. - e.preventDefault(); // Stop double-clicking from selecting text. -}; - -/** - * Fires a zoom control UI event. - * @private - */ -ZoomControls.prototype.fireZoomEvent_ = function() { - const uiEvent = new (eventUtils.get(eventUtils.CLICK))( - null, this.workspace_.id, 'zoom_controls'); - eventUtils.fire(uiEvent); -}; + setTimeout(this.workspace_.endCanvasTransition.bind(this.workspace_), 500); + this.fireZoomEvent_(); + Touch.clearTouchIdentifier(); // Don't block future drags. + e.stopPropagation(); // Don't start a workspace scroll. + e.preventDefault(); // Stop double-clicking from selecting text. + } + /** + * Fires a zoom control UI event. + * @private + */ + fireZoomEvent_() { + const uiEvent = new (eventUtils.get(eventUtils.CLICK))( + null, this.workspace_.id, 'zoom_controls'); + eventUtils.fire(uiEvent); + } +} /** * CSS for zoom controls. See css.js for use. diff --git a/scripts/gulpfiles/chunks.json b/scripts/gulpfiles/chunks.json index f63efe1db..a7fcc3cbf 100644 --- a/scripts/gulpfiles/chunks.json +++ b/scripts/gulpfiles/chunks.json @@ -244,10 +244,10 @@ "./core/serialization/priorities.js", "./core/serialization/blocks.js", "./core/utils/toolbox.js", - "./core/mutator.js", - "./core/msg.js", "./core/connection_type.js", "./core/internal_constants.js", + "./core/mutator.js", + "./core/msg.js", "./core/utils/colour.js", "./core/utils/parsing.js", "./core/extensions.js",