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
This commit is contained in:
Rachel Fenichel
2022-01-06 13:13:40 -08:00
committed by GitHub
parent 69642f74ea
commit df2eafb8dd
24 changed files with 2242 additions and 2252 deletions

View File

@@ -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<string>}
*/
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<string>}
*/
Block.prototype.getDeveloperVariables;
/**
* Dispose of this block.
* @param {boolean} healStack If true, then try to heal any gap by connecting

View File

@@ -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.

View File

@@ -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<!Object>)}
*/
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<string, number>}
* @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<string, number>}
* @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<!Object>)}
*/
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<!Icon>} List of icons.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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<!Array<number>>}
*/
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<!Array<number>>}
*/
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.

View File

@@ -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

View File

@@ -69,20 +69,21 @@ const Input = function(type, name, block, connection) {
this.connection = connection;
/** @type {!Array<!Field>} */
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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -21,65 +21,157 @@ const registry = goog.require('Blockly.registry');
/**
* Class for a theme.
* @param {string} name Theme name.
* @param {!Object<string, Theme.BlockStyle>=} opt_blockStyles A map
* from style names (strings) to objects with style attributes for blocks.
* @param {!Object<string, Theme.CategoryStyle>=} 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<string, Theme.BlockStyle>=} opt_blockStyles A map
* from style names (strings) to objects with style attributes for blocks.
* @param {!Object<string, Theme.CategoryStyle>=} 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<string, !Theme.BlockStyle>}
* @package
*/
this.blockStyles = opt_blockStyles || Object.create(null);
/**
* The category styles map.
* @type {!Object<string, Theme.CategoryStyle>}
* @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<string, !Theme.BlockStyle>}
* 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<string, Theme.CategoryStyle>}
* @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;

View File

@@ -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.

View File

@@ -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<string, !Array<VariableModel>>}
* @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<string, !Array<VariableModel>>}
* @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<!Block>} 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<!Block>} 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<!Block>} 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<!Block>} 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<!Block>} 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<!Block>} 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<!VariableModel>} 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<string>} 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<!VariableModel>} 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<string>} 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<!Block>} 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<!VariableModel>} 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<string>} 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<!VariableModel>} 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<string>} 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<!Block>} 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;

View File

@@ -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;

View File

@@ -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.

View File

@@ -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<!ConnectionDB>}
*/
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<!ConnectionDB>}
*/
Workspace.prototype.connectionDBList = null;
/**
* Dispose of this workspace.
* Unlink from all DOM elements to prevent memory leaks.

View File

@@ -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<string>} 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<string>} 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;

View File

@@ -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.
* <svg class="blocklyWsDragSurface" style=transform:translate3d(...)>
* <g class="blocklyBlockCanvas"></g>
* <g class="blocklyBubbleCanvas">/g>
* </svg>
* @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.
* <svg class="blocklyWsDragSurface" style=transform:translate3d(...)>
* <g class="blocklyBlockCanvas"></g>
* <g class="blocklyBubbleCanvas">/g>
* </svg>
*/
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 <g> element from the
* workspace.
* @param {!SVGElement} bubbleCanvas The <g> 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 <g> element from the
* workspace.
* @param {!SVGElement} bubbleCanvas The <g> 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;

View File

@@ -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;

View File

@@ -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<!Object>} 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<!Object>} 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.

View File

@@ -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 <g> element.
* @type {SVGGElement}
* @private
*/
this.zoomInGroup_ = null;
/**
* The zoom out svg <g> element.
* @type {SVGGElement}
* @private
*/
this.zoomOutGroup_ = null;
/**
* The zoom reset svg <g> 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 <g> element.
* @type {SVGGElement}
* @private
*/
this.zoomInGroup_ = null;
/**
* The zoom out svg <g> element.
* @type {SVGGElement}
* @private
*/
this.zoomOutGroup_ = null;
/**
* The zoom reset svg <g> 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<!Rect>} 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<!Rect>} 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_:
<g class="blocklyZoom">
<clipPath id="blocklyZoomoutClipPath837493">
<rect width="32" height="32></rect>
</clipPath>
<image width="96" height="124" x="-64" y="-92"
xlink:href="media/sprites.png"
clip-path="url(#blocklyZoomoutClipPath837493)"></image>
</g>
*/
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_:
<g class="blocklyZoom">
<clipPath id="blocklyZoominClipPath837493">
<rect width="32" height="32"></rect>
</clipPath>
<image width="96" height="124" x="-32" y="-92"
xlink:href="media/sprites.png"
clip-path="url(#blocklyZoominClipPath837493)"></image>
</g>
*/
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_:
<g class="blocklyZoom">
<clipPath id="blocklyZoomoutClipPath837493">
<rect width="32" height="32></rect>
</clipPath>
<image width="96" height="124" x="-64" y="-92"
xlink:href="media/sprites.png"
clip-path="url(#blocklyZoomoutClipPath837493)"></image>
</g>
*/
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_:
<g class="blocklyZoom">
<clipPath id="blocklyZoominClipPath837493">
<rect width="32" height="32"></rect>
</clipPath>
<image width="96" height="124" x="-32" y="-92"
xlink:href="media/sprites.png"
clip-path="url(#blocklyZoominClipPath837493)"></image>
</g>
*/
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_:
<g class="blocklyZoom">
<clipPath id="blocklyZoomresetClipPath837493">
<rect width="32" height="32"></rect>
</clipPath>
<image width="96" height="124" x="-32" y="-92"
xlink:href="media/sprites.png"
clip-path="url(#blocklyZoomresetClipPath837493)"></image>
</g>
*/
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_:
<g class="blocklyZoom">
<clipPath id="blocklyZoomresetClipPath837493">
<rect width="32" height="32"></rect>
</clipPath>
<image width="96" height="124" x="-32" y="-92"
xlink:href="media/sprites.png"
clip-path="url(#blocklyZoomresetClipPath837493)"></image>
</g>
*/
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.

View File

@@ -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",