diff --git a/core/block_svg.js b/core/block_svg.js
index 5fd46d8a7..832659136 100644
--- a/core/block_svg.js
+++ b/core/block_svg.js
@@ -1423,14 +1423,17 @@ BlockSvg.prototype.appendInput_ = function(type, name) {
*/
BlockSvg.prototype.setConnectionTracking = function(track) {
if (this.previousConnection) {
- this.previousConnection.setTracking(track);
+ /** @type {!RenderedConnection} */ (this.previousConnection)
+ .setTracking(track);
}
if (this.outputConnection) {
- this.outputConnection.setTracking(track);
+ /** @type {!RenderedConnection} */ (this.outputConnection)
+ .setTracking(track);
}
if (this.nextConnection) {
- this.nextConnection.setTracking(track);
- const child = this.nextConnection.targetBlock();
+ /** @type {!RenderedConnection} */ (this.nextConnection).setTracking(track);
+ const child =
+ /** @type {!RenderedConnection} */ (this.nextConnection).targetBlock();
if (child) {
child.setConnectionTracking(track);
}
@@ -1444,7 +1447,8 @@ BlockSvg.prototype.setConnectionTracking = function(track) {
}
for (let i = 0; i < this.inputList.length; i++) {
- const conn = this.inputList[i].connection;
+ const conn =
+ /** @type {!RenderedConnection} */ (this.inputList[i].connection);
if (conn) {
conn.setTracking(track);
@@ -1547,23 +1551,26 @@ BlockSvg.prototype.bumpNeighbours = function() {
// Loop through every connection on this block.
const myConnections = this.getConnections_(false);
for (let i = 0, connection; (connection = myConnections[i]); i++) {
+ const renderedConn = /** @type {!RenderedConnection} */ (connection);
// Spider down from this block bumping all sub-blocks.
- if (connection.isConnected() && connection.isSuperior()) {
- connection.targetBlock().bumpNeighbours();
+ if (renderedConn.isConnected() && renderedConn.isSuperior()) {
+ renderedConn.targetBlock().bumpNeighbours();
}
- const neighbours = connection.neighbours(internalConstants.SNAP_RADIUS);
+ const neighbours = renderedConn.neighbours(internalConstants.SNAP_RADIUS);
for (let j = 0, otherConnection; (otherConnection = neighbours[j]); j++) {
+ const renderedOther =
+ /** @type {!RenderedConnection} */ (otherConnection);
// If both connections are connected, that's probably fine. But if
// either one of them is unconnected, then there could be confusion.
- if (!connection.isConnected() || !otherConnection.isConnected()) {
+ if (!renderedConn.isConnected() || !renderedOther.isConnected()) {
// Only bump blocks if they are from different tree structures.
- if (otherConnection.getSourceBlock().getRootBlock() !== rootBlock) {
+ if (renderedOther.getSourceBlock().getRootBlock() !== rootBlock) {
// Always bump the inferior block.
- if (connection.isSuperior()) {
- otherConnection.bumpAwayFrom(connection);
+ if (renderedConn.isSuperior()) {
+ renderedOther.bumpAwayFrom(renderedConn);
} else {
- connection.bumpAwayFrom(otherConnection);
+ renderedConn.bumpAwayFrom(renderedOther);
}
}
}
@@ -1706,7 +1713,8 @@ BlockSvg.prototype.updateConnectionLocations_ = function() {
}
for (let i = 0; i < this.inputList.length; i++) {
- const conn = this.inputList[i].connection;
+ const conn =
+ /** @type {!RenderedConnection} */ (this.inputList[i].connection);
if (conn) {
conn.moveToOffset(blockTL);
if (conn.isConnected()) {
diff --git a/core/comment.js b/core/comment.js
index d11e2b636..8f1efbe93 100644
--- a/core/comment.js
+++ b/core/comment.js
@@ -19,7 +19,6 @@ const Css = goog.require('Blockly.Css');
const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
-const object = goog.require('Blockly.utils.object');
const userAgent = goog.require('Blockly.utils.userAgent');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
@@ -44,391 +43,392 @@ goog.require('Blockly.Warning');
/**
* Class for a comment.
- * @param {!Block} block The block associated with this comment.
* @extends {Icon}
- * @constructor
- * @alias Blockly.Comment
*/
-const Comment = function(block) {
- Comment.superClass_.constructor.call(this, block);
-
+class Comment extends Icon {
/**
- * The model for this comment.
- * @type {!Block.CommentModel}
- * @private
+ * @param {!BlockSvg} block The block associated with this comment.
+ * @alias Blockly.Comment
*/
- this.model_ = block.commentModel;
- // If someone creates the comment directly instead of calling
- // block.setCommentText we want to make sure the text is non-null;
- this.model_.text = this.model_.text || '';
+ constructor(block) {
+ super(block);
- /**
- * The model's text value at the start of an edit.
- * Used to tell if an event should be fired at the end of an edit.
- * @type {?string}
- * @private
- */
- this.cachedText_ = '';
+ /**
+ * The model for this comment.
+ * @type {!Block.CommentModel}
+ * @private
+ */
+ this.model_ = block.commentModel;
+ // If someone creates the comment directly instead of calling
+ // block.setCommentText we want to make sure the text is non-null;
+ this.model_.text = this.model_.text || '';
- /**
- * Mouse up event data.
- * @type {?browserEvents.Data}
- * @private
- */
- this.onMouseUpWrapper_ = null;
+ /**
+ * The model's text value at the start of an edit.
+ * Used to tell if an event should be fired at the end of an edit.
+ * @type {?string}
+ * @private
+ */
+ this.cachedText_ = '';
- /**
- * Wheel event data.
- * @type {?browserEvents.Data}
- * @private
- */
- this.onWheelWrapper_ = null;
-
- /**
- * Change event data.
- * @type {?browserEvents.Data}
- * @private
- */
- this.onChangeWrapper_ = null;
-
- /**
- * Input event data.
- * @type {?browserEvents.Data}
- * @private
- */
- this.onInputWrapper_ = null;
-
- /**
- * The SVG element that contains the text edit area, or null if not created.
- * @type {?SVGForeignObjectElement}
- * @private
- */
- this.foreignObject_ = null;
-
- /**
- * The editable text area, or null if not created.
- * @type {?Element}
- * @private
- */
- this.textarea_ = null;
-
- /**
- * The top-level node of the comment text, or null if not created.
- * @type {?SVGTextElement}
- * @private
- */
- this.paragraphElement_ = null;
-
-
- this.createIcon();
-};
-object.inherits(Comment, Icon);
-
-/**
- * Draw the comment icon.
- * @param {!Element} group The icon group.
- * @protected
- */
-Comment.prototype.drawIcon_ = function(group) {
- // Circle.
- dom.createSvgElement(
- Svg.CIRCLE, {'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'},
- group);
- // Can't use a real '?' text character since different browsers and operating
- // systems render it differently.
- // Body of question mark.
- dom.createSvgElement(
- Svg.PATH, {
- 'class': 'blocklyIconSymbol',
- 'd': 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' +
- '0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' +
- '-1.201,0.998 -1.201,1.528 -1.204,2.19z',
- },
- group);
- // Dot of question mark.
- dom.createSvgElement(
- Svg.RECT, {
- 'class': 'blocklyIconSymbol',
- 'x': '6.8',
- 'y': '10.78',
- 'height': '2',
- 'width': '2',
- },
- group);
-};
-
-/**
- * Create the editor for the comment's bubble.
- * @return {!SVGElement} The top-level node of the editor.
- * @private
- */
-Comment.prototype.createEditor_ = function() {
- /* Create the editor. Here's the markup that will be generated in
- * editable mode:
-
-
-
-
-
- * For non-editable mode see Warning.textToDom_.
- */
-
- this.foreignObject_ = dom.createSvgElement(
- Svg.FOREIGNOBJECT, {'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH},
- null);
-
- const body = document.createElementNS(dom.HTML_NS, 'body');
- body.setAttribute('xmlns', dom.HTML_NS);
- body.className = 'blocklyMinimalBody';
-
- this.textarea_ = document.createElementNS(dom.HTML_NS, 'textarea');
- const textarea = this.textarea_;
- textarea.className = 'blocklyCommentTextarea';
- textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
- textarea.value = this.model_.text;
- this.resizeTextarea_();
-
- body.appendChild(textarea);
- this.foreignObject_.appendChild(body);
-
- // Ideally this would be hooked to the focus event for the comment.
- // However doing so in Firefox swallows the cursor for unknown reasons.
- // So this is hooked to mouseup instead. No big deal.
- this.onMouseUpWrapper_ = browserEvents.conditionalBind(
- textarea, 'mouseup', this, this.startEdit_, true, true);
- // Don't zoom with mousewheel.
- this.onWheelWrapper_ =
- browserEvents.conditionalBind(textarea, 'wheel', this, function(e) {
- e.stopPropagation();
- });
- this.onChangeWrapper_ = browserEvents.conditionalBind(
- textarea, 'change', this,
- /**
- * @this {Comment}
- * @param {Event} _e Unused event parameter.
- */
- function(_e) {
- if (this.cachedText_ !== this.model_.text) {
- eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
- this.block_, 'comment', null, this.cachedText_,
- this.model_.text));
- }
- });
- this.onInputWrapper_ = browserEvents.conditionalBind(
- textarea, 'input', this,
- /**
- * @this {Comment}
- * @param {Event} _e Unused event parameter.
- */
- function(_e) {
- this.model_.text = textarea.value;
- });
-
- setTimeout(textarea.focus.bind(textarea), 0);
-
- return this.foreignObject_;
-};
-
-/**
- * Add or remove editability of the comment.
- * @override
- */
-Comment.prototype.updateEditable = function() {
- Comment.superClass_.updateEditable.call(this);
- if (this.isVisible()) {
- // Recreate the bubble with the correct UI.
- this.disposeBubble_();
- this.createBubble_();
- }
-};
-
-/**
- * Callback function triggered when the bubble has resized.
- * Resize the text area accordingly.
- * @private
- */
-Comment.prototype.onBubbleResize_ = function() {
- if (!this.isVisible()) {
- return;
- }
- this.model_.size = this.bubble_.getBubbleSize();
- this.resizeTextarea_();
-};
-
-/**
- * Resizes the text area to match the size defined on the model (which is
- * the size of the bubble).
- * @private
- */
-Comment.prototype.resizeTextarea_ = function() {
- const size = this.model_.size;
- const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
- const widthMinusBorder = size.width - doubleBorderWidth;
- const heightMinusBorder = size.height - doubleBorderWidth;
- this.foreignObject_.setAttribute('width', widthMinusBorder);
- this.foreignObject_.setAttribute('height', heightMinusBorder);
- this.textarea_.style.width = (widthMinusBorder - 4) + 'px';
- this.textarea_.style.height = (heightMinusBorder - 4) + 'px';
-};
-
-/**
- * Show or hide the comment bubble.
- * @param {boolean} visible True if the bubble should be visible.
- */
-Comment.prototype.setVisible = function(visible) {
- if (visible === this.isVisible()) {
- return;
- }
- eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
- this.block_, visible, 'comment'));
- this.model_.pinned = visible;
- if (visible) {
- this.createBubble_();
- } else {
- this.disposeBubble_();
- }
-};
-
-/**
- * Show the bubble. Handles deciding if it should be editable or not.
- * @private
- */
-Comment.prototype.createBubble_ = function() {
- if (!this.block_.isEditable() || userAgent.IE) {
- // MSIE does not support foreignobject; textareas are impossible.
- // https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-svg/56e6e04c-7c8c-44dd-8100-bd745ee42034
- // Always treat comments in IE as uneditable.
- this.createNonEditableBubble_();
- } else {
- this.createEditableBubble_();
- }
-};
-
-/**
- * Show an editable bubble.
- * @private
- */
-Comment.prototype.createEditableBubble_ = function() {
- this.bubble_ = new Bubble(
- /** @type {!WorkspaceSvg} */ (this.block_.workspace),
- this.createEditor_(), this.block_.pathObject.svgPath,
- /** @type {!Coordinate} */ (this.iconXY_), this.model_.size.width,
- this.model_.size.height);
- // Expose this comment's block's ID on its top-level SVG group.
- this.bubble_.setSvgId(this.block_.id);
- this.bubble_.registerResizeEvent(this.onBubbleResize_.bind(this));
- this.applyColour();
-};
-
-/**
- * Show a non-editable bubble.
- * @private
- * @suppress {checkTypes} Suppress `this` type mismatch.
- */
-Comment.prototype.createNonEditableBubble_ = function() {
- // TODO (#2917): It would be great if the comment could support line breaks.
- this.paragraphElement_ = Bubble.textToDom(this.block_.getCommentText());
- this.bubble_ = Bubble.createNonEditableBubble(
- this.paragraphElement_, /** @type {!BlockSvg} */ (this.block_),
- /** @type {!Coordinate} */ (this.iconXY_));
- this.applyColour();
-};
-
-/**
- * Dispose of the bubble.
- * @private
- * @suppress {checkTypes} Suppress `this` type mismatch.
- */
-Comment.prototype.disposeBubble_ = function() {
- if (this.onMouseUpWrapper_) {
- browserEvents.unbind(this.onMouseUpWrapper_);
+ /**
+ * Mouse up event data.
+ * @type {?browserEvents.Data}
+ * @private
+ */
this.onMouseUpWrapper_ = null;
- }
- if (this.onWheelWrapper_) {
- browserEvents.unbind(this.onWheelWrapper_);
+
+ /**
+ * Wheel event data.
+ * @type {?browserEvents.Data}
+ * @private
+ */
this.onWheelWrapper_ = null;
- }
- if (this.onChangeWrapper_) {
- browserEvents.unbind(this.onChangeWrapper_);
+
+ /**
+ * Change event data.
+ * @type {?browserEvents.Data}
+ * @private
+ */
this.onChangeWrapper_ = null;
- }
- if (this.onInputWrapper_) {
- browserEvents.unbind(this.onInputWrapper_);
+
+ /**
+ * Input event data.
+ * @type {?browserEvents.Data}
+ * @private
+ */
this.onInputWrapper_ = null;
- }
- this.bubble_.dispose();
- this.bubble_ = null;
- this.textarea_ = null;
- this.foreignObject_ = null;
- this.paragraphElement_ = null;
-};
-/**
- * Callback fired when an edit starts.
- *
- * Bring the comment to the top of the stack when clicked on. Also cache the
- * current text so it can be used to fire a change event.
- * @param {!Event} _e Mouse up event.
- * @private
- */
-Comment.prototype.startEdit_ = function(_e) {
- if (this.bubble_.promote()) {
- // Since the act of moving this node within the DOM causes a loss of focus,
- // we need to reapply the focus.
- this.textarea_.focus();
+ /**
+ * The SVG element that contains the text edit area, or null if not created.
+ * @type {?SVGForeignObjectElement}
+ * @private
+ */
+ this.foreignObject_ = null;
+
+ /**
+ * The editable text area, or null if not created.
+ * @type {?Element}
+ * @private
+ */
+ this.textarea_ = null;
+
+ /**
+ * The top-level node of the comment text, or null if not created.
+ * @type {?SVGTextElement}
+ * @private
+ */
+ this.paragraphElement_ = null;
+
+
+ this.createIcon();
}
- this.cachedText_ = this.model_.text;
-};
-
-/**
- * Get the dimensions of this comment's bubble.
- * @return {Size} Object with width and height properties.
- */
-Comment.prototype.getBubbleSize = function() {
- return this.model_.size;
-};
-
-/**
- * Size this comment's bubble.
- * @param {number} width Width of the bubble.
- * @param {number} height Height of the bubble.
- */
-Comment.prototype.setBubbleSize = function(width, height) {
- if (this.bubble_) {
- this.bubble_.setBubbleSize(width, height);
- } else {
- this.model_.size.width = width;
- this.model_.size.height = height;
+ /**
+ * Draw the comment icon.
+ * @param {!Element} group The icon group.
+ * @protected
+ */
+ drawIcon_(group) {
+ // Circle.
+ dom.createSvgElement(
+ Svg.CIRCLE,
+ {'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'}, group);
+ // Can't use a real '?' text character since different browsers and
+ // operating systems render it differently. Body of question mark.
+ dom.createSvgElement(
+ Svg.PATH, {
+ 'class': 'blocklyIconSymbol',
+ 'd': 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' +
+ '0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' +
+ '-1.201,0.998 -1.201,1.528 -1.204,2.19z',
+ },
+ group);
+ // Dot of question mark.
+ dom.createSvgElement(
+ Svg.RECT, {
+ 'class': 'blocklyIconSymbol',
+ 'x': '6.8',
+ 'y': '10.78',
+ 'height': '2',
+ 'width': '2',
+ },
+ group);
}
-};
-/**
- * Update the comment's view to match the model.
- * @package
- */
-Comment.prototype.updateText = function() {
- if (this.textarea_) {
- this.textarea_.value = this.model_.text;
- } else if (this.paragraphElement_) {
- // Non-Editable mode.
- // TODO (#2917): If 2917 gets added this will probably need to be updated.
- this.paragraphElement_.firstChild.textContent = this.model_.text;
+ /**
+ * Create the editor for the comment's bubble.
+ * @return {!SVGElement} The top-level node of the editor.
+ * @private
+ */
+ createEditor_() {
+ /* Create the editor. Here's the markup that will be generated in
+ * editable mode:
+
+
+
+
+
+ * For non-editable mode see Warning.textToDom_.
+ */
+
+ this.foreignObject_ = dom.createSvgElement(
+ Svg.FOREIGNOBJECT, {'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH},
+ null);
+
+ const body = document.createElementNS(dom.HTML_NS, 'body');
+ body.setAttribute('xmlns', dom.HTML_NS);
+ body.className = 'blocklyMinimalBody';
+
+ this.textarea_ = document.createElementNS(dom.HTML_NS, 'textarea');
+ const textarea = this.textarea_;
+ textarea.className = 'blocklyCommentTextarea';
+ textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
+ textarea.value = this.model_.text;
+ this.resizeTextarea_();
+
+ body.appendChild(textarea);
+ this.foreignObject_.appendChild(body);
+
+ // Ideally this would be hooked to the focus event for the comment.
+ // However doing so in Firefox swallows the cursor for unknown reasons.
+ // So this is hooked to mouseup instead. No big deal.
+ this.onMouseUpWrapper_ = browserEvents.conditionalBind(
+ textarea, 'mouseup', this, this.startEdit_, true, true);
+ // Don't zoom with mousewheel.
+ this.onWheelWrapper_ =
+ browserEvents.conditionalBind(textarea, 'wheel', this, function(e) {
+ e.stopPropagation();
+ });
+ this.onChangeWrapper_ = browserEvents.conditionalBind(
+ textarea, 'change', this,
+ /**
+ * @this {Comment}
+ * @param {Event} _e Unused event parameter.
+ */
+ function(_e) {
+ if (this.cachedText_ !== this.model_.text) {
+ eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
+ this.block_, 'comment', null, this.cachedText_,
+ this.model_.text));
+ }
+ });
+ this.onInputWrapper_ = browserEvents.conditionalBind(
+ textarea, 'input', this,
+ /**
+ * @this {Comment}
+ * @param {Event} _e Unused event parameter.
+ */
+ function(_e) {
+ this.model_.text = textarea.value;
+ });
+
+ setTimeout(textarea.focus.bind(textarea), 0);
+
+ return this.foreignObject_;
}
-};
-/**
- * Dispose of this comment.
- *
- * If you want to receive a comment "delete" event (newValue: null), then this
- * should not be called directly. Instead call block.setCommentText(null);
- */
-Comment.prototype.dispose = function() {
- this.block_.comment = null;
- Icon.prototype.dispose.call(this);
-};
+ /**
+ * Add or remove editability of the comment.
+ * @override
+ */
+ updateEditable() {
+ super.updateEditable();
+ if (this.isVisible()) {
+ // Recreate the bubble with the correct UI.
+ this.disposeBubble_();
+ this.createBubble_();
+ }
+ }
+
+ /**
+ * Callback function triggered when the bubble has resized.
+ * Resize the text area accordingly.
+ * @private
+ */
+ onBubbleResize_() {
+ if (!this.isVisible()) {
+ return;
+ }
+ this.model_.size = this.bubble_.getBubbleSize();
+ this.resizeTextarea_();
+ }
+
+ /**
+ * Resizes the text area to match the size defined on the model (which is
+ * the size of the bubble).
+ * @private
+ */
+ resizeTextarea_() {
+ const size = this.model_.size;
+ const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
+ const widthMinusBorder = size.width - doubleBorderWidth;
+ const heightMinusBorder = size.height - doubleBorderWidth;
+ this.foreignObject_.setAttribute('width', widthMinusBorder);
+ this.foreignObject_.setAttribute('height', heightMinusBorder);
+ this.textarea_.style.width = (widthMinusBorder - 4) + 'px';
+ this.textarea_.style.height = (heightMinusBorder - 4) + 'px';
+ }
+
+ /**
+ * Show or hide the comment bubble.
+ * @param {boolean} visible True if the bubble should be visible.
+ */
+ setVisible(visible) {
+ if (visible === this.isVisible()) {
+ return;
+ }
+ eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
+ this.block_, visible, 'comment'));
+ this.model_.pinned = visible;
+ if (visible) {
+ this.createBubble_();
+ } else {
+ this.disposeBubble_();
+ }
+ }
+
+ /**
+ * Show the bubble. Handles deciding if it should be editable or not.
+ * @private
+ */
+ createBubble_() {
+ if (!this.block_.isEditable() || userAgent.IE) {
+ // MSIE does not support foreignobject; textareas are impossible.
+ // https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-svg/56e6e04c-7c8c-44dd-8100-bd745ee42034
+ // Always treat comments in IE as uneditable.
+ this.createNonEditableBubble_();
+ } else {
+ this.createEditableBubble_();
+ }
+ }
+
+ /**
+ * Show an editable bubble.
+ * @private
+ */
+ createEditableBubble_() {
+ this.bubble_ = new Bubble(
+ /** @type {!WorkspaceSvg} */ (this.block_.workspace),
+ this.createEditor_(), this.block_.pathObject.svgPath,
+ /** @type {!Coordinate} */ (this.iconXY_), this.model_.size.width,
+ this.model_.size.height);
+ // Expose this comment's block's ID on its top-level SVG group.
+ this.bubble_.setSvgId(this.block_.id);
+ this.bubble_.registerResizeEvent(this.onBubbleResize_.bind(this));
+ this.applyColour();
+ }
+
+ /**
+ * Show a non-editable bubble.
+ * @private
+ * @suppress {checkTypes} Suppress `this` type mismatch.
+ */
+ createNonEditableBubble_() {
+ // TODO (#2917): It would be great if the comment could support line breaks.
+ this.paragraphElement_ = Bubble.textToDom(this.block_.getCommentText());
+ this.bubble_ = Bubble.createNonEditableBubble(
+ this.paragraphElement_, /** @type {!BlockSvg} */ (this.block_),
+ /** @type {!Coordinate} */ (this.iconXY_));
+ this.applyColour();
+ }
+
+ /**
+ * Dispose of the bubble.
+ * @private
+ * @suppress {checkTypes} Suppress `this` type mismatch.
+ */
+ disposeBubble_() {
+ if (this.onMouseUpWrapper_) {
+ browserEvents.unbind(this.onMouseUpWrapper_);
+ this.onMouseUpWrapper_ = null;
+ }
+ if (this.onWheelWrapper_) {
+ browserEvents.unbind(this.onWheelWrapper_);
+ this.onWheelWrapper_ = null;
+ }
+ if (this.onChangeWrapper_) {
+ browserEvents.unbind(this.onChangeWrapper_);
+ this.onChangeWrapper_ = null;
+ }
+ if (this.onInputWrapper_) {
+ browserEvents.unbind(this.onInputWrapper_);
+ this.onInputWrapper_ = null;
+ }
+ this.bubble_.dispose();
+ this.bubble_ = null;
+ this.textarea_ = null;
+ this.foreignObject_ = null;
+ this.paragraphElement_ = null;
+ }
+
+ /**
+ * Callback fired when an edit starts.
+ *
+ * Bring the comment to the top of the stack when clicked on. Also cache the
+ * current text so it can be used to fire a change event.
+ * @param {!Event} _e Mouse up event.
+ * @private
+ */
+ startEdit_(_e) {
+ if (this.bubble_.promote()) {
+ // Since the act of moving this node within the DOM causes a loss of
+ // focus, we need to reapply the focus.
+ this.textarea_.focus();
+ }
+
+ this.cachedText_ = this.model_.text;
+ }
+
+ /**
+ * Get the dimensions of this comment's bubble.
+ * @return {Size} Object with width and height properties.
+ */
+ getBubbleSize() {
+ return this.model_.size;
+ }
+
+ /**
+ * Size this comment's bubble.
+ * @param {number} width Width of the bubble.
+ * @param {number} height Height of the bubble.
+ */
+ setBubbleSize(width, height) {
+ if (this.bubble_) {
+ this.bubble_.setBubbleSize(width, height);
+ } else {
+ this.model_.size.width = width;
+ this.model_.size.height = height;
+ }
+ }
+
+ /**
+ * Update the comment's view to match the model.
+ * @package
+ */
+ updateText() {
+ if (this.textarea_) {
+ this.textarea_.value = this.model_.text;
+ } else if (this.paragraphElement_) {
+ // Non-Editable mode.
+ // TODO (#2917): If 2917 gets added this will probably need to be updated.
+ this.paragraphElement_.firstChild.textContent = this.model_.text;
+ }
+ }
+
+ /**
+ * Dispose of this comment.
+ *
+ * If you want to receive a comment "delete" event (newValue: null), then this
+ * should not be called directly. Instead call block.setCommentText(null);
+ */
+ dispose() {
+ this.block_.comment = null;
+ Icon.prototype.dispose.call(this);
+ }
+}
/**
* CSS for block comment. See css.js for use.
diff --git a/core/connection.js b/core/connection.js
index 3273ab100..962b884ca 100644
--- a/core/connection.js
+++ b/core/connection.js
@@ -35,62 +35,627 @@ goog.require('Blockly.constants');
/**
* Class for a connection between blocks.
- * @param {!Block} source The block establishing this connection.
- * @param {number} type The type of the connection.
- * @constructor
* @implements {IASTNodeLocationWithBlock}
- * @alias Blockly.Connection
*/
-const Connection = function(source, type) {
+class Connection {
/**
- * @type {!Block}
+ * @param {!Block} source The block establishing this connection.
+ * @param {number} type The type of the connection.
+ * @alias Blockly.Connection
+ */
+ constructor(source, type) {
+ /**
+ * @type {!Block}
+ * @protected
+ */
+ 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;
+
+ /**
+ * @type {?blocks.State}
+ * @private
+ */
+ this.shadowState_ = null;
+ }
+
+ /**
+ * Connect two connections together. This is the connection on the superior
+ * block.
+ * @param {!Connection} childConnection Connection on inferior block.
* @protected
*/
- this.sourceBlock_ = source;
- /** @type {number} */
- this.type = type;
+ connect_(childConnection) {
+ const INPUT = ConnectionType.INPUT_VALUE;
+ const parentConnection = this;
+ const parentBlock = parentConnection.getSourceBlock();
+ const childBlock = childConnection.getSourceBlock();
+
+ // Make sure the childConnection is available.
+ if (childConnection.isConnected()) {
+ childConnection.disconnect();
+ }
+
+ // Make sure the parentConnection is available.
+ let orphan;
+ if (parentConnection.isConnected()) {
+ const shadowState = parentConnection.stashShadowState_();
+ const target = parentConnection.targetBlock();
+ if (target.isShadow()) {
+ target.dispose(false);
+ } else {
+ parentConnection.disconnect();
+ orphan = target;
+ }
+ parentConnection.applyShadowState_(shadowState);
+ }
+
+ // Connect the new connection to the parent.
+ let event;
+ if (eventUtils.isEnabled()) {
+ event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock);
+ }
+ connectReciprocally(parentConnection, childConnection);
+ childBlock.setParent(parentBlock);
+ if (event) {
+ event.recordNew();
+ eventUtils.fire(event);
+ }
+
+ // Deal with the orphan if it exists.
+ if (orphan) {
+ const orphanConnection = parentConnection.type === INPUT ?
+ orphan.outputConnection :
+ orphan.previousConnection;
+ const connection = Connection.getConnectionForOrphanedConnection(
+ childBlock, /** @type {!Connection} */ (orphanConnection));
+ if (connection) {
+ orphanConnection.connect(connection);
+ } else {
+ orphanConnection.onFailedConnect(parentConnection);
+ }
+ }
+ }
/**
- * Connection this connection connects to. Null if not connected.
- * @type {Connection}
- */
- this.targetConnection = null;
-
- /**
- * Has this connection been disposed of?
- * @type {boolean}
+ * Dispose of this connection and deal with connected blocks.
* @package
*/
- this.disposed = false;
+ dispose() {
+ // isConnected returns true for shadows and non-shadows.
+ if (this.isConnected()) {
+ // Destroy the attached shadow block & its children (if it exists).
+ this.setShadowStateInternal_();
+
+ const targetBlock = this.targetBlock();
+ if (targetBlock) {
+ // Disconnect the attached normal block.
+ targetBlock.unplug();
+ }
+ }
+
+ this.disposed = true;
+ }
/**
- * List of compatible value types. Null if all types are compatible.
- * @type {Array}
+ * Get the source block for this connection.
+ * @return {!Block} The source block.
+ */
+ getSourceBlock() {
+ return this.sourceBlock_;
+ }
+
+ /**
+ * Does the connection belong to a superior block (higher in the source
+ * stack)?
+ * @return {boolean} True if connection faces down or right.
+ */
+ isSuperior() {
+ return this.type === ConnectionType.INPUT_VALUE ||
+ this.type === ConnectionType.NEXT_STATEMENT;
+ }
+
+ /**
+ * Is the connection connected?
+ * @return {boolean} True if connection is connected to another connection.
+ */
+ isConnected() {
+ return !!this.targetConnection;
+ }
+
+ /**
+ * Get the workspace's connection type checker object.
+ * @return {!IConnectionChecker} The connection type checker for the
+ * source block's workspace.
+ * @package
+ */
+ getConnectionChecker() {
+ return this.sourceBlock_.workspace.connectionChecker;
+ }
+
+ /**
+ * Called when an attempted connection fails. NOP by default (i.e. for
+ * headless workspaces).
+ * @param {!Connection} _otherConnection Connection that this connection
+ * failed to connect to.
+ * @package
+ */
+ onFailedConnect(_otherConnection) {
+ // NOP
+ }
+
+ /**
+ * Connect this connection to another connection.
+ * @param {!Connection} otherConnection Connection to connect to.
+ * @return {boolean} Whether the the blocks are now connected or not.
+ */
+ connect(otherConnection) {
+ if (this.targetConnection === otherConnection) {
+ // Already connected together. NOP.
+ return true;
+ }
+
+ const checker = this.getConnectionChecker();
+ if (checker.canConnect(this, otherConnection, false)) {
+ const eventGroup = eventUtils.getGroup();
+ if (!eventGroup) {
+ eventUtils.setGroup(true);
+ }
+ // Determine which block is superior (higher in the source stack).
+ if (this.isSuperior()) {
+ // Superior block.
+ this.connect_(otherConnection);
+ } else {
+ // Inferior block.
+ otherConnection.connect_(this);
+ }
+ if (!eventGroup) {
+ eventUtils.setGroup(false);
+ }
+ }
+
+ return this.isConnected();
+ }
+
+ /**
+ * Disconnect this connection.
+ */
+ disconnect() {
+ const otherConnection = this.targetConnection;
+ if (!otherConnection) {
+ throw Error('Source connection not connected.');
+ }
+ if (otherConnection.targetConnection !== this) {
+ throw Error('Target connection not connected to source connection.');
+ }
+ let parentBlock;
+ let childBlock;
+ let parentConnection;
+ if (this.isSuperior()) {
+ // Superior block.
+ parentBlock = this.sourceBlock_;
+ childBlock = otherConnection.getSourceBlock();
+ parentConnection = this;
+ } else {
+ // Inferior block.
+ parentBlock = otherConnection.getSourceBlock();
+ childBlock = this.sourceBlock_;
+ parentConnection = otherConnection;
+ }
+
+ const eventGroup = eventUtils.getGroup();
+ if (!eventGroup) {
+ eventUtils.setGroup(true);
+ }
+ this.disconnectInternal_(parentBlock, childBlock);
+ if (!childBlock.isShadow()) {
+ // If we were disconnecting a shadow, no need to spawn a new one.
+ parentConnection.respawnShadow_();
+ }
+ if (!eventGroup) {
+ eventUtils.setGroup(false);
+ }
+ }
+
+ /**
+ * Disconnect two blocks that are connected by this connection.
+ * @param {!Block} parentBlock The superior block.
+ * @param {!Block} childBlock The inferior block.
+ * @protected
+ */
+ disconnectInternal_(parentBlock, childBlock) {
+ let event;
+ if (eventUtils.isEnabled()) {
+ event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock);
+ }
+ const otherConnection = this.targetConnection;
+ otherConnection.targetConnection = null;
+ this.targetConnection = null;
+ childBlock.setParent(null);
+ if (event) {
+ event.recordNew();
+ eventUtils.fire(event);
+ }
+ }
+
+ /**
+ * Respawn the shadow block if there was one connected to the this connection.
+ * @protected
+ */
+ respawnShadow_() {
+ // Have to keep respawnShadow_ for backwards compatibility.
+ this.createShadowBlock_(true);
+ }
+
+ /**
+ * Returns the block that this connection connects to.
+ * @return {?Block} The connected block or null if none is connected.
+ */
+ targetBlock() {
+ if (this.isConnected()) {
+ return this.targetConnection.getSourceBlock();
+ }
+ return null;
+ }
+
+ /**
+ * Function to be called when this connection's compatible types have changed.
+ * @protected
+ */
+ onCheckChanged_() {
+ // The new value type may not be compatible with the existing connection.
+ if (this.isConnected() &&
+ (!this.targetConnection ||
+ !this.getConnectionChecker().canConnect(
+ this, this.targetConnection, false))) {
+ const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
+ child.unplug();
+ }
+ }
+
+ /**
+ * Change a connection's compatibility.
+ * @param {?(string|!Array)} check Compatible value type or list of
+ * value types. Null if all types are compatible.
+ * @return {!Connection} The connection being modified
+ * (to allow chaining).
+ */
+ setCheck(check) {
+ if (check) {
+ // Ensure that check is in an array.
+ if (!Array.isArray(check)) {
+ check = [check];
+ }
+ this.check_ = check;
+ this.onCheckChanged_();
+ } else {
+ this.check_ = null;
+ }
+ return this;
+ }
+
+ /**
+ * Get a connection's compatibility.
+ * @return {?Array} List of compatible value types.
+ * Null if all types are compatible.
+ * @public
+ */
+ getCheck() {
+ return this.check_;
+ }
+
+ /**
+ * Changes the connection's shadow block.
+ * @param {?Element} shadowDom DOM representation of a block or null.
+ */
+ setShadowDom(shadowDom) {
+ this.setShadowStateInternal_({shadowDom: shadowDom});
+ }
+
+ /**
+ * Returns the xml representation of the connection's shadow block.
+ * @param {boolean=} returnCurrent If true, and the shadow block is currently
+ * attached to this connection, this serializes the state of that block
+ * and returns it (so that field values are correct). Otherwise the saved
+ * shadowDom is just returned.
+ * @return {?Element} Shadow DOM representation of a block or null.
+ */
+ getShadowDom(returnCurrent) {
+ return (returnCurrent && this.targetBlock().isShadow()) ?
+ /** @type {!Element} */ (Xml.blockToDom(
+ /** @type {!Block} */ (this.targetBlock()))) :
+ this.shadowDom_;
+ }
+
+ /**
+ * Changes the connection's shadow block.
+ * @param {?blocks.State} shadowState An state represetation of the block or
+ * null.
+ */
+ setShadowState(shadowState) {
+ this.setShadowStateInternal_({shadowState: shadowState});
+ }
+
+ /**
+ * Returns the serialized object representation of the connection's shadow
+ * block.
+ * @param {boolean=} returnCurrent If true, and the shadow block is currently
+ * attached to this connection, this serializes the state of that block
+ * and returns it (so that field values are correct). Otherwise the saved
+ * state is just returned.
+ * @return {?blocks.State} Serialized object representation of the block, or
+ * null.
+ */
+ getShadowState(returnCurrent) {
+ if (returnCurrent && this.targetBlock() && this.targetBlock().isShadow()) {
+ return blocks.save(/** @type {!Block} */ (this.targetBlock()));
+ }
+ return this.shadowState_;
+ }
+
+ /**
+ * Find all nearby compatible connections to this connection.
+ * Type checking does not apply, since this function is used for bumping.
+ *
+ * Headless configurations (the default) do not have neighboring connection,
+ * and always return an empty list (the default).
+ * {@link Blockly.RenderedConnection} overrides this behavior with a list
+ * computed from the rendered positioning.
+ * @param {number} _maxLimit The maximum radius to another connection.
+ * @return {!Array} List of connections.
+ * @package
+ */
+ neighbours(_maxLimit) {
+ return [];
+ }
+
+ /**
+ * Get the parent input of a connection.
+ * @return {?Input} The input that the connection belongs to or null if
+ * no parent exists.
+ * @package
+ */
+ getParentInput() {
+ let parentInput = null;
+ const inputs = this.sourceBlock_.inputList;
+ for (let i = 0; i < inputs.length; i++) {
+ if (inputs[i].connection === this) {
+ parentInput = inputs[i];
+ break;
+ }
+ }
+ return parentInput;
+ }
+
+ /**
+ * This method returns a string describing this Connection in developer terms
+ * (English only). Intended to on be used in console logs and errors.
+ * @return {string} The description.
+ */
+ toString() {
+ const block = this.sourceBlock_;
+ if (!block) {
+ return 'Orphan Connection';
+ }
+ let msg;
+ if (block.outputConnection === this) {
+ msg = 'Output Connection of ';
+ } else if (block.previousConnection === this) {
+ msg = 'Previous Connection of ';
+ } else if (block.nextConnection === this) {
+ msg = 'Next Connection of ';
+ } else {
+ let parentInput = null;
+ for (let i = 0, input; (input = block.inputList[i]); i++) {
+ if (input.connection === this) {
+ parentInput = input;
+ break;
+ }
+ }
+ if (parentInput) {
+ msg = 'Input "' + parentInput.name + '" connection on ';
+ } else {
+ console.warn('Connection not actually connected to sourceBlock_');
+ return 'Orphan Connection';
+ }
+ }
+ return msg + block.toDevString();
+ }
+
+ /**
+ * Returns the state of the shadowDom_ and shadowState_ properties, then
+ * temporarily sets those properties to null so no shadow respawns.
+ * @return {{shadowDom: ?Element, shadowState: ?blocks.State}} The state of
+ * both the shadowDom_ and shadowState_ properties.
* @private
*/
- this.check_ = null;
+ stashShadowState_() {
+ const shadowDom = this.getShadowDom(true);
+ const shadowState = this.getShadowState(true);
+ // Set to null so it doesn't respawn.
+ this.shadowDom_ = null;
+ this.shadowState_ = null;
+ return {shadowDom, shadowState};
+ }
/**
- * DOM representation of a shadow block, or null if none.
- * @type {Element}
+ * Reapplies the stashed state of the shadowDom_ and shadowState_ properties.
+ * @param {{shadowDom: ?Element, shadowState: ?blocks.State}} param0 The state
+ * to reapply to the shadowDom_ and shadowState_ properties.
* @private
*/
- this.shadowDom_ = null;
+ applyShadowState_({shadowDom, shadowState}) {
+ this.shadowDom_ = shadowDom;
+ this.shadowState_ = shadowState;
+ }
/**
- * Horizontal location of this connection.
- * @type {number}
- * @package
+ * Sets the state of the shadow of this connection.
+ * @param {{shadowDom: (?Element|undefined), shadowState:
+ * (?blocks.State|undefined)}=} param0 The state to set the shadow of this
+ * connection to.
+ * @private
*/
- this.x = 0;
+ setShadowStateInternal_({shadowDom = null, shadowState = null} = {}) {
+ // One or both of these should always be null.
+ // If neither is null, the shadowState will get priority.
+ this.shadowDom_ = shadowDom;
+ this.shadowState_ = shadowState;
+
+ const target = this.targetBlock();
+ if (!target) {
+ this.respawnShadow_();
+ if (this.targetBlock() && this.targetBlock().isShadow()) {
+ this.serializeShadow_(this.targetBlock());
+ }
+ } else if (target.isShadow()) {
+ target.dispose(false);
+ this.respawnShadow_();
+ if (this.targetBlock() && this.targetBlock().isShadow()) {
+ this.serializeShadow_(this.targetBlock());
+ }
+ } else {
+ const shadow = this.createShadowBlock_(false);
+ this.serializeShadow_(shadow);
+ if (shadow) {
+ shadow.dispose(false);
+ }
+ }
+ }
/**
- * Vertical location of this connection.
- * @type {number}
- * @package
+ * Creates a shadow block based on the current shadowState_ or shadowDom_.
+ * shadowState_ gets priority.
+ * @param {boolean} attemptToConnect Whether to try to connect the shadow
+ * block to this connection or not.
+ * @return {?Block} The shadow block that was created, or null if both the
+ * shadowState_ and shadowDom_ are null.
+ * @private
*/
- this.y = 0;
-};
+ createShadowBlock_(attemptToConnect) {
+ const parentBlock = this.getSourceBlock();
+ const shadowState = this.getShadowState();
+ const shadowDom = this.getShadowDom();
+ if (!parentBlock.workspace || (!shadowState && !shadowDom)) {
+ return null;
+ }
+
+ let blockShadow;
+ if (shadowState) {
+ blockShadow = blocks.appendInternal(shadowState, parentBlock.workspace, {
+ parentConnection: attemptToConnect ? this : undefined,
+ isShadow: true,
+ recordUndo: false,
+ });
+ return blockShadow;
+ }
+
+ if (shadowDom) {
+ blockShadow = Xml.domToBlock(shadowDom, parentBlock.workspace);
+ if (attemptToConnect) {
+ if (this.type === ConnectionType.INPUT_VALUE) {
+ if (!blockShadow.outputConnection) {
+ throw new Error('Shadow block is missing an output connection');
+ }
+ if (!this.connect(blockShadow.outputConnection)) {
+ throw new Error('Could not connect shadow block to connection');
+ }
+ } else if (this.type === ConnectionType.NEXT_STATEMENT) {
+ if (!blockShadow.previousConnection) {
+ throw new Error('Shadow block is missing previous connection');
+ }
+ if (!this.connect(blockShadow.previousConnection)) {
+ throw new Error('Could not connect shadow block to connection');
+ }
+ } else {
+ throw new Error(
+ 'Cannot connect a shadow block to a previous/output connection');
+ }
+ }
+ return blockShadow;
+ }
+ return null;
+ }
+
+ /**
+ * Saves the given shadow block to both the shadowDom_ and shadowState_
+ * properties, in their respective serialized forms.
+ * @param {?Block} shadow The shadow to serialize, or null.
+ * @private
+ */
+ serializeShadow_(shadow) {
+ if (!shadow) {
+ return;
+ }
+ this.shadowDom_ = /** @type {!Element} */ (Xml.blockToDom(shadow));
+ this.shadowState_ = blocks.save(shadow);
+ }
+
+ /**
+ * Returns the connection (starting at the startBlock) which will accept
+ * the given connection. This includes compatible connection types and
+ * connection checks.
+ * @param {!Block} startBlock The block on which to start the search.
+ * @param {!Connection} orphanConnection The connection that is looking
+ * for a home.
+ * @return {?Connection} The suitable connection point on the chain of
+ * blocks, or null.
+ */
+ static getConnectionForOrphanedConnection(startBlock, orphanConnection) {
+ if (orphanConnection.type === ConnectionType.OUTPUT_VALUE) {
+ return getConnectionForOrphanedOutput(
+ startBlock, orphanConnection.getSourceBlock());
+ }
+ // Otherwise we're dealing with a stack.
+ const connection = startBlock.lastConnectionInStack(true);
+ const checker = orphanConnection.getConnectionChecker();
+ if (connection && checker.canConnect(orphanConnection, connection, false)) {
+ return connection;
+ }
+ return null;
+ }
+}
/**
* Constants for checking whether two connections are compatible.
@@ -105,163 +670,6 @@ Connection.REASON_SHADOW_PARENT = 6;
Connection.REASON_DRAG_CHECKS_FAILED = 7;
Connection.REASON_PREVIOUS_AND_OUTPUT = 8;
-/**
- * Connect two connections together. This is the connection on the superior
- * block.
- * @param {!Connection} childConnection Connection on inferior block.
- * @protected
- */
-Connection.prototype.connect_ = function(childConnection) {
- const INPUT = ConnectionType.INPUT_VALUE;
- const parentConnection = this;
- const parentBlock = parentConnection.getSourceBlock();
- const childBlock = childConnection.getSourceBlock();
-
- // Make sure the childConnection is available.
- if (childConnection.isConnected()) {
- childConnection.disconnect();
- }
-
- // Make sure the parentConnection is available.
- let orphan;
- if (parentConnection.isConnected()) {
- const shadowState = parentConnection.stashShadowState_();
- const target = parentConnection.targetBlock();
- if (target.isShadow()) {
- target.dispose(false);
- } else {
- parentConnection.disconnect();
- orphan = target;
- }
- parentConnection.applyShadowState_(shadowState);
- }
-
- // Connect the new connection to the parent.
- let event;
- if (eventUtils.isEnabled()) {
- event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock);
- }
- connectReciprocally(parentConnection, childConnection);
- childBlock.setParent(parentBlock);
- if (event) {
- event.recordNew();
- eventUtils.fire(event);
- }
-
- // Deal with the orphan if it exists.
- if (orphan) {
- const orphanConnection = parentConnection.type === INPUT ?
- orphan.outputConnection :
- orphan.previousConnection;
- const connection = Connection.getConnectionForOrphanedConnection(
- childBlock, /** @type {!Connection} */ (orphanConnection));
- if (connection) {
- orphanConnection.connect(connection);
- } else {
- orphanConnection.onFailedConnect(parentConnection);
- }
- }
-};
-
-/**
- * Dispose of this connection and deal with connected blocks.
- * @package
- */
-Connection.prototype.dispose = function() {
- // isConnected returns true for shadows and non-shadows.
- if (this.isConnected()) {
- // Destroy the attached shadow block & its children (if it exists).
- this.setShadowStateInternal_();
-
- const targetBlock = this.targetBlock();
- if (targetBlock) {
- // Disconnect the attached normal block.
- targetBlock.unplug();
- }
- }
-
- this.disposed = true;
-};
-
-/**
- * Get the source block for this connection.
- * @return {!Block} The source block.
- */
-Connection.prototype.getSourceBlock = function() {
- return this.sourceBlock_;
-};
-
-/**
- * Does the connection belong to a superior block (higher in the source stack)?
- * @return {boolean} True if connection faces down or right.
- */
-Connection.prototype.isSuperior = function() {
- return this.type === ConnectionType.INPUT_VALUE ||
- this.type === ConnectionType.NEXT_STATEMENT;
-};
-
-/**
- * Is the connection connected?
- * @return {boolean} True if connection is connected to another connection.
- */
-Connection.prototype.isConnected = function() {
- return !!this.targetConnection;
-};
-
-/**
- * Get the workspace's connection type checker object.
- * @return {!IConnectionChecker} The connection type checker for the
- * source block's workspace.
- * @package
- */
-Connection.prototype.getConnectionChecker = function() {
- return this.sourceBlock_.workspace.connectionChecker;
-};
-
-/**
- * Called when an attempted connection fails. NOP by default (i.e. for headless
- * workspaces).
- * @param {!Connection} _otherConnection Connection that this connection
- * failed to connect to.
- * @package
- */
-Connection.prototype.onFailedConnect = function(_otherConnection) {
- // NOP
-};
-
-/**
- * Connect this connection to another connection.
- * @param {!Connection} otherConnection Connection to connect to.
- * @return {boolean} Whether the the blocks are now connected or not.
- */
-Connection.prototype.connect = function(otherConnection) {
- if (this.targetConnection === otherConnection) {
- // Already connected together. NOP.
- return true;
- }
-
- const checker = this.getConnectionChecker();
- if (checker.canConnect(this, otherConnection, false)) {
- const eventGroup = eventUtils.getGroup();
- if (!eventGroup) {
- eventUtils.setGroup(true);
- }
- // Determine which block is superior (higher in the source stack).
- if (this.isSuperior()) {
- // Superior block.
- this.connect_(otherConnection);
- } else {
- // Inferior block.
- otherConnection.connect_(this);
- }
- if (!eventGroup) {
- eventUtils.setGroup(false);
- }
- }
-
- return this.isConnected();
-};
-
/**
* Update two connections to target each other.
* @param {Connection} first The first connection to update.
@@ -327,404 +735,4 @@ const getConnectionForOrphanedOutput = function(startBlock, orphanBlock) {
return null;
};
-/**
- * Returns the connection (starting at the startBlock) which will accept
- * the given connection. This includes compatible connection types and
- * connection checks.
- * @param {!Block} startBlock The block on which to start the search.
- * @param {!Connection} orphanConnection The connection that is looking
- * for a home.
- * @return {?Connection} The suitable connection point on the chain of
- * blocks, or null.
- */
-Connection.getConnectionForOrphanedConnection = function(
- startBlock, orphanConnection) {
- if (orphanConnection.type === ConnectionType.OUTPUT_VALUE) {
- return getConnectionForOrphanedOutput(
- startBlock, orphanConnection.getSourceBlock());
- }
- // Otherwise we're dealing with a stack.
- const connection = startBlock.lastConnectionInStack(true);
- const checker = orphanConnection.getConnectionChecker();
- if (connection && checker.canConnect(orphanConnection, connection, false)) {
- return connection;
- }
- return null;
-};
-
-/**
- * Disconnect this connection.
- */
-Connection.prototype.disconnect = function() {
- const otherConnection = this.targetConnection;
- if (!otherConnection) {
- throw Error('Source connection not connected.');
- }
- if (otherConnection.targetConnection !== this) {
- throw Error('Target connection not connected to source connection.');
- }
- let parentBlock;
- let childBlock;
- let parentConnection;
- if (this.isSuperior()) {
- // Superior block.
- parentBlock = this.sourceBlock_;
- childBlock = otherConnection.getSourceBlock();
- parentConnection = this;
- } else {
- // Inferior block.
- parentBlock = otherConnection.getSourceBlock();
- childBlock = this.sourceBlock_;
- parentConnection = otherConnection;
- }
-
- const eventGroup = eventUtils.getGroup();
- if (!eventGroup) {
- eventUtils.setGroup(true);
- }
- this.disconnectInternal_(parentBlock, childBlock);
- if (!childBlock.isShadow()) {
- // If we were disconnecting a shadow, no need to spawn a new one.
- parentConnection.respawnShadow_();
- }
- if (!eventGroup) {
- eventUtils.setGroup(false);
- }
-};
-
-/**
- * Disconnect two blocks that are connected by this connection.
- * @param {!Block} parentBlock The superior block.
- * @param {!Block} childBlock The inferior block.
- * @protected
- */
-Connection.prototype.disconnectInternal_ = function(parentBlock, childBlock) {
- let event;
- if (eventUtils.isEnabled()) {
- event = new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock);
- }
- const otherConnection = this.targetConnection;
- otherConnection.targetConnection = null;
- this.targetConnection = null;
- childBlock.setParent(null);
- if (event) {
- event.recordNew();
- eventUtils.fire(event);
- }
-};
-
-/**
- * Respawn the shadow block if there was one connected to the this connection.
- * @protected
- */
-Connection.prototype.respawnShadow_ = function() {
- // Have to keep respawnShadow_ for backwards compatibility.
- this.createShadowBlock_(true);
-};
-
-/**
- * Returns the block that this connection connects to.
- * @return {?Block} The connected block or null if none is connected.
- */
-Connection.prototype.targetBlock = function() {
- if (this.isConnected()) {
- return this.targetConnection.getSourceBlock();
- }
- return null;
-};
-
-/**
- * Function to be called when this connection's compatible types have changed.
- * @protected
- */
-Connection.prototype.onCheckChanged_ = function() {
- // The new value type may not be compatible with the existing connection.
- if (this.isConnected() &&
- (!this.targetConnection ||
- !this.getConnectionChecker().canConnect(
- this, this.targetConnection, false))) {
- const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
- child.unplug();
- }
-};
-
-/**
- * Change a connection's compatibility.
- * @param {?(string|!Array)} check Compatible value type or list of
- * value types. Null if all types are compatible.
- * @return {!Connection} The connection being modified
- * (to allow chaining).
- */
-Connection.prototype.setCheck = function(check) {
- if (check) {
- // Ensure that check is in an array.
- if (!Array.isArray(check)) {
- check = [check];
- }
- this.check_ = check;
- this.onCheckChanged_();
- } else {
- this.check_ = null;
- }
- return this;
-};
-
-/**
- * Get a connection's compatibility.
- * @return {?Array} List of compatible value types.
- * Null if all types are compatible.
- * @public
- */
-Connection.prototype.getCheck = function() {
- return this.check_;
-};
-
-/**
- * Changes the connection's shadow block.
- * @param {?Element} shadowDom DOM representation of a block or null.
- */
-Connection.prototype.setShadowDom = function(shadowDom) {
- this.setShadowStateInternal_({shadowDom: shadowDom});
-};
-
-/**
- * Returns the xml representation of the connection's shadow block.
- * @param {boolean=} returnCurrent If true, and the shadow block is currently
- * attached to this connection, this serializes the state of that block
- * and returns it (so that field values are correct). Otherwise the saved
- * shadowDom is just returned.
- * @return {?Element} Shadow DOM representation of a block or null.
- */
-Connection.prototype.getShadowDom = function(returnCurrent) {
- return (returnCurrent && this.targetBlock().isShadow()) ?
- /** @type {!Element} */ (Xml.blockToDom(
- /** @type {!Block} */ (this.targetBlock()))) :
- this.shadowDom_;
-};
-
-/**
- * Changes the connection's shadow block.
- * @param {?blocks.State} shadowState An state represetation of the block or
- * null.
- */
-Connection.prototype.setShadowState = function(shadowState) {
- this.setShadowStateInternal_({shadowState: shadowState});
-};
-
-/**
- * Returns the serialized object representation of the connection's shadow
- * block.
- * @param {boolean=} returnCurrent If true, and the shadow block is currently
- * attached to this connection, this serializes the state of that block
- * and returns it (so that field values are correct). Otherwise the saved
- * state is just returned.
- * @return {?blocks.State} Serialized object representation of the block, or
- * null.
- */
-Connection.prototype.getShadowState = function(returnCurrent) {
- if (returnCurrent && this.targetBlock() && this.targetBlock().isShadow()) {
- return blocks.save(/** @type {!Block} */ (this.targetBlock()));
- }
- return this.shadowState_;
-};
-
-/**
- * Find all nearby compatible connections to this connection.
- * Type checking does not apply, since this function is used for bumping.
- *
- * Headless configurations (the default) do not have neighboring connection,
- * and always return an empty list (the default).
- * {@link Blockly.RenderedConnection} overrides this behavior with a list
- * computed from the rendered positioning.
- * @param {number} _maxLimit The maximum radius to another connection.
- * @return {!Array} List of connections.
- * @package
- */
-Connection.prototype.neighbours = function(_maxLimit) {
- return [];
-};
-
-/**
- * Get the parent input of a connection.
- * @return {?Input} The input that the connection belongs to or null if
- * no parent exists.
- * @package
- */
-Connection.prototype.getParentInput = function() {
- let parentInput = null;
- const inputs = this.sourceBlock_.inputList;
- for (let i = 0; i < inputs.length; i++) {
- if (inputs[i].connection === this) {
- parentInput = inputs[i];
- break;
- }
- }
- return parentInput;
-};
-
-/**
- * This method returns a string describing this Connection in developer terms
- * (English only). Intended to on be used in console logs and errors.
- * @return {string} The description.
- */
-Connection.prototype.toString = function() {
- const block = this.sourceBlock_;
- if (!block) {
- return 'Orphan Connection';
- }
- let msg;
- if (block.outputConnection === this) {
- msg = 'Output Connection of ';
- } else if (block.previousConnection === this) {
- msg = 'Previous Connection of ';
- } else if (block.nextConnection === this) {
- msg = 'Next Connection of ';
- } else {
- let parentInput = null;
- for (let i = 0, input; (input = block.inputList[i]); i++) {
- if (input.connection === this) {
- parentInput = input;
- break;
- }
- }
- if (parentInput) {
- msg = 'Input "' + parentInput.name + '" connection on ';
- } else {
- console.warn('Connection not actually connected to sourceBlock_');
- return 'Orphan Connection';
- }
- }
- return msg + block.toDevString();
-};
-
-/**
- * Returns the state of the shadowDom_ and shadowState_ properties, then
- * temporarily sets those properties to null so no shadow respawns.
- * @return {{shadowDom: ?Element, shadowState: ?blocks.State}} The state of both
- * the shadowDom_ and shadowState_ properties.
- * @private
- */
-Connection.prototype.stashShadowState_ = function() {
- const shadowDom = this.getShadowDom(true);
- const shadowState = this.getShadowState(true);
- // Set to null so it doesn't respawn.
- this.shadowDom_ = null;
- this.shadowState_ = null;
- return {shadowDom, shadowState};
-};
-
-/**
- * Reapplies the stashed state of the shadowDom_ and shadowState_ properties.
- * @param {{shadowDom: ?Element, shadowState: ?blocks.State}} param0 The state
- * to reapply to the shadowDom_ and shadowState_ properties.
- * @private
- */
-Connection.prototype.applyShadowState_ = function({shadowDom, shadowState}) {
- this.shadowDom_ = shadowDom;
- this.shadowState_ = shadowState;
-};
-
-/**
- * Sets the state of the shadow of this connection.
- * @param {{shadowDom: (?Element|undefined), shadowState:
- * (?blocks.State|undefined)}=} param0 The state to set the shadow of this
- * connection to.
- * @private
- */
-Connection.prototype.setShadowStateInternal_ = function(
- {shadowDom = null, shadowState = null} = {}) {
- // One or both of these should always be null.
- // If neither is null, the shadowState will get priority.
- this.shadowDom_ = shadowDom;
- this.shadowState_ = shadowState;
-
- const target = this.targetBlock();
- if (!target) {
- this.respawnShadow_();
- if (this.targetBlock() && this.targetBlock().isShadow()) {
- this.serializeShadow_(this.targetBlock());
- }
- } else if (target.isShadow()) {
- target.dispose(false);
- this.respawnShadow_();
- if (this.targetBlock() && this.targetBlock().isShadow()) {
- this.serializeShadow_(this.targetBlock());
- }
- } else {
- const shadow = this.createShadowBlock_(false);
- this.serializeShadow_(shadow);
- if (shadow) {
- shadow.dispose(false);
- }
- }
-};
-
-/**
- * Creates a shadow block based on the current shadowState_ or shadowDom_.
- * shadowState_ gets priority.
- * @param {boolean} attemptToConnect Whether to try to connect the shadow block
- * to this connection or not.
- * @return {?Block} The shadow block that was created, or null if both the
- * shadowState_ and shadowDom_ are null.
- * @private
- */
-Connection.prototype.createShadowBlock_ = function(attemptToConnect) {
- const parentBlock = this.getSourceBlock();
- const shadowState = this.getShadowState();
- const shadowDom = this.getShadowDom();
- if (!parentBlock.workspace || (!shadowState && !shadowDom)) {
- return null;
- }
-
- let blockShadow;
- if (shadowState) {
- blockShadow = blocks.appendInternal(shadowState, parentBlock.workspace, {
- parentConnection: attemptToConnect ? this : undefined,
- isShadow: true,
- recordUndo: false,
- });
- return blockShadow;
- }
-
- if (shadowDom) {
- blockShadow = Xml.domToBlock(shadowDom, parentBlock.workspace);
- if (attemptToConnect) {
- if (this.type === ConnectionType.INPUT_VALUE) {
- if (!blockShadow.outputConnection) {
- throw new Error('Shadow block is missing an output connection');
- }
- if (!this.connect(blockShadow.outputConnection)) {
- throw new Error('Could not connect shadow block to connection');
- }
- } else if (this.type === ConnectionType.NEXT_STATEMENT) {
- if (!blockShadow.previousConnection) {
- throw new Error('Shadow block is missing previous connection');
- }
- if (!this.connect(blockShadow.previousConnection)) {
- throw new Error('Could not connect shadow block to connection');
- }
- } else {
- throw new Error(
- 'Cannot connect a shadow block to a previous/output connection');
- }
- }
- return blockShadow;
- }
- return null;
-};
-
-/**
- * Saves the given shadow block to both the shadowDom_ and shadowState_
- * properties, in their respective serialized forms.
- * @param {?Block} shadow The shadow to serialize, or null.
- * @private
- */
-Connection.prototype.serializeShadow_ = function(shadow) {
- if (!shadow) {
- return;
- }
- this.shadowDom_ = /** @type {!Element} */ (Xml.blockToDom(shadow));
- this.shadowState_ = blocks.save(shadow);
-};
-
exports.Connection = Connection;
diff --git a/core/connection_checker.js b/core/connection_checker.js
index d512488ef..5ad5e36f5 100644
--- a/core/connection_checker.js
+++ b/core/connection_checker.js
@@ -31,282 +31,284 @@ const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
/**
* Class for connection type checking logic.
* @implements {IConnectionChecker}
- * @constructor
- * @alias Blockly.ConnectionChecker
*/
-const ConnectionChecker = function() {};
+class ConnectionChecker {
+ /**
+ * @alias Blockly.ConnectionChecker
+ */
+ constructor() {}
-/**
- * Check whether the current connection can connect with the target
- * connection.
- * @param {Connection} a Connection to check compatibility with.
- * @param {Connection} b Connection to check compatibility with.
- * @param {boolean} isDragging True if the connection is being made by dragging
- * a block.
- * @param {number=} opt_distance The max allowable distance between the
- * connections for drag checks.
- * @return {boolean} Whether the connection is legal.
- * @public
- */
-ConnectionChecker.prototype.canConnect = function(
- a, b, isDragging, opt_distance) {
- return this.canConnectWithReason(a, b, isDragging, opt_distance) ===
- Connection.CAN_CONNECT;
-};
-
-/**
- * Checks whether the current connection can connect with the target
- * connection, and return an error code if there are problems.
- * @param {Connection} a Connection to check compatibility with.
- * @param {Connection} b Connection to check compatibility with.
- * @param {boolean} isDragging True if the connection is being made by dragging
- * a block.
- * @param {number=} opt_distance The max allowable distance between the
- * connections for drag checks.
- * @return {number} Connection.CAN_CONNECT if the connection is legal,
- * an error code otherwise.
- * @public
- */
-ConnectionChecker.prototype.canConnectWithReason = function(
- a, b, isDragging, opt_distance) {
- const safety = this.doSafetyChecks(a, b);
- if (safety !== Connection.CAN_CONNECT) {
- return safety;
+ /**
+ * Check whether the current connection can connect with the target
+ * connection.
+ * @param {Connection} a Connection to check compatibility with.
+ * @param {Connection} b Connection to check compatibility with.
+ * @param {boolean} isDragging True if the connection is being made by
+ * dragging a block.
+ * @param {number=} opt_distance The max allowable distance between the
+ * connections for drag checks.
+ * @return {boolean} Whether the connection is legal.
+ * @public
+ */
+ canConnect(a, b, isDragging, opt_distance) {
+ return this.canConnectWithReason(a, b, isDragging, opt_distance) ===
+ Connection.CAN_CONNECT;
}
- // If the safety checks passed, both connections are non-null.
- const connOne = /** @type {!Connection} **/ (a);
- const connTwo = /** @type {!Connection} **/ (b);
- if (!this.doTypeChecks(connOne, connTwo)) {
- return Connection.REASON_CHECKS_FAILED;
- }
-
- if (isDragging &&
- !this.doDragChecks(
- /** @type {!RenderedConnection} **/ (a),
- /** @type {!RenderedConnection} **/ (b), opt_distance || 0)) {
- return Connection.REASON_DRAG_CHECKS_FAILED;
- }
-
- return Connection.CAN_CONNECT;
-};
-
-/**
- * Helper method that translates a connection error code into a string.
- * @param {number} errorCode The error code.
- * @param {Connection} a One of the two connections being checked.
- * @param {Connection} b The second of the two connections being
- * checked.
- * @return {string} A developer-readable error string.
- * @public
- */
-ConnectionChecker.prototype.getErrorMessage = function(errorCode, a, b) {
- switch (errorCode) {
- case Connection.REASON_SELF_CONNECTION:
- return 'Attempted to connect a block to itself.';
- case Connection.REASON_DIFFERENT_WORKSPACES:
- // Usually this means one block has been deleted.
- return 'Blocks not on same workspace.';
- case Connection.REASON_WRONG_TYPE:
- return 'Attempt to connect incompatible types.';
- case Connection.REASON_TARGET_NULL:
- return 'Target connection is null.';
- case Connection.REASON_CHECKS_FAILED: {
- const connOne = /** @type {!Connection} **/ (a);
- const connTwo = /** @type {!Connection} **/ (b);
- let msg = 'Connection checks failed. ';
- msg += connOne + ' expected ' + connOne.getCheck() + ', found ' +
- connTwo.getCheck();
- return msg;
+ /**
+ * Checks whether the current connection can connect with the target
+ * connection, and return an error code if there are problems.
+ * @param {Connection} a Connection to check compatibility with.
+ * @param {Connection} b Connection to check compatibility with.
+ * @param {boolean} isDragging True if the connection is being made by
+ * dragging a block.
+ * @param {number=} opt_distance The max allowable distance between the
+ * connections for drag checks.
+ * @return {number} Connection.CAN_CONNECT if the connection is legal,
+ * an error code otherwise.
+ * @public
+ */
+ canConnectWithReason(a, b, isDragging, opt_distance) {
+ const safety = this.doSafetyChecks(a, b);
+ if (safety !== Connection.CAN_CONNECT) {
+ return safety;
}
- case Connection.REASON_SHADOW_PARENT:
- return 'Connecting non-shadow to shadow block.';
- case Connection.REASON_DRAG_CHECKS_FAILED:
- return 'Drag checks failed.';
- case Connection.REASON_PREVIOUS_AND_OUTPUT:
- return 'Block would have an output and a previous connection.';
- default:
- return 'Unknown connection failure: this should never happen!';
- }
-};
-/**
- * Check that connecting the given connections is safe, meaning that it would
- * not break any of Blockly's basic assumptions (e.g. no self connections).
- * @param {Connection} a The first of the connections to check.
- * @param {Connection} b The second of the connections to check.
- * @return {number} An enum with the reason this connection is safe or unsafe.
- * @public
- */
-ConnectionChecker.prototype.doSafetyChecks = function(a, b) {
- if (!a || !b) {
- return Connection.REASON_TARGET_NULL;
- }
- let superiorBlock;
- let inferiorBlock;
- let superiorConnection;
- let inferiorConnection;
- if (a.isSuperior()) {
- superiorBlock = a.getSourceBlock();
- inferiorBlock = b.getSourceBlock();
- superiorConnection = a;
- inferiorConnection = b;
- } else {
- inferiorBlock = a.getSourceBlock();
- superiorBlock = b.getSourceBlock();
- inferiorConnection = a;
- superiorConnection = b;
- }
- if (superiorBlock === inferiorBlock) {
- return Connection.REASON_SELF_CONNECTION;
- } else if (
- inferiorConnection.type !==
- internalConstants.OPPOSITE_TYPE[superiorConnection.type]) {
- return Connection.REASON_WRONG_TYPE;
- } else if (superiorBlock.workspace !== inferiorBlock.workspace) {
- return Connection.REASON_DIFFERENT_WORKSPACES;
- } else if (superiorBlock.isShadow() && !inferiorBlock.isShadow()) {
- return Connection.REASON_SHADOW_PARENT;
- } else if (
- inferiorConnection.type === ConnectionType.OUTPUT_VALUE &&
- inferiorBlock.previousConnection &&
- inferiorBlock.previousConnection.isConnected()) {
- return Connection.REASON_PREVIOUS_AND_OUTPUT;
- } else if (
- inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT &&
- inferiorBlock.outputConnection &&
- inferiorBlock.outputConnection.isConnected()) {
- return Connection.REASON_PREVIOUS_AND_OUTPUT;
- }
- return Connection.CAN_CONNECT;
-};
+ // If the safety checks passed, both connections are non-null.
+ const connOne = /** @type {!Connection} **/ (a);
+ const connTwo = /** @type {!Connection} **/ (b);
+ if (!this.doTypeChecks(connOne, connTwo)) {
+ return Connection.REASON_CHECKS_FAILED;
+ }
-/**
- * Check whether this connection is compatible with another connection with
- * respect to the value type system. E.g. square_root("Hello") is not
- * compatible.
- * @param {!Connection} a Connection to compare.
- * @param {!Connection} b Connection to compare against.
- * @return {boolean} True if the connections share a type.
- * @public
- */
-ConnectionChecker.prototype.doTypeChecks = function(a, b) {
- const checkArrayOne = a.getCheck();
- const checkArrayTwo = b.getCheck();
+ if (isDragging &&
+ !this.doDragChecks(
+ /** @type {!RenderedConnection} **/ (a),
+ /** @type {!RenderedConnection} **/ (b), opt_distance || 0)) {
+ return Connection.REASON_DRAG_CHECKS_FAILED;
+ }
- if (!checkArrayOne || !checkArrayTwo) {
- // One or both sides are promiscuous enough that anything will fit.
- return true;
+ return Connection.CAN_CONNECT;
}
- // Find any intersection in the check lists.
- for (let i = 0; i < checkArrayOne.length; i++) {
- if (checkArrayTwo.indexOf(checkArrayOne[i]) !== -1) {
+
+ /**
+ * Helper method that translates a connection error code into a string.
+ * @param {number} errorCode The error code.
+ * @param {Connection} a One of the two connections being checked.
+ * @param {Connection} b The second of the two connections being
+ * checked.
+ * @return {string} A developer-readable error string.
+ * @public
+ */
+ getErrorMessage(errorCode, a, b) {
+ switch (errorCode) {
+ case Connection.REASON_SELF_CONNECTION:
+ return 'Attempted to connect a block to itself.';
+ case Connection.REASON_DIFFERENT_WORKSPACES:
+ // Usually this means one block has been deleted.
+ return 'Blocks not on same workspace.';
+ case Connection.REASON_WRONG_TYPE:
+ return 'Attempt to connect incompatible types.';
+ case Connection.REASON_TARGET_NULL:
+ return 'Target connection is null.';
+ case Connection.REASON_CHECKS_FAILED: {
+ const connOne = /** @type {!Connection} **/ (a);
+ const connTwo = /** @type {!Connection} **/ (b);
+ let msg = 'Connection checks failed. ';
+ msg += connOne + ' expected ' + connOne.getCheck() + ', found ' +
+ connTwo.getCheck();
+ return msg;
+ }
+ case Connection.REASON_SHADOW_PARENT:
+ return 'Connecting non-shadow to shadow block.';
+ case Connection.REASON_DRAG_CHECKS_FAILED:
+ return 'Drag checks failed.';
+ case Connection.REASON_PREVIOUS_AND_OUTPUT:
+ return 'Block would have an output and a previous connection.';
+ default:
+ return 'Unknown connection failure: this should never happen!';
+ }
+ }
+
+ /**
+ * Check that connecting the given connections is safe, meaning that it would
+ * not break any of Blockly's basic assumptions (e.g. no self connections).
+ * @param {Connection} a The first of the connections to check.
+ * @param {Connection} b The second of the connections to check.
+ * @return {number} An enum with the reason this connection is safe or unsafe.
+ * @public
+ */
+ doSafetyChecks(a, b) {
+ if (!a || !b) {
+ return Connection.REASON_TARGET_NULL;
+ }
+ let superiorBlock;
+ let inferiorBlock;
+ let superiorConnection;
+ let inferiorConnection;
+ if (a.isSuperior()) {
+ superiorBlock = a.getSourceBlock();
+ inferiorBlock = b.getSourceBlock();
+ superiorConnection = a;
+ inferiorConnection = b;
+ } else {
+ inferiorBlock = a.getSourceBlock();
+ superiorBlock = b.getSourceBlock();
+ inferiorConnection = a;
+ superiorConnection = b;
+ }
+ if (superiorBlock === inferiorBlock) {
+ return Connection.REASON_SELF_CONNECTION;
+ } else if (
+ inferiorConnection.type !==
+ internalConstants.OPPOSITE_TYPE[superiorConnection.type]) {
+ return Connection.REASON_WRONG_TYPE;
+ } else if (superiorBlock.workspace !== inferiorBlock.workspace) {
+ return Connection.REASON_DIFFERENT_WORKSPACES;
+ } else if (superiorBlock.isShadow() && !inferiorBlock.isShadow()) {
+ return Connection.REASON_SHADOW_PARENT;
+ } else if (
+ inferiorConnection.type === ConnectionType.OUTPUT_VALUE &&
+ inferiorBlock.previousConnection &&
+ inferiorBlock.previousConnection.isConnected()) {
+ return Connection.REASON_PREVIOUS_AND_OUTPUT;
+ } else if (
+ inferiorConnection.type === ConnectionType.PREVIOUS_STATEMENT &&
+ inferiorBlock.outputConnection &&
+ inferiorBlock.outputConnection.isConnected()) {
+ return Connection.REASON_PREVIOUS_AND_OUTPUT;
+ }
+ return Connection.CAN_CONNECT;
+ }
+
+ /**
+ * Check whether this connection is compatible with another connection with
+ * respect to the value type system. E.g. square_root("Hello") is not
+ * compatible.
+ * @param {!Connection} a Connection to compare.
+ * @param {!Connection} b Connection to compare against.
+ * @return {boolean} True if the connections share a type.
+ * @public
+ */
+ doTypeChecks(a, b) {
+ const checkArrayOne = a.getCheck();
+ const checkArrayTwo = b.getCheck();
+
+ if (!checkArrayOne || !checkArrayTwo) {
+ // One or both sides are promiscuous enough that anything will fit.
return true;
}
- }
- // No intersection.
- return false;
-};
-
-/**
- * Check whether this connection can be made by dragging.
- * @param {!RenderedConnection} a Connection to compare.
- * @param {!RenderedConnection} b Connection to compare against.
- * @param {number} distance The maximum allowable distance between connections.
- * @return {boolean} True if the connection is allowed during a drag.
- * @public
- */
-ConnectionChecker.prototype.doDragChecks = function(a, b, distance) {
- if (a.distanceFrom(b) > distance) {
+ // Find any intersection in the check lists.
+ for (let i = 0; i < checkArrayOne.length; i++) {
+ if (checkArrayTwo.indexOf(checkArrayOne[i]) !== -1) {
+ return true;
+ }
+ }
+ // No intersection.
return false;
}
- // Don't consider insertion markers.
- if (b.getSourceBlock().isInsertionMarker()) {
- return false;
- }
-
- switch (b.type) {
- case ConnectionType.PREVIOUS_STATEMENT:
- return this.canConnectToPrevious_(a, b);
- case ConnectionType.OUTPUT_VALUE: {
- // Don't offer to connect an already connected left (male) value plug to
- // an available right (female) value plug.
- if ((b.isConnected() && !b.targetBlock().isInsertionMarker()) ||
- a.isConnected()) {
- return false;
- }
- break;
- }
- case ConnectionType.INPUT_VALUE: {
- // Offering to connect the left (male) of a value block to an already
- // connected value pair is ok, we'll splice it in.
- // However, don't offer to splice into an immovable block.
- if (b.isConnected() && !b.targetBlock().isMovable() &&
- !b.targetBlock().isShadow()) {
- return false;
- }
- break;
- }
- case ConnectionType.NEXT_STATEMENT: {
- // Don't let a block with no next connection bump other blocks out of the
- // stack. But covering up a shadow block or stack of shadow blocks is
- // fine. Similarly, replacing a terminal statement with another terminal
- // statement is allowed.
- if (b.isConnected() && !a.getSourceBlock().nextConnection &&
- !b.targetBlock().isShadow() && b.targetBlock().nextConnection) {
- return false;
- }
- break;
- }
- default:
- // Unexpected connection type.
+ /**
+ * Check whether this connection can be made by dragging.
+ * @param {!RenderedConnection} a Connection to compare.
+ * @param {!RenderedConnection} b Connection to compare against.
+ * @param {number} distance The maximum allowable distance between
+ * connections.
+ * @return {boolean} True if the connection is allowed during a drag.
+ * @public
+ */
+ doDragChecks(a, b, distance) {
+ if (a.distanceFrom(b) > distance) {
return false;
- }
+ }
- // Don't let blocks try to connect to themselves or ones they nest.
- if (common.draggingConnections.indexOf(b) !== -1) {
- return false;
- }
+ // Don't consider insertion markers.
+ if (b.getSourceBlock().isInsertionMarker()) {
+ return false;
+ }
- return true;
-};
+ switch (b.type) {
+ case ConnectionType.PREVIOUS_STATEMENT:
+ return this.canConnectToPrevious_(a, b);
+ case ConnectionType.OUTPUT_VALUE: {
+ // Don't offer to connect an already connected left (male) value plug to
+ // an available right (female) value plug.
+ if ((b.isConnected() && !b.targetBlock().isInsertionMarker()) ||
+ a.isConnected()) {
+ return false;
+ }
+ break;
+ }
+ case ConnectionType.INPUT_VALUE: {
+ // Offering to connect the left (male) of a value block to an already
+ // connected value pair is ok, we'll splice it in.
+ // However, don't offer to splice into an immovable block.
+ if (b.isConnected() && !b.targetBlock().isMovable() &&
+ !b.targetBlock().isShadow()) {
+ return false;
+ }
+ break;
+ }
+ case ConnectionType.NEXT_STATEMENT: {
+ // Don't let a block with no next connection bump other blocks out of
+ // the stack. But covering up a shadow block or stack of shadow blocks
+ // is fine. Similarly, replacing a terminal statement with another
+ // terminal statement is allowed.
+ if (b.isConnected() && !a.getSourceBlock().nextConnection &&
+ !b.targetBlock().isShadow() && b.targetBlock().nextConnection) {
+ return false;
+ }
+ break;
+ }
+ default:
+ // Unexpected connection type.
+ return false;
+ }
-/**
- * Helper function for drag checking.
- * @param {!Connection} a The connection to check, which must be a
- * statement input or next connection.
- * @param {!Connection} b A nearby connection to check, which
- * must be a previous connection.
- * @return {boolean} True if the connection is allowed, false otherwise.
- * @protected
- */
-ConnectionChecker.prototype.canConnectToPrevious_ = function(a, b) {
- if (a.targetConnection) {
- // This connection is already occupied.
- // A next connection will never disconnect itself mid-drag.
- return false;
- }
+ // Don't let blocks try to connect to themselves or ones they nest.
+ if (common.draggingConnections.indexOf(b) !== -1) {
+ return false;
+ }
- // Don't let blocks try to connect to themselves or ones they nest.
- if (common.draggingConnections.indexOf(b) !== -1) {
- return false;
- }
-
- if (!b.targetConnection) {
return true;
}
- const targetBlock = b.targetBlock();
- // If it is connected to a real block, game over.
- if (!targetBlock.isInsertionMarker()) {
- return false;
+ /**
+ * Helper function for drag checking.
+ * @param {!Connection} a The connection to check, which must be a
+ * statement input or next connection.
+ * @param {!Connection} b A nearby connection to check, which
+ * must be a previous connection.
+ * @return {boolean} True if the connection is allowed, false otherwise.
+ * @protected
+ */
+ canConnectToPrevious_(a, b) {
+ if (a.targetConnection) {
+ // This connection is already occupied.
+ // A next connection will never disconnect itself mid-drag.
+ return false;
+ }
+
+ // Don't let blocks try to connect to themselves or ones they nest.
+ if (common.draggingConnections.indexOf(b) !== -1) {
+ return false;
+ }
+
+ if (!b.targetConnection) {
+ return true;
+ }
+
+ const targetBlock = b.targetBlock();
+ // If it is connected to a real block, game over.
+ if (!targetBlock.isInsertionMarker()) {
+ return false;
+ }
+ // If it's connected to an insertion marker but that insertion marker
+ // is the first block in a stack, it's still fine. If that insertion
+ // marker is in the middle of a stack, it won't work.
+ return !targetBlock.getPreviousBlock();
}
- // If it's connected to an insertion marker but that insertion marker
- // is the first block in a stack, it's still fine. If that insertion
- // marker is in the middle of a stack, it won't work.
- return !targetBlock.getPreviousBlock();
-};
+}
registry.register(
registry.Type.CONNECTION_CHECKER, registry.DEFAULT, ConnectionChecker);
diff --git a/core/connection_db.js b/core/connection_db.js
index 35c1363a1..1979b68f7 100644
--- a/core/connection_db.js
+++ b/core/connection_db.js
@@ -34,276 +34,281 @@ goog.require('Blockly.constants');
* Database of connections.
* Connections are stored in order of their vertical component. This way
* connections in an area may be looked up quickly using a binary search.
- * @param {!IConnectionChecker} checker The workspace's
- * connection type checker, used to decide if connections are valid during a
- * drag.
- * @constructor
- * @alias Blockly.ConnectionDB
*/
-const ConnectionDB = function(checker) {
+class ConnectionDB {
/**
- * Array of connections sorted by y position in workspace units.
- * @type {!Array}
+ * @param {!IConnectionChecker} checker The workspace's
+ * connection type checker, used to decide if connections are valid during
+ * a drag.
+ * @alias Blockly.ConnectionDB
+ */
+ constructor(checker) {
+ /**
+ * Array of connections sorted by y position in workspace units.
+ * @type {!Array}
+ * @private
+ */
+ this.connections_ = [];
+ /**
+ * The workspace's connection type checker, used to decide if connections
+ * are valid during a drag.
+ * @type {!IConnectionChecker}
+ * @private
+ */
+ this.connectionChecker_ = checker;
+ }
+
+ /**
+ * Add a connection to the database. Should not already exist in the database.
+ * @param {!RenderedConnection} connection The connection to be added.
+ * @param {number} yPos The y position used to decide where to insert the
+ * connection.
+ * @package
+ */
+ addConnection(connection, yPos) {
+ const index = this.calculateIndexForYPos_(yPos);
+ this.connections_.splice(index, 0, connection);
+ }
+
+ /**
+ * Finds the index of the given connection.
+ *
+ * Starts by doing a binary search to find the approximate location, then
+ * linearly searches nearby for the exact connection.
+ * @param {!RenderedConnection} conn The connection to find.
+ * @param {number} yPos The y position used to find the index of the
+ * connection.
+ * @return {number} The index of the connection, or -1 if the connection was
+ * not found.
* @private
*/
- this.connections_ = [];
- /**
- * The workspace's connection type checker, used to decide if connections are
- * valid during a drag.
- * @type {!IConnectionChecker}
- * @private
- */
- this.connectionChecker_ = checker;
-};
+ findIndexOfConnection_(conn, yPos) {
+ if (!this.connections_.length) {
+ return -1;
+ }
-/**
- * Add a connection to the database. Should not already exist in the database.
- * @param {!RenderedConnection} connection The connection to be added.
- * @param {number} yPos The y position used to decide where to insert the
- * connection.
- * @package
- */
-ConnectionDB.prototype.addConnection = function(connection, yPos) {
- const index = this.calculateIndexForYPos_(yPos);
- this.connections_.splice(index, 0, connection);
-};
+ const bestGuess = this.calculateIndexForYPos_(yPos);
+ if (bestGuess >= this.connections_.length) {
+ // Not in list
+ return -1;
+ }
-/**
- * Finds the index of the given connection.
- *
- * Starts by doing a binary search to find the approximate location, then
- * linearly searches nearby for the exact connection.
- * @param {!RenderedConnection} conn The connection to find.
- * @param {number} yPos The y position used to find the index of the connection.
- * @return {number} The index of the connection, or -1 if the connection was
- * not found.
- * @private
- */
-ConnectionDB.prototype.findIndexOfConnection_ = function(conn, yPos) {
- if (!this.connections_.length) {
+ yPos = conn.y;
+ // Walk forward and back on the y axis looking for the connection.
+ let pointer = bestGuess;
+ while (pointer >= 0 && this.connections_[pointer].y === yPos) {
+ if (this.connections_[pointer] === conn) {
+ return pointer;
+ }
+ pointer--;
+ }
+
+ pointer = bestGuess;
+ while (pointer < this.connections_.length &&
+ this.connections_[pointer].y === yPos) {
+ if (this.connections_[pointer] === conn) {
+ return pointer;
+ }
+ pointer++;
+ }
return -1;
}
- const bestGuess = this.calculateIndexForYPos_(yPos);
- if (bestGuess >= this.connections_.length) {
- // Not in list
- return -1;
- }
-
- yPos = conn.y;
- // Walk forward and back on the y axis looking for the connection.
- let pointer = bestGuess;
- while (pointer >= 0 && this.connections_[pointer].y === yPos) {
- if (this.connections_[pointer] === conn) {
- return pointer;
- }
- pointer--;
- }
-
- pointer = bestGuess;
- while (pointer < this.connections_.length &&
- this.connections_[pointer].y === yPos) {
- if (this.connections_[pointer] === conn) {
- return pointer;
- }
- pointer++;
- }
- return -1;
-};
-
-/**
- * Finds the correct index for the given y position.
- * @param {number} yPos The y position used to decide where to
- * insert the connection.
- * @return {number} The candidate index.
- * @private
- */
-ConnectionDB.prototype.calculateIndexForYPos_ = function(yPos) {
- if (!this.connections_.length) {
- return 0;
- }
- let pointerMin = 0;
- let pointerMax = this.connections_.length;
- while (pointerMin < pointerMax) {
- const pointerMid = Math.floor((pointerMin + pointerMax) / 2);
- if (this.connections_[pointerMid].y < yPos) {
- pointerMin = pointerMid + 1;
- } else if (this.connections_[pointerMid].y > yPos) {
- pointerMax = pointerMid;
- } else {
- pointerMin = pointerMid;
- break;
- }
- }
- return pointerMin;
-};
-
-/**
- * Remove a connection from the database. Must already exist in DB.
- * @param {!RenderedConnection} connection The connection to be removed.
- * @param {number} yPos The y position used to find the index of the connection.
- * @throws {Error} If the connection cannot be found in the database.
- */
-ConnectionDB.prototype.removeConnection = function(connection, yPos) {
- const index = this.findIndexOfConnection_(connection, yPos);
- if (index === -1) {
- throw Error('Unable to find connection in connectionDB.');
- }
- this.connections_.splice(index, 1);
-};
-
-/**
- * Find all nearby connections to the given connection.
- * Type checking does not apply, since this function is used for bumping.
- * @param {!RenderedConnection} connection The connection whose
- * neighbours should be returned.
- * @param {number} maxRadius The maximum radius to another connection.
- * @return {!Array} List of connections.
- */
-ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) {
- const db = this.connections_;
- const currentX = connection.x;
- const currentY = connection.y;
-
- // Binary search to find the closest y location.
- let pointerMin = 0;
- let pointerMax = db.length - 2;
- let pointerMid = pointerMax;
- while (pointerMin < pointerMid) {
- if (db[pointerMid].y < currentY) {
- pointerMin = pointerMid;
- } else {
- pointerMax = pointerMid;
- }
- pointerMid = Math.floor((pointerMin + pointerMax) / 2);
- }
-
- const neighbours = [];
/**
- * Computes if the current connection is within the allowed radius of another
- * connection.
- * This function is a closure and has access to outside variables.
- * @param {number} yIndex The other connection's index in the database.
- * @return {boolean} True if the current connection's vertical distance from
- * the other connection is less than the allowed radius.
+ * Finds the correct index for the given y position.
+ * @param {number} yPos The y position used to decide where to
+ * insert the connection.
+ * @return {number} The candidate index.
+ * @private
*/
- function checkConnection_(yIndex) {
- const dx = currentX - db[yIndex].x;
- const dy = currentY - db[yIndex].y;
- const r = Math.sqrt(dx * dx + dy * dy);
- if (r <= maxRadius) {
- neighbours.push(db[yIndex]);
+ calculateIndexForYPos_(yPos) {
+ if (!this.connections_.length) {
+ return 0;
}
- return dy < maxRadius;
+ let pointerMin = 0;
+ let pointerMax = this.connections_.length;
+ while (pointerMin < pointerMax) {
+ const pointerMid = Math.floor((pointerMin + pointerMax) / 2);
+ if (this.connections_[pointerMid].y < yPos) {
+ pointerMin = pointerMid + 1;
+ } else if (this.connections_[pointerMid].y > yPos) {
+ pointerMax = pointerMid;
+ } else {
+ pointerMin = pointerMid;
+ break;
+ }
+ }
+ return pointerMin;
}
- // Walk forward and back on the y axis looking for the closest x,y point.
- pointerMin = pointerMid;
- pointerMax = pointerMid;
- if (db.length) {
- while (pointerMin >= 0 && checkConnection_(pointerMin)) {
+ /**
+ * Remove a connection from the database. Must already exist in DB.
+ * @param {!RenderedConnection} connection The connection to be removed.
+ * @param {number} yPos The y position used to find the index of the
+ * connection.
+ * @throws {Error} If the connection cannot be found in the database.
+ */
+ removeConnection(connection, yPos) {
+ const index = this.findIndexOfConnection_(connection, yPos);
+ if (index === -1) {
+ throw Error('Unable to find connection in connectionDB.');
+ }
+ this.connections_.splice(index, 1);
+ }
+
+ /**
+ * Find all nearby connections to the given connection.
+ * Type checking does not apply, since this function is used for bumping.
+ * @param {!RenderedConnection} connection The connection whose
+ * neighbours should be returned.
+ * @param {number} maxRadius The maximum radius to another connection.
+ * @return {!Array} List of connections.
+ */
+ getNeighbours(connection, maxRadius) {
+ const db = this.connections_;
+ const currentX = connection.x;
+ const currentY = connection.y;
+
+ // Binary search to find the closest y location.
+ let pointerMin = 0;
+ let pointerMax = db.length - 2;
+ let pointerMid = pointerMax;
+ while (pointerMin < pointerMid) {
+ if (db[pointerMid].y < currentY) {
+ pointerMin = pointerMid;
+ } else {
+ pointerMax = pointerMid;
+ }
+ pointerMid = Math.floor((pointerMin + pointerMax) / 2);
+ }
+
+ const neighbours = [];
+ /**
+ * Computes if the current connection is within the allowed radius of
+ * another connection. This function is a closure and has access to outside
+ * variables.
+ * @param {number} yIndex The other connection's index in the database.
+ * @return {boolean} True if the current connection's vertical distance from
+ * the other connection is less than the allowed radius.
+ */
+ function checkConnection_(yIndex) {
+ const dx = currentX - db[yIndex].x;
+ const dy = currentY - db[yIndex].y;
+ const r = Math.sqrt(dx * dx + dy * dy);
+ if (r <= maxRadius) {
+ neighbours.push(db[yIndex]);
+ }
+ return dy < maxRadius;
+ }
+
+ // Walk forward and back on the y axis looking for the closest x,y point.
+ pointerMin = pointerMid;
+ pointerMax = pointerMid;
+ if (db.length) {
+ while (pointerMin >= 0 && checkConnection_(pointerMin)) {
+ pointerMin--;
+ }
+ do {
+ pointerMax++;
+ } while (pointerMax < db.length && checkConnection_(pointerMax));
+ }
+
+ return neighbours;
+ }
+
+ /**
+ * Is the candidate connection close to the reference connection.
+ * Extremely fast; only looks at Y distance.
+ * @param {number} index Index in database of candidate connection.
+ * @param {number} baseY Reference connection's Y value.
+ * @param {number} maxRadius The maximum radius to another connection.
+ * @return {boolean} True if connection is in range.
+ * @private
+ */
+ isInYRange_(index, baseY, maxRadius) {
+ return (Math.abs(this.connections_[index].y - baseY) <= maxRadius);
+ }
+
+ /**
+ * Find the closest compatible connection to this connection.
+ * @param {!RenderedConnection} conn The connection searching for a compatible
+ * mate.
+ * @param {number} maxRadius The maximum radius to another connection.
+ * @param {!Coordinate} dxy Offset between this connection's
+ * location in the database and the current location (as a result of
+ * dragging).
+ * @return {!{connection: RenderedConnection, radius: number}}
+ * Contains two properties: 'connection' which is either another
+ * connection or null, and 'radius' which is the distance.
+ */
+ searchForClosest(conn, maxRadius, dxy) {
+ if (!this.connections_.length) {
+ // Don't bother.
+ return {connection: null, radius: maxRadius};
+ }
+
+ // Stash the values of x and y from before the drag.
+ const baseY = conn.y;
+ const baseX = conn.x;
+
+ conn.x = baseX + dxy.x;
+ conn.y = baseY + dxy.y;
+
+ // calculateIndexForYPos_ finds an index for insertion, which is always
+ // after any block with the same y index. We want to search both forward
+ // and back, so search on both sides of the index.
+ const closestIndex = this.calculateIndexForYPos_(conn.y);
+
+ let bestConnection = null;
+ let bestRadius = maxRadius;
+ let temp;
+
+ // Walk forward and back on the y axis looking for the closest x,y point.
+ let pointerMin = closestIndex - 1;
+ while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y, maxRadius)) {
+ temp = this.connections_[pointerMin];
+ if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
+ bestConnection = temp;
+ bestRadius = temp.distanceFrom(conn);
+ }
pointerMin--;
}
- do {
+
+ let pointerMax = closestIndex;
+ while (pointerMax < this.connections_.length &&
+ this.isInYRange_(pointerMax, conn.y, maxRadius)) {
+ temp = this.connections_[pointerMax];
+ if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
+ bestConnection = temp;
+ bestRadius = temp.distanceFrom(conn);
+ }
pointerMax++;
- } while (pointerMax < db.length && checkConnection_(pointerMax));
- }
-
- return neighbours;
-};
-
-/**
- * Is the candidate connection close to the reference connection.
- * Extremely fast; only looks at Y distance.
- * @param {number} index Index in database of candidate connection.
- * @param {number} baseY Reference connection's Y value.
- * @param {number} maxRadius The maximum radius to another connection.
- * @return {boolean} True if connection is in range.
- * @private
- */
-ConnectionDB.prototype.isInYRange_ = function(index, baseY, maxRadius) {
- return (Math.abs(this.connections_[index].y - baseY) <= maxRadius);
-};
-
-/**
- * Find the closest compatible connection to this connection.
- * @param {!RenderedConnection} conn The connection searching for a compatible
- * mate.
- * @param {number} maxRadius The maximum radius to another connection.
- * @param {!Coordinate} dxy Offset between this connection's
- * location in the database and the current location (as a result of
- * dragging).
- * @return {!{connection: RenderedConnection, radius: number}}
- * Contains two properties: 'connection' which is either another
- * connection or null, and 'radius' which is the distance.
- */
-ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, dxy) {
- if (!this.connections_.length) {
- // Don't bother.
- return {connection: null, radius: maxRadius};
- }
-
- // Stash the values of x and y from before the drag.
- const baseY = conn.y;
- const baseX = conn.x;
-
- conn.x = baseX + dxy.x;
- conn.y = baseY + dxy.y;
-
- // calculateIndexForYPos_ finds an index for insertion, which is always
- // after any block with the same y index. We want to search both forward
- // and back, so search on both sides of the index.
- const closestIndex = this.calculateIndexForYPos_(conn.y);
-
- let bestConnection = null;
- let bestRadius = maxRadius;
- let temp;
-
- // Walk forward and back on the y axis looking for the closest x,y point.
- let pointerMin = closestIndex - 1;
- while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y, maxRadius)) {
- temp = this.connections_[pointerMin];
- if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
- bestConnection = temp;
- bestRadius = temp.distanceFrom(conn);
}
- pointerMin--;
+
+ // Reset the values of x and y.
+ conn.x = baseX;
+ conn.y = baseY;
+
+ // If there were no valid connections, bestConnection will be null.
+ return {connection: bestConnection, radius: bestRadius};
}
- let pointerMax = closestIndex;
- while (pointerMax < this.connections_.length &&
- this.isInYRange_(pointerMax, conn.y, maxRadius)) {
- temp = this.connections_[pointerMax];
- if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
- bestConnection = temp;
- bestRadius = temp.distanceFrom(conn);
- }
- pointerMax++;
+ /**
+ * Initialize a set of connection DBs for a workspace.
+ * @param {!IConnectionChecker} checker The workspace's
+ * connection checker, used to decide if connections are valid during a
+ * drag.
+ * @return {!Array} Array of databases.
+ */
+ static init(checker) {
+ // Create four databases, one for each connection type.
+ const dbList = [];
+ dbList[ConnectionType.INPUT_VALUE] = new ConnectionDB(checker);
+ dbList[ConnectionType.OUTPUT_VALUE] = new ConnectionDB(checker);
+ dbList[ConnectionType.NEXT_STATEMENT] = new ConnectionDB(checker);
+ dbList[ConnectionType.PREVIOUS_STATEMENT] = new ConnectionDB(checker);
+ return dbList;
}
-
- // Reset the values of x and y.
- conn.x = baseX;
- conn.y = baseY;
-
- // If there were no valid connections, bestConnection will be null.
- return {connection: bestConnection, radius: bestRadius};
-};
-
-/**
- * Initialize a set of connection DBs for a workspace.
- * @param {!IConnectionChecker} checker The workspace's
- * connection checker, used to decide if connections are valid during a
- * drag.
- * @return {!Array} Array of databases.
- */
-ConnectionDB.init = function(checker) {
- // Create four databases, one for each connection type.
- const dbList = [];
- dbList[ConnectionType.INPUT_VALUE] = new ConnectionDB(checker);
- dbList[ConnectionType.OUTPUT_VALUE] = new ConnectionDB(checker);
- dbList[ConnectionType.NEXT_STATEMENT] = new ConnectionDB(checker);
- dbList[ConnectionType.PREVIOUS_STATEMENT] = new ConnectionDB(checker);
- return dbList;
-};
+}
exports.ConnectionDB = ConnectionDB;
diff --git a/core/field_label_serializable.js b/core/field_label_serializable.js
index 4c4990f03..b8d3e027a 100644
--- a/core/field_label_serializable.js
+++ b/core/field_label_serializable.js
@@ -20,59 +20,62 @@
goog.module('Blockly.FieldLabelSerializable');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
-const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
const {FieldLabel} = goog.require('Blockly.FieldLabel');
/**
* Class for a non-editable, serializable text field.
- * @param {*} opt_value The initial value of the field. Should cast to a
- * string. Defaults to an empty string if null or undefined.
- * @param {string=} opt_class Optional CSS class for the field's text.
- * @param {Object=} opt_config A map of options used to configure the field.
- * See the [field creation documentation]{@link
- * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label-serializable#creation}
- * for a list of properties this parameter supports.
* @extends {FieldLabel}
- * @constructor
- *
- * @alias Blockly.FieldLabelSerializable
*/
-const FieldLabelSerializable = function(opt_value, opt_class, opt_config) {
- FieldLabelSerializable.superClass_.constructor.call(
- this, opt_value, opt_class, opt_config);
-};
-object.inherits(FieldLabelSerializable, FieldLabel);
+class FieldLabelSerializable extends FieldLabel {
+ /**
+ * @param {*} opt_value The initial value of the field. Should cast to a
+ * string. Defaults to an empty string if null or undefined.
+ * @param {string=} opt_class Optional CSS class for the field's text.
+ * @param {Object=} opt_config A map of options used to configure the field.
+ * See the [field creation documentation]{@link
+ * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label-serializable#creation}
+ * for a list of properties this parameter supports.
+ *
+ * @alias Blockly.FieldLabelSerializable
+ */
+ constructor(opt_value, opt_class, opt_config) {
+ const stringValue = opt_value == undefined ? '' : String(opt_value);
+ super(stringValue, opt_class, opt_config);
-/**
- * Construct a FieldLabelSerializable from a JSON arg object,
- * dereferencing any string table references.
- * @param {!Object} options A JSON object with options (text, and class).
- * @return {!FieldLabelSerializable} The new field instance.
- * @package
- * @nocollapse
- */
-FieldLabelSerializable.fromJson = function(options) {
- const text = parsing.replaceMessageReferences(options['text']);
- // `this` might be a subclass of FieldLabelSerializable if that class doesn't
- // override the static fromJson method.
- return new this(text, undefined, options);
-};
+ /**
+ * Editable fields usually show some sort of UI indicating they are
+ * editable. This field should not.
+ * @type {boolean}
+ */
+ this.EDITABLE = false;
-/**
- * Editable fields usually show some sort of UI indicating they are
- * editable. This field should not.
- * @type {boolean}
- */
-FieldLabelSerializable.prototype.EDITABLE = false;
+ /**
+ * Serializable fields are saved by the XML renderer, non-serializable
+ * fields are not. This field should be serialized, but only edited
+ * programmatically.
+ * @type {boolean}
+ */
+ this.SERIALIZABLE = true;
+ }
-/**
- * Serializable fields are saved by the XML renderer, non-serializable fields
- * are not. This field should be serialized, but only edited programmatically.
- * @type {boolean}
- */
-FieldLabelSerializable.prototype.SERIALIZABLE = true;
+ /**
+ * Construct a FieldLabelSerializable from a JSON arg object,
+ * dereferencing any string table references.
+ * @param {!Object} options A JSON object with options (text, and class).
+ * @return {!FieldLabelSerializable} The new field instance.
+ * @package
+ * @nocollapse
+ * @override
+ */
+ static fromJson(options) {
+ const text = parsing.replaceMessageReferences(options['text']);
+ // `this` might be a subclass of FieldLabelSerializable if that class
+ // doesn't override the static fromJson method.
+ return new this(text, undefined, options);
+ }
+}
fieldRegistry.register('field_label_serializable', FieldLabelSerializable);
diff --git a/core/field_multilineinput.js b/core/field_multilineinput.js
index 9f7899d6c..b03dc2d2b 100644
--- a/core/field_multilineinput.js
+++ b/core/field_multilineinput.js
@@ -20,7 +20,6 @@ const WidgetDiv = goog.require('Blockly.WidgetDiv');
const aria = goog.require('Blockly.utils.aria');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
-const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
const userAgent = goog.require('Blockly.utils.userAgent');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
@@ -31,401 +30,419 @@ const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for an editable text area field.
- * @param {string=} opt_value The initial content of the field. Should cast to a
- * string. Defaults to an empty string if null or undefined.
- * @param {Function=} opt_validator An optional function that is called
- * to validate any constraints on what the user entered. Takes the new
- * text as an argument and returns either the accepted text, a replacement
- * text, or null to abort the change.
- * @param {Object=} opt_config A map of options used to configure the field.
- * See the [field creation documentation]{@link
- * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
- * for a list of properties this parameter supports.
* @extends {FieldTextInput}
- * @constructor
- * @alias Blockly.FieldMultilineInput
*/
-const FieldMultilineInput = function(opt_value, opt_validator, opt_config) {
- FieldMultilineInput.superClass_.constructor.call(
- this, opt_value, opt_validator, opt_config);
-
+class FieldMultilineInput extends FieldTextInput {
/**
- * The SVG group element that will contain a text element for each text row
- * when initialized.
- * @type {SVGGElement}
+ * @param {string=} opt_value The initial content of the field. Should cast to
+ * a
+ * string. Defaults to an empty string if null or undefined.
+ * @param {Function=} opt_validator An optional function that is called
+ * to validate any constraints on what the user entered. Takes the new
+ * text as an argument and returns either the accepted text, a replacement
+ * text, or null to abort the change.
+ * @param {Object=} opt_config A map of options used to configure the field.
+ * See the [field creation documentation]{@link
+ * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
+ * for a list of properties this parameter supports.
+ * @alias Blockly.FieldMultilineInput
*/
- this.textGroup_ = null;
+ constructor(opt_value, opt_validator, opt_config) {
+ const stringValue = opt_value == undefined ? '' : String(opt_value);
+ super(stringValue, opt_validator, opt_config);
+
+ /**
+ * The SVG group element that will contain a text element for each text row
+ * when initialized.
+ * @type {SVGGElement}
+ */
+ this.textGroup_ = null;
+
+ /**
+ * Defines the maximum number of lines of field.
+ * If exceeded, scrolling functionality is enabled.
+ * @type {number}
+ * @protected
+ */
+ this.maxLines_ = Infinity;
+
+ /**
+ * Whether Y overflow is currently occurring.
+ * @type {boolean}
+ * @protected
+ */
+ this.isOverflowedY_ = false;
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ this.isBeingEdited_ = false;
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ this.isTextValid_ = false;
+ }
/**
- * Defines the maximum number of lines of field.
- * If exceeded, scrolling functionality is enabled.
- * @type {number}
+ * @override
+ */
+ configure_(config) {
+ super.configure_(config);
+ config.maxLines && this.setMaxLines(config.maxLines);
+ }
+
+ /**
+ * Serializes this field's value to XML. Should only be called by Blockly.Xml.
+ * @param {!Element} fieldElement The element to populate with info about the
+ * field's state.
+ * @return {!Element} The element containing info about the field's state.
+ * @package
+ */
+ toXml(fieldElement) {
+ // Replace '\n' characters with HTML-escaped equivalent '
'. This is
+ // needed so the plain-text representation of the XML produced by
+ // `Blockly.Xml.domToText` will appear on a single line (this is a
+ // limitation of the plain-text format).
+ fieldElement.textContent = this.getValue().replace(/\n/g, '
');
+ return fieldElement;
+ }
+
+ /**
+ * Sets the field's value based on the given XML element. Should only be
+ * called by Blockly.Xml.
+ * @param {!Element} fieldElement The element containing info about the
+ * field's state.
+ * @package
+ */
+ fromXml(fieldElement) {
+ this.setValue(fieldElement.textContent.replace(/
/g, '\n'));
+ }
+
+ /**
+ * Saves this field's value.
+ * @return {*} The state of this field.
+ * @package
+ */
+ saveState() {
+ const legacyState = this.saveLegacyState(FieldMultilineInput);
+ if (legacyState !== null) {
+ return legacyState;
+ }
+ return this.getValue();
+ }
+
+ /**
+ * Sets the field's value based on the given state.
+ * @param {*} state The state of the variable to assign to this variable
+ * field.
+ * @override
+ * @package
+ */
+ loadState(state) {
+ if (this.loadLegacyState(Field, state)) {
+ return;
+ }
+ this.setValue(state);
+ }
+
+ /**
+ * Create the block UI for this field.
+ * @package
+ */
+ initView() {
+ this.createBorderRect_();
+ this.textGroup_ = dom.createSvgElement(
+ Svg.G, {
+ 'class': 'blocklyEditableText',
+ },
+ this.fieldGroup_);
+ }
+
+ /**
+ * Get the text from this field as displayed on screen. May differ from
+ * getText due to ellipsis, and other formatting.
+ * @return {string} Currently displayed text.
+ * @protected
+ * @override
+ */
+ getDisplayText_() {
+ let textLines = this.getText();
+ if (!textLines) {
+ // Prevent the field from disappearing if empty.
+ return Field.NBSP;
+ }
+ const lines = textLines.split('\n');
+ textLines = '';
+ const displayLinesNumber =
+ this.isOverflowedY_ ? this.maxLines_ : lines.length;
+ for (let i = 0; i < displayLinesNumber; i++) {
+ let text = lines[i];
+ if (text.length > this.maxDisplayLength) {
+ // Truncate displayed string and add an ellipsis ('...').
+ text = text.substring(0, this.maxDisplayLength - 4) + '...';
+ } else if (this.isOverflowedY_ && i === displayLinesNumber - 1) {
+ text = text.substring(0, text.length - 3) + '...';
+ }
+ // Replace whitespace with non-breaking spaces so the text doesn't
+ // collapse.
+ text = text.replace(/\s/g, Field.NBSP);
+
+ textLines += text;
+ if (i !== displayLinesNumber - 1) {
+ textLines += '\n';
+ }
+ }
+ if (this.sourceBlock_.RTL) {
+ // The SVG is LTR, force value to be RTL.
+ textLines += '\u200F';
+ }
+ return textLines;
+ }
+
+ /**
+ * Called by setValue if the text input is valid. Updates the value of the
+ * field, and updates the text of the field if it is not currently being
+ * edited (i.e. handled by the htmlInput_). Is being redefined here to update
+ * overflow state of the field.
+ * @param {*} newValue The value to be saved. The default validator guarantees
+ * that this is a string.
* @protected
*/
- this.maxLines_ = Infinity;
+ doValueUpdate_(newValue) {
+ super.doValueUpdate_(newValue);
+ this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
+ }
/**
- * Whether Y overflow is currently occurring.
- * @type {boolean}
+ * Updates the text of the textElement.
* @protected
*/
- this.isOverflowedY_ = false;
-};
-object.inherits(FieldMultilineInput, FieldTextInput);
-
-/**
- * @override
- */
-FieldMultilineInput.prototype.configure_ = function(config) {
- FieldMultilineInput.superClass_.configure_.call(this, config);
- config.maxLines && this.setMaxLines(config.maxLines);
-};
-
-/**
- * Construct a FieldMultilineInput from a JSON arg object,
- * dereferencing any string table references.
- * @param {!Object} options A JSON object with options (text, and spellcheck).
- * @return {!FieldMultilineInput} The new field instance.
- * @package
- * @nocollapse
- */
-FieldMultilineInput.fromJson = function(options) {
- const text = parsing.replaceMessageReferences(options['text']);
- // `this` might be a subclass of FieldMultilineInput if that class doesn't
- // override the static fromJson method.
- return new this(text, undefined, options);
-};
-
-/**
- * Serializes this field's value to XML. Should only be called by Blockly.Xml.
- * @param {!Element} fieldElement The element to populate with info about the
- * field's state.
- * @return {!Element} The element containing info about the field's state.
- * @package
- */
-FieldMultilineInput.prototype.toXml = function(fieldElement) {
- // Replace '\n' characters with HTML-escaped equivalent '
'. This is
- // needed so the plain-text representation of the XML produced by
- // `Blockly.Xml.domToText` will appear on a single line (this is a limitation
- // of the plain-text format).
- fieldElement.textContent = this.getValue().replace(/\n/g, '
');
- return fieldElement;
-};
-
-/**
- * Sets the field's value based on the given XML element. Should only be
- * called by Blockly.Xml.
- * @param {!Element} fieldElement The element containing info about the
- * field's state.
- * @package
- */
-FieldMultilineInput.prototype.fromXml = function(fieldElement) {
- this.setValue(fieldElement.textContent.replace(/
/g, '\n'));
-};
-
-/**
- * Saves this field's value.
- * @return {*} The state of this field.
- * @package
- */
-FieldMultilineInput.prototype.saveState = function() {
- const legacyState = this.saveLegacyState(FieldMultilineInput);
- if (legacyState !== null) {
- return legacyState;
- }
- return this.getValue();
-};
-
-/**
- * Sets the field's value based on the given state.
- * @param {*} state The state of the variable to assign to this variable field.
- * @override
- * @package
- */
-FieldMultilineInput.prototype.loadState = function(state) {
- if (this.loadLegacyState(Field, state)) {
- return;
- }
- this.setValue(state);
-};
-
-/**
- * Create the block UI for this field.
- * @package
- */
-FieldMultilineInput.prototype.initView = function() {
- this.createBorderRect_();
- this.textGroup_ = dom.createSvgElement(
- Svg.G, {
- 'class': 'blocklyEditableText',
- },
- this.fieldGroup_);
-};
-
-/**
- * Get the text from this field as displayed on screen. May differ from getText
- * due to ellipsis, and other formatting.
- * @return {string} Currently displayed text.
- * @protected
- * @override
- */
-FieldMultilineInput.prototype.getDisplayText_ = function() {
- let textLines = this.getText();
- if (!textLines) {
- // Prevent the field from disappearing if empty.
- return Field.NBSP;
- }
- const lines = textLines.split('\n');
- textLines = '';
- const displayLinesNumber =
- this.isOverflowedY_ ? this.maxLines_ : lines.length;
- for (let i = 0; i < displayLinesNumber; i++) {
- let text = lines[i];
- if (text.length > this.maxDisplayLength) {
- // Truncate displayed string and add an ellipsis ('...').
- text = text.substring(0, this.maxDisplayLength - 4) + '...';
- } else if (this.isOverflowedY_ && i === displayLinesNumber - 1) {
- text = text.substring(0, text.length - 3) + '...';
+ render_() {
+ // Remove all text group children.
+ let currentChild;
+ while ((currentChild = this.textGroup_.firstChild)) {
+ this.textGroup_.removeChild(currentChild);
}
- // Replace whitespace with non-breaking spaces so the text doesn't collapse.
- text = text.replace(/\s/g, Field.NBSP);
- textLines += text;
- if (i !== displayLinesNumber - 1) {
- textLines += '\n';
+ // Add in text elements into the group.
+ const lines = this.getDisplayText_().split('\n');
+ let y = 0;
+ for (let i = 0; i < lines.length; i++) {
+ const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
+ this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
+ const span = dom.createSvgElement(
+ Svg.TEXT, {
+ 'class': 'blocklyText blocklyMultilineText',
+ 'x': this.getConstants().FIELD_BORDER_RECT_X_PADDING,
+ 'y': y + this.getConstants().FIELD_BORDER_RECT_Y_PADDING,
+ 'dy': this.getConstants().FIELD_TEXT_BASELINE,
+ },
+ this.textGroup_);
+ span.appendChild(document.createTextNode(lines[i]));
+ y += lineHeight;
+ }
+
+ if (this.isBeingEdited_) {
+ const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
+ if (this.isOverflowedY_) {
+ dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
+ } else {
+ dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
+ }
+ }
+
+ this.updateSize_();
+
+ if (this.isBeingEdited_) {
+ if (this.sourceBlock_.RTL) {
+ // in RTL, we need to let the browser reflow before resizing
+ // in order to get the correct bounding box of the borderRect
+ // avoiding issue #2777.
+ setTimeout(this.resizeEditor_.bind(this), 0);
+ } else {
+ this.resizeEditor_();
+ }
+ const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
+ if (!this.isTextValid_) {
+ dom.addClass(htmlInput, 'blocklyInvalidInput');
+ aria.setState(htmlInput, aria.State.INVALID, true);
+ } else {
+ dom.removeClass(htmlInput, 'blocklyInvalidInput');
+ aria.setState(htmlInput, aria.State.INVALID, false);
+ }
}
}
- if (this.sourceBlock_.RTL) {
- // The SVG is LTR, force value to be RTL.
- textLines += '\u200F';
- }
- return textLines;
-};
-/**
- * Called by setValue if the text input is valid. Updates the value of the
- * field, and updates the text of the field if it is not currently being
- * edited (i.e. handled by the htmlInput_). Is being redefined here to update
- * overflow state of the field.
- * @param {*} newValue The value to be saved. The default validator guarantees
- * that this is a string.
- * @protected
- */
-FieldMultilineInput.prototype.doValueUpdate_ = function(newValue) {
- FieldMultilineInput.superClass_.doValueUpdate_.call(this, newValue);
- this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
-};
+ /**
+ * Updates the size of the field based on the text.
+ * @protected
+ */
+ updateSize_() {
+ const nodes = this.textGroup_.childNodes;
+ let totalWidth = 0;
+ let totalHeight = 0;
+ for (let i = 0; i < nodes.length; i++) {
+ const tspan = /** @type {!Element} */ (nodes[i]);
+ const textWidth = dom.getTextWidth(tspan);
+ if (textWidth > totalWidth) {
+ totalWidth = textWidth;
+ }
+ totalHeight += this.getConstants().FIELD_TEXT_HEIGHT +
+ (i > 0 ? this.getConstants().FIELD_BORDER_RECT_Y_PADDING : 0);
+ }
+ if (this.isBeingEdited_) {
+ // The default width is based on the longest line in the display text,
+ // but when it's being edited, width should be calculated based on the
+ // absolute longest line, even if it would be truncated after editing.
+ // Otherwise we would get wrong editor width when there are more
+ // lines than this.maxLines_.
+ const actualEditorLines = this.value_.split('\n');
+ const dummyTextElement = dom.createSvgElement(
+ Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
+ const fontSize = this.getConstants().FIELD_TEXT_FONTSIZE;
+ const fontWeight = this.getConstants().FIELD_TEXT_FONTWEIGHT;
+ const fontFamily = this.getConstants().FIELD_TEXT_FONTFAMILY;
-/**
- * Updates the text of the textElement.
- * @protected
- */
-FieldMultilineInput.prototype.render_ = function() {
- // Remove all text group children.
- let currentChild;
- while ((currentChild = this.textGroup_.firstChild)) {
- this.textGroup_.removeChild(currentChild);
+ for (let i = 0; i < actualEditorLines.length; i++) {
+ if (actualEditorLines[i].length > this.maxDisplayLength) {
+ actualEditorLines[i] =
+ actualEditorLines[i].substring(0, this.maxDisplayLength);
+ }
+ dummyTextElement.textContent = actualEditorLines[i];
+ const lineWidth = dom.getFastTextWidth(
+ dummyTextElement, fontSize, fontWeight, fontFamily);
+ if (lineWidth > totalWidth) {
+ totalWidth = lineWidth;
+ }
+ }
+
+ const scrollbarWidth =
+ this.htmlInput_.offsetWidth - this.htmlInput_.clientWidth;
+ totalWidth += scrollbarWidth;
+ }
+ if (this.borderRect_) {
+ totalHeight += this.getConstants().FIELD_BORDER_RECT_Y_PADDING * 2;
+ totalWidth += this.getConstants().FIELD_BORDER_RECT_X_PADDING * 2;
+ this.borderRect_.setAttribute('width', totalWidth);
+ this.borderRect_.setAttribute('height', totalHeight);
+ }
+ this.size_.width = totalWidth;
+ this.size_.height = totalHeight;
+
+ this.positionBorderRect_();
}
- // Add in text elements into the group.
- const lines = this.getDisplayText_().split('\n');
- let y = 0;
- for (let i = 0; i < lines.length; i++) {
+ /**
+ * Show the inline free-text editor on top of the text.
+ * Overrides the default behaviour to force rerender in order to
+ * correct block size, based on editor text.
+ * @param {Event=} _opt_e Optional mouse event that triggered the field to
+ * open, or undefined if triggered programmatically.
+ * @param {boolean=} opt_quietInput True if editor should be created without
+ * focus. Defaults to false.
+ * @override
+ */
+ showEditor_(_opt_e, opt_quietInput) {
+ super.showEditor_(_opt_e, opt_quietInput);
+ this.forceRerender();
+ }
+
+ /**
+ * Create the text input editor widget.
+ * @return {!HTMLTextAreaElement} The newly created text input editor.
+ * @protected
+ */
+ widgetCreate_() {
+ const div = WidgetDiv.getDiv();
+ const scale = this.workspace_.getScale();
+
+ const htmlInput =
+ /** @type {HTMLTextAreaElement} */ (document.createElement('textarea'));
+ htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
+ htmlInput.setAttribute('spellcheck', this.spellcheck_);
+ const fontSize = (this.getConstants().FIELD_TEXT_FONTSIZE * scale) + 'pt';
+ div.style.fontSize = fontSize;
+ htmlInput.style.fontSize = fontSize;
+ const borderRadius = (FieldTextInput.BORDERRADIUS * scale) + 'px';
+ htmlInput.style.borderRadius = borderRadius;
+ const paddingX = this.getConstants().FIELD_BORDER_RECT_X_PADDING * scale;
+ const paddingY =
+ this.getConstants().FIELD_BORDER_RECT_Y_PADDING * scale / 2;
+ htmlInput.style.padding = paddingY + 'px ' + paddingX + 'px ' + paddingY +
+ 'px ' + paddingX + 'px';
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
- const span = dom.createSvgElement(
- Svg.TEXT, {
- 'class': 'blocklyText blocklyMultilineText',
- 'x': this.getConstants().FIELD_BORDER_RECT_X_PADDING,
- 'y': y + this.getConstants().FIELD_BORDER_RECT_Y_PADDING,
- 'dy': this.getConstants().FIELD_TEXT_BASELINE,
- },
- this.textGroup_);
- span.appendChild(document.createTextNode(lines[i]));
- y += lineHeight;
- }
+ htmlInput.style.lineHeight = (lineHeight * scale) + 'px';
- if (this.isBeingEdited_) {
- const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
- if (this.isOverflowedY_) {
- dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
- } else {
- dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
- }
- }
+ div.appendChild(htmlInput);
- this.updateSize_();
-
- if (this.isBeingEdited_) {
- if (this.sourceBlock_.RTL) {
- // in RTL, we need to let the browser reflow before resizing
- // in order to get the correct bounding box of the borderRect
- // avoiding issue #2777.
+ htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
+ htmlInput.untypedDefaultValue_ = this.value_;
+ htmlInput.oldValue_ = null;
+ if (userAgent.GECKO) {
+ // In FF, ensure the browser reflows before resizing to avoid issue #2777.
setTimeout(this.resizeEditor_.bind(this), 0);
} else {
this.resizeEditor_();
}
- const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
- if (!this.isTextValid_) {
- dom.addClass(htmlInput, 'blocklyInvalidInput');
- aria.setState(htmlInput, aria.State.INVALID, true);
- } else {
- dom.removeClass(htmlInput, 'blocklyInvalidInput');
- aria.setState(htmlInput, aria.State.INVALID, false);
+
+ this.bindInputEvents_(htmlInput);
+
+ return htmlInput;
+ }
+
+ /**
+ * Sets the maxLines config for this field.
+ * @param {number} maxLines Defines the maximum number of lines allowed,
+ * before scrolling functionality is enabled.
+ */
+ setMaxLines(maxLines) {
+ if (typeof maxLines === 'number' && maxLines > 0 &&
+ maxLines !== this.maxLines_) {
+ this.maxLines_ = maxLines;
+ this.forceRerender();
}
}
-};
-/**
- * Updates the size of the field based on the text.
- * @protected
- */
-FieldMultilineInput.prototype.updateSize_ = function() {
- const nodes = this.textGroup_.childNodes;
- let totalWidth = 0;
- let totalHeight = 0;
- for (let i = 0; i < nodes.length; i++) {
- const tspan = /** @type {!Element} */ (nodes[i]);
- const textWidth = dom.getTextWidth(tspan);
- if (textWidth > totalWidth) {
- totalWidth = textWidth;
+ /**
+ * Returns the maxLines config of this field.
+ * @return {number} The maxLines config value.
+ */
+ getMaxLines() {
+ return this.maxLines_;
+ }
+
+ /**
+ * Handle key down to the editor. Override the text input definition of this
+ * so as to not close the editor when enter is typed in.
+ * @param {!Event} e Keyboard event.
+ * @protected
+ */
+ onHtmlInputKeyDown_(e) {
+ if (e.keyCode !== KeyCodes.ENTER) {
+ super.onHtmlInputKeyDown_(e);
}
- totalHeight += this.getConstants().FIELD_TEXT_HEIGHT +
- (i > 0 ? this.getConstants().FIELD_BORDER_RECT_Y_PADDING : 0);
- }
- if (this.isBeingEdited_) {
- // The default width is based on the longest line in the display text,
- // but when it's being edited, width should be calculated based on the
- // absolute longest line, even if it would be truncated after editing.
- // Otherwise we would get wrong editor width when there are more
- // lines than this.maxLines_.
- const actualEditorLines = this.value_.split('\n');
- const dummyTextElement = dom.createSvgElement(
- Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
- const fontSize = this.getConstants().FIELD_TEXT_FONTSIZE;
- const fontWeight = this.getConstants().FIELD_TEXT_FONTWEIGHT;
- const fontFamily = this.getConstants().FIELD_TEXT_FONTFAMILY;
-
- for (let i = 0; i < actualEditorLines.length; i++) {
- if (actualEditorLines[i].length > this.maxDisplayLength) {
- actualEditorLines[i] =
- actualEditorLines[i].substring(0, this.maxDisplayLength);
- }
- dummyTextElement.textContent = actualEditorLines[i];
- const lineWidth = dom.getFastTextWidth(
- dummyTextElement, fontSize, fontWeight, fontFamily);
- if (lineWidth > totalWidth) {
- totalWidth = lineWidth;
- }
- }
-
- const scrollbarWidth =
- this.htmlInput_.offsetWidth - this.htmlInput_.clientWidth;
- totalWidth += scrollbarWidth;
- }
- if (this.borderRect_) {
- totalHeight += this.getConstants().FIELD_BORDER_RECT_Y_PADDING * 2;
- totalWidth += this.getConstants().FIELD_BORDER_RECT_X_PADDING * 2;
- this.borderRect_.setAttribute('width', totalWidth);
- this.borderRect_.setAttribute('height', totalHeight);
- }
- this.size_.width = totalWidth;
- this.size_.height = totalHeight;
-
- this.positionBorderRect_();
-};
-
-/**
- * Show the inline free-text editor on top of the text.
- * Overrides the default behaviour to force rerender in order to
- * correct block size, based on editor text.
- * @param {Event=} _opt_e Optional mouse event that triggered the field to open,
- * or undefined if triggered programmatically.
- * @param {boolean=} opt_quietInput True if editor should be created without
- * focus. Defaults to false.
- * @override
- */
-FieldMultilineInput.prototype.showEditor_ = function(_opt_e, opt_quietInput) {
- FieldMultilineInput.superClass_.showEditor_.call(
- this, _opt_e, opt_quietInput);
- this.forceRerender();
-};
-
-/**
- * Create the text input editor widget.
- * @return {!HTMLTextAreaElement} The newly created text input editor.
- * @protected
- */
-FieldMultilineInput.prototype.widgetCreate_ = function() {
- const div = WidgetDiv.getDiv();
- const scale = this.workspace_.getScale();
-
- const htmlInput =
- /** @type {HTMLTextAreaElement} */ (document.createElement('textarea'));
- htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
- htmlInput.setAttribute('spellcheck', this.spellcheck_);
- const fontSize = (this.getConstants().FIELD_TEXT_FONTSIZE * scale) + 'pt';
- div.style.fontSize = fontSize;
- htmlInput.style.fontSize = fontSize;
- const borderRadius = (FieldTextInput.BORDERRADIUS * scale) + 'px';
- htmlInput.style.borderRadius = borderRadius;
- const paddingX = this.getConstants().FIELD_BORDER_RECT_X_PADDING * scale;
- const paddingY = this.getConstants().FIELD_BORDER_RECT_Y_PADDING * scale / 2;
- htmlInput.style.padding =
- paddingY + 'px ' + paddingX + 'px ' + paddingY + 'px ' + paddingX + 'px';
- const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
- this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
- htmlInput.style.lineHeight = (lineHeight * scale) + 'px';
-
- div.appendChild(htmlInput);
-
- htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
- htmlInput.untypedDefaultValue_ = this.value_;
- htmlInput.oldValue_ = null;
- if (userAgent.GECKO) {
- // In FF, ensure the browser reflows before resizing to avoid issue #2777.
- setTimeout(this.resizeEditor_.bind(this), 0);
- } else {
- this.resizeEditor_();
}
- this.bindInputEvents_(htmlInput);
-
- return htmlInput;
-};
-
-/**
- * Sets the maxLines config for this field.
- * @param {number} maxLines Defines the maximum number of lines allowed,
- * before scrolling functionality is enabled.
- */
-FieldMultilineInput.prototype.setMaxLines = function(maxLines) {
- if (typeof maxLines === 'number' && maxLines > 0 &&
- maxLines !== this.maxLines_) {
- this.maxLines_ = maxLines;
- this.forceRerender();
+ /**
+ * Construct a FieldMultilineInput from a JSON arg object,
+ * dereferencing any string table references.
+ * @param {!Object} options A JSON object with options (text, and spellcheck).
+ * @return {!FieldMultilineInput} The new field instance.
+ * @package
+ * @nocollapse
+ * @override
+ */
+ static fromJson(options) {
+ const text = parsing.replaceMessageReferences(options['text']);
+ // `this` might be a subclass of FieldMultilineInput if that class doesn't
+ // override the static fromJson method.
+ return new this(text, undefined, options);
}
-};
-
-/**
- * Returns the maxLines config of this field.
- * @return {number} The maxLines config value.
- */
-FieldMultilineInput.prototype.getMaxLines = function() {
- return this.maxLines_;
-};
-
-/**
- * Handle key down to the editor. Override the text input definition of this
- * so as to not close the editor when enter is typed in.
- * @param {!Event} e Keyboard event.
- * @protected
- */
-FieldMultilineInput.prototype.onHtmlInputKeyDown_ = function(e) {
- if (e.keyCode !== KeyCodes.ENTER) {
- FieldMultilineInput.superClass_.onHtmlInputKeyDown_.call(this, e);
- }
-};
+}
/**
* CSS for multiline field. See css.js for use.
diff --git a/core/icon.js b/core/icon.js
index 3f81e9795..66a4a7299 100644
--- a/core/icon.js
+++ b/core/icon.js
@@ -29,189 +29,197 @@ const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for an icon.
- * @param {BlockSvg} block The block associated with this icon.
- * @constructor
* @abstract
- * @alias Blockly.Icon
*/
-const Icon = function(block) {
+class Icon {
/**
- * The block this icon is attached to.
- * @type {BlockSvg}
+ * @param {BlockSvg} block The block associated with this icon.
+ * @alias Blockly.Icon
+ */
+ constructor(block) {
+ /**
+ * The block this icon is attached to.
+ * @type {BlockSvg}
+ * @protected
+ */
+ this.block_ = block;
+
+ /**
+ * The icon SVG group.
+ * @type {?SVGGElement}
+ */
+ this.iconGroup_ = null;
+
+ /**
+ * Whether this icon gets hidden when the block is collapsed.
+ * @type {boolean}
+ */
+ this.collapseHidden = true;
+
+ /**
+ * Height and width of icons.
+ * @const
+ */
+ this.SIZE = 17;
+
+ /**
+ * Bubble UI (if visible).
+ * @type {?Bubble}
+ * @protected
+ */
+ this.bubble_ = null;
+
+ /**
+ * Absolute coordinate of icon's center.
+ * @type {?Coordinate}
+ * @protected
+ */
+ this.iconXY_ = null;
+ }
+
+ /**
+ * Create the icon on the block.
+ */
+ createIcon() {
+ if (this.iconGroup_) {
+ // Icon already exists.
+ return;
+ }
+ /* Here's the markup that will be generated:
+
+ ...
+
+ */
+ this.iconGroup_ =
+ dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'}, null);
+ if (this.block_.isInFlyout) {
+ dom.addClass(
+ /** @type {!Element} */ (this.iconGroup_),
+ 'blocklyIconGroupReadonly');
+ }
+ this.drawIcon_(this.iconGroup_);
+
+ this.block_.getSvgRoot().appendChild(this.iconGroup_);
+ browserEvents.conditionalBind(
+ this.iconGroup_, 'mouseup', this, this.iconClick_);
+ this.updateEditable();
+ }
+
+ /**
+ * Dispose of this icon.
+ */
+ dispose() {
+ // Dispose of and unlink the icon.
+ dom.removeNode(this.iconGroup_);
+ this.iconGroup_ = null;
+ // Dispose of and unlink the bubble.
+ this.setVisible(false);
+ this.block_ = null;
+ }
+
+ /**
+ * Add or remove the UI indicating if this icon may be clicked or not.
+ */
+ updateEditable() {
+ // No-op on the base class.
+ }
+
+ /**
+ * Is the associated bubble visible?
+ * @return {boolean} True if the bubble is visible.
+ */
+ isVisible() {
+ return !!this.bubble_;
+ }
+
+ /**
+ * Clicking on the icon toggles if the bubble is visible.
+ * @param {!Event} e Mouse click event.
* @protected
*/
- this.block_ = block;
+ iconClick_(e) {
+ if (this.block_.workspace.isDragging()) {
+ // Drag operation is concluding. Don't open the editor.
+ return;
+ }
+ if (!this.block_.isInFlyout && !browserEvents.isRightButton(e)) {
+ this.setVisible(!this.isVisible());
+ }
+ }
/**
- * The icon SVG group.
- * @type {?SVGGElement}
+ * Change the colour of the associated bubble to match its block.
*/
- this.iconGroup_ = null;
+ applyColour() {
+ if (this.isVisible()) {
+ this.bubble_.setColour(this.block_.style.colourPrimary);
+ }
+ }
/**
- * Whether this icon gets hidden when the block is collapsed.
- * @type {boolean}
+ * Notification that the icon has moved. Update the arrow accordingly.
+ * @param {!Coordinate} xy Absolute location in workspace coordinates.
*/
- this.collapseHidden = true;
+ setIconLocation(xy) {
+ this.iconXY_ = xy;
+ if (this.isVisible()) {
+ this.bubble_.setAnchorLocation(xy);
+ }
+ }
/**
- * Height and width of icons.
- * @const
+ * Notification that the icon has moved, but we don't really know where.
+ * Recompute the icon's location from scratch.
*/
- this.SIZE = 17;
+ computeIconLocation() {
+ // Find coordinates for the centre of the icon and update the arrow.
+ const blockXY = this.block_.getRelativeToSurfaceXY();
+ const iconXY = svgMath.getRelativeXY(
+ /** @type {!SVGElement} */ (this.iconGroup_));
+ const newXY = new Coordinate(
+ blockXY.x + iconXY.x + this.SIZE / 2,
+ blockXY.y + iconXY.y + this.SIZE / 2);
+ if (!Coordinate.equals(this.getIconLocation(), newXY)) {
+ this.setIconLocation(newXY);
+ }
+ }
/**
- * Bubble UI (if visible).
- * @type {?Bubble}
+ * Returns the center of the block's icon relative to the surface.
+ * @return {?Coordinate} Object with x and y properties in
+ * workspace coordinates.
+ */
+ getIconLocation() {
+ return this.iconXY_;
+ }
+
+ /**
+ * Get the size of the icon as used for rendering.
+ * This differs from the actual size of the icon, because it bulges slightly
+ * out of its row rather than increasing the height of its row.
+ * @return {!Size} Height and width.
+ */
+ getCorrectedSize() {
+ // TODO (#2562): Remove getCorrectedSize.
+ return new Size(this.SIZE, this.SIZE - 2);
+ }
+
+ /**
+ * Draw the icon.
+ * @param {!Element} _group The icon group.
* @protected
*/
- this.bubble_ = null;
+ drawIcon_(_group) {
+ // No-op on base class.
+ }
/**
- * Absolute coordinate of icon's center.
- * @type {?Coordinate}
- * @protected
+ * Show or hide the icon.
+ * @param {boolean} _visible True if the icon should be visible.
*/
- this.iconXY_ = null;
-};
-
-/**
- * Create the icon on the block.
- */
-Icon.prototype.createIcon = function() {
- if (this.iconGroup_) {
- // Icon already exists.
- return;
+ setVisible(_visible) {
+ // No-op on base class
}
- /* Here's the markup that will be generated:
-
- ...
-
- */
- this.iconGroup_ =
- dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'}, null);
- if (this.block_.isInFlyout) {
- dom.addClass(
- /** @type {!Element} */ (this.iconGroup_), 'blocklyIconGroupReadonly');
- }
- this.drawIcon_(this.iconGroup_);
-
- this.block_.getSvgRoot().appendChild(this.iconGroup_);
- browserEvents.conditionalBind(
- this.iconGroup_, 'mouseup', this, this.iconClick_);
- this.updateEditable();
-};
-
-/**
- * Dispose of this icon.
- */
-Icon.prototype.dispose = function() {
- // Dispose of and unlink the icon.
- dom.removeNode(this.iconGroup_);
- this.iconGroup_ = null;
- // Dispose of and unlink the bubble.
- this.setVisible(false);
- this.block_ = null;
-};
-
-/**
- * Add or remove the UI indicating if this icon may be clicked or not.
- */
-Icon.prototype.updateEditable = function() {
- // No-op on the base class.
-};
-
-/**
- * Is the associated bubble visible?
- * @return {boolean} True if the bubble is visible.
- */
-Icon.prototype.isVisible = function() {
- return !!this.bubble_;
-};
-
-/**
- * Clicking on the icon toggles if the bubble is visible.
- * @param {!Event} e Mouse click event.
- * @protected
- */
-Icon.prototype.iconClick_ = function(e) {
- if (this.block_.workspace.isDragging()) {
- // Drag operation is concluding. Don't open the editor.
- return;
- }
- if (!this.block_.isInFlyout && !browserEvents.isRightButton(e)) {
- this.setVisible(!this.isVisible());
- }
-};
-
-/**
- * Change the colour of the associated bubble to match its block.
- */
-Icon.prototype.applyColour = function() {
- if (this.isVisible()) {
- this.bubble_.setColour(this.block_.style.colourPrimary);
- }
-};
-
-/**
- * Notification that the icon has moved. Update the arrow accordingly.
- * @param {!Coordinate} xy Absolute location in workspace coordinates.
- */
-Icon.prototype.setIconLocation = function(xy) {
- this.iconXY_ = xy;
- if (this.isVisible()) {
- this.bubble_.setAnchorLocation(xy);
- }
-};
-
-/**
- * Notification that the icon has moved, but we don't really know where.
- * Recompute the icon's location from scratch.
- */
-Icon.prototype.computeIconLocation = function() {
- // Find coordinates for the centre of the icon and update the arrow.
- const blockXY = this.block_.getRelativeToSurfaceXY();
- const iconXY = svgMath.getRelativeXY(
- /** @type {!SVGElement} */ (this.iconGroup_));
- const newXY = new Coordinate(
- blockXY.x + iconXY.x + this.SIZE / 2,
- blockXY.y + iconXY.y + this.SIZE / 2);
- if (!Coordinate.equals(this.getIconLocation(), newXY)) {
- this.setIconLocation(newXY);
- }
-};
-
-/**
- * Returns the center of the block's icon relative to the surface.
- * @return {?Coordinate} Object with x and y properties in
- * workspace coordinates.
- */
-Icon.prototype.getIconLocation = function() {
- return this.iconXY_;
-};
-
-/**
- * Get the size of the icon as used for rendering.
- * This differs from the actual size of the icon, because it bulges slightly
- * out of its row rather than increasing the height of its row.
- * @return {!Size} Height and width.
- */
-// TODO (#2562): Remove getCorrectedSize.
-Icon.prototype.getCorrectedSize = function() {
- return new Size(this.SIZE, this.SIZE - 2);
-};
-
-/**
- * Draw the icon.
- * @param {!Element} group The icon group.
- * @protected
- */
-Icon.prototype.drawIcon_;
-
-/**
- * Show or hide the icon.
- * @param {boolean} visible True if the icon should be visible.
- */
-Icon.prototype.setVisible;
+}
exports.Icon = Icon;
diff --git a/core/marker_manager.js b/core/marker_manager.js
index 035f6a4c4..79bc384ea 100644
--- a/core/marker_manager.js
+++ b/core/marker_manager.js
@@ -25,40 +25,183 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/**
* Class to manage the multiple markers and the cursor on a workspace.
- * @param {!WorkspaceSvg} workspace The workspace for the marker manager.
- * @constructor
- * @alias Blockly.MarkerManager
- * @package
*/
-const MarkerManager = function(workspace) {
+class MarkerManager {
/**
- * The cursor.
- * @type {?Cursor}
- * @private
+ * @param {!WorkspaceSvg} workspace The workspace for the marker manager.
+ * @alias Blockly.MarkerManager
+ * @package
*/
- this.cursor_ = null;
+ constructor(workspace) {
+ /**
+ * The cursor.
+ * @type {?Cursor}
+ * @private
+ */
+ this.cursor_ = null;
+
+ /**
+ * The cursor's SVG element.
+ * @type {?SVGElement}
+ * @private
+ */
+ this.cursorSvg_ = null;
+
+ /**
+ * The map of markers for the workspace.
+ * @type {!Object}
+ * @private
+ */
+ this.markers_ = Object.create(null);
+
+ /**
+ * The workspace this marker manager is associated with.
+ * @type {!WorkspaceSvg}
+ * @private
+ */
+ this.workspace_ = workspace;
+
+ /**
+ * The marker's SVG element.
+ * @type {?SVGElement}
+ * @private
+ */
+ this.markerSvg_ = null;
+ }
/**
- * The cursor's SVG element.
- * @type {?SVGElement}
- * @private
+ * Register the marker by adding it to the map of markers.
+ * @param {string} id A unique identifier for the marker.
+ * @param {!Marker} marker The marker to register.
*/
- this.cursorSvg_ = null;
+ registerMarker(id, marker) {
+ if (this.markers_[id]) {
+ this.unregisterMarker(id);
+ }
+ marker.setDrawer(this.workspace_.getRenderer().makeMarkerDrawer(
+ this.workspace_, marker));
+ this.setMarkerSvg(marker.getDrawer().createDom());
+ this.markers_[id] = marker;
+ }
/**
- * The map of markers for the workspace.
- * @type {!Object}
- * @private
+ * Unregister the marker by removing it from the map of markers.
+ * @param {string} id The ID of the marker to unregister.
*/
- this.markers_ = Object.create(null);
+ unregisterMarker(id) {
+ const marker = this.markers_[id];
+ if (marker) {
+ marker.dispose();
+ delete this.markers_[id];
+ } else {
+ throw Error(
+ 'Marker with ID ' + id + ' does not exist. ' +
+ 'Can only unregister markers that exist.');
+ }
+ }
/**
- * The workspace this marker manager is associated with.
- * @type {!WorkspaceSvg}
- * @private
+ * Get the cursor for the workspace.
+ * @return {?Cursor} The cursor for this workspace.
*/
- this.workspace_ = workspace;
-};
+ getCursor() {
+ return this.cursor_;
+ }
+
+ /**
+ * Get a single marker that corresponds to the given ID.
+ * @param {string} id A unique identifier for the marker.
+ * @return {?Marker} The marker that corresponds to the given ID,
+ * or null if none exists.
+ */
+ getMarker(id) {
+ return this.markers_[id] || null;
+ }
+
+ /**
+ * Sets the cursor and initializes the drawer for use with keyboard
+ * navigation.
+ * @param {Cursor} cursor The cursor used to move around this workspace.
+ */
+ setCursor(cursor) {
+ if (this.cursor_ && this.cursor_.getDrawer()) {
+ this.cursor_.getDrawer().dispose();
+ }
+ this.cursor_ = cursor;
+ if (this.cursor_) {
+ const drawer = this.workspace_.getRenderer().makeMarkerDrawer(
+ this.workspace_, this.cursor_);
+ this.cursor_.setDrawer(drawer);
+ this.setCursorSvg(this.cursor_.getDrawer().createDom());
+ }
+ }
+
+ /**
+ * Add the cursor SVG to this workspace SVG group.
+ * @param {?SVGElement} cursorSvg The SVG root of the cursor to be added to
+ * the workspace SVG group.
+ * @package
+ */
+ setCursorSvg(cursorSvg) {
+ if (!cursorSvg) {
+ this.cursorSvg_ = null;
+ return;
+ }
+
+ this.workspace_.getBlockCanvas().appendChild(cursorSvg);
+ this.cursorSvg_ = cursorSvg;
+ }
+
+ /**
+ * Add the marker SVG to this workspaces SVG group.
+ * @param {?SVGElement} markerSvg The SVG root of the marker to be added to
+ * the workspace SVG group.
+ * @package
+ */
+ setMarkerSvg(markerSvg) {
+ if (!markerSvg) {
+ this.markerSvg_ = null;
+ return;
+ }
+
+ if (this.workspace_.getBlockCanvas()) {
+ if (this.cursorSvg_) {
+ this.workspace_.getBlockCanvas().insertBefore(
+ markerSvg, this.cursorSvg_);
+ } else {
+ this.workspace_.getBlockCanvas().appendChild(markerSvg);
+ }
+ }
+ }
+
+ /**
+ * Redraw the attached cursor SVG if needed.
+ * @package
+ */
+ updateMarkers() {
+ if (this.workspace_.keyboardAccessibilityMode && this.cursorSvg_) {
+ this.workspace_.getCursor().draw();
+ }
+ }
+
+ /**
+ * Dispose of the marker manager.
+ * Go through and delete all markers associated with this marker manager.
+ * @suppress {checkTypes}
+ * @package
+ */
+ dispose() {
+ const markerIds = Object.keys(this.markers_);
+ for (let i = 0, markerId; (markerId = markerIds[i]); i++) {
+ this.unregisterMarker(markerId);
+ }
+ this.markers_ = null;
+ if (this.cursor_) {
+ this.cursor_.dispose();
+ this.cursor_ = null;
+ }
+ }
+}
/**
* The name of the local marker.
@@ -67,135 +210,4 @@ const MarkerManager = function(workspace) {
*/
MarkerManager.LOCAL_MARKER = 'local_marker_1';
-/**
- * Register the marker by adding it to the map of markers.
- * @param {string} id A unique identifier for the marker.
- * @param {!Marker} marker The marker to register.
- */
-MarkerManager.prototype.registerMarker = function(id, marker) {
- if (this.markers_[id]) {
- this.unregisterMarker(id);
- }
- marker.setDrawer(
- this.workspace_.getRenderer().makeMarkerDrawer(this.workspace_, marker));
- this.setMarkerSvg(marker.getDrawer().createDom());
- this.markers_[id] = marker;
-};
-
-/**
- * Unregister the marker by removing it from the map of markers.
- * @param {string} id The ID of the marker to unregister.
- */
-MarkerManager.prototype.unregisterMarker = function(id) {
- const marker = this.markers_[id];
- if (marker) {
- marker.dispose();
- delete this.markers_[id];
- } else {
- throw Error(
- 'Marker with ID ' + id + ' does not exist. ' +
- 'Can only unregister markers that exist.');
- }
-};
-
-/**
- * Get the cursor for the workspace.
- * @return {?Cursor} The cursor for this workspace.
- */
-MarkerManager.prototype.getCursor = function() {
- return this.cursor_;
-};
-
-/**
- * Get a single marker that corresponds to the given ID.
- * @param {string} id A unique identifier for the marker.
- * @return {?Marker} The marker that corresponds to the given ID,
- * or null if none exists.
- */
-MarkerManager.prototype.getMarker = function(id) {
- return this.markers_[id] || null;
-};
-
-/**
- * Sets the cursor and initializes the drawer for use with keyboard navigation.
- * @param {Cursor} cursor The cursor used to move around this workspace.
- */
-MarkerManager.prototype.setCursor = function(cursor) {
- if (this.cursor_ && this.cursor_.getDrawer()) {
- this.cursor_.getDrawer().dispose();
- }
- this.cursor_ = cursor;
- if (this.cursor_) {
- const drawer = this.workspace_.getRenderer().makeMarkerDrawer(
- this.workspace_, this.cursor_);
- this.cursor_.setDrawer(drawer);
- this.setCursorSvg(this.cursor_.getDrawer().createDom());
- }
-};
-
-/**
- * Add the cursor SVG to this workspace SVG group.
- * @param {?SVGElement} cursorSvg The SVG root of the cursor to be added to the
- * workspace SVG group.
- * @package
- */
-MarkerManager.prototype.setCursorSvg = function(cursorSvg) {
- if (!cursorSvg) {
- this.cursorSvg_ = null;
- return;
- }
-
- this.workspace_.getBlockCanvas().appendChild(cursorSvg);
- this.cursorSvg_ = cursorSvg;
-};
-
-/**
- * Add the marker SVG to this workspaces SVG group.
- * @param {?SVGElement} markerSvg The SVG root of the marker to be added to the
- * workspace SVG group.
- * @package
- */
-MarkerManager.prototype.setMarkerSvg = function(markerSvg) {
- if (!markerSvg) {
- this.markerSvg_ = null;
- return;
- }
-
- if (this.workspace_.getBlockCanvas()) {
- if (this.cursorSvg_) {
- this.workspace_.getBlockCanvas().insertBefore(markerSvg, this.cursorSvg_);
- } else {
- this.workspace_.getBlockCanvas().appendChild(markerSvg);
- }
- }
-};
-
-/**
- * Redraw the attached cursor SVG if needed.
- * @package
- */
-MarkerManager.prototype.updateMarkers = function() {
- if (this.workspace_.keyboardAccessibilityMode && this.cursorSvg_) {
- this.workspace_.getCursor().draw();
- }
-};
-
-/**
- * Dispose of the marker manager.
- * Go through and delete all markers associated with this marker manager.
- * @suppress {checkTypes}
- * @package
- */
-MarkerManager.prototype.dispose = function() {
- const markerIds = Object.keys(this.markers_);
- for (let i = 0, markerId; (markerId = markerIds[i]); i++) {
- this.unregisterMarker(markerId);
- }
- this.markers_ = null;
- if (this.cursor_) {
- this.cursor_.dispose();
- this.cursor_ = null;
- }
-};
-
exports.MarkerManager = MarkerManager;
diff --git a/core/mutator.js b/core/mutator.js
index d1c2ba07d..97adf1b43 100644
--- a/core/mutator.js
+++ b/core/mutator.js
@@ -22,7 +22,6 @@ const Abstract = goog.requireType('Blockly.Events.Abstract');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const internalConstants = goog.require('Blockly.internalConstants');
-const object = goog.require('Blockly.utils.object');
const toolbox = goog.require('Blockly.utils.toolbox');
const xml = goog.require('Blockly.utils.xml');
const {BlockChange} = goog.require('Blockly.Events.BlockChange');
@@ -49,526 +48,529 @@ goog.require('Blockly.Events.BubbleOpen');
/**
* Class for a mutator dialog.
- * @param {!Array} quarkNames List of names of sub-blocks for flyout.
* @extends {Icon}
- * @constructor
- * @alias Blockly.Mutator
*/
-const Mutator = function(quarkNames) {
- Mutator.superClass_.constructor.call(this, null);
- this.quarkNames_ = quarkNames;
-
+class Mutator extends Icon {
/**
- * Workspace in the mutator's bubble.
- * @type {?WorkspaceSvg}
- * @private
+ * @param {!Array} quarkNames List of names of sub-blocks for flyout.
+ * @alias Blockly.Mutator
*/
- this.workspace_ = null;
+ constructor(quarkNames) {
+ super(null);
+ this.quarkNames_ = quarkNames;
- /**
- * Width of workspace.
- * @type {number}
- * @private
- */
- this.workspaceWidth_ = 0;
+ /**
+ * Workspace in the mutator's bubble.
+ * @type {?WorkspaceSvg}
+ * @private
+ */
+ this.workspace_ = null;
- /**
- * Height of workspace.
- * @type {number}
- * @private
- */
- this.workspaceHeight_ = 0;
+ /**
+ * Width of workspace.
+ * @type {number}
+ * @private
+ */
+ this.workspaceWidth_ = 0;
- /**
- * The SVG element that is the parent of the mutator workspace, or null if
- * not created.
- * @type {?SVGSVGElement}
- * @private
- */
- this.svgDialog_ = null;
+ /**
+ * Height of workspace.
+ * @type {number}
+ * @private
+ */
+ this.workspaceHeight_ = 0;
- /**
- * The root block of the mutator workspace, created by decomposing the source
- * block.
- * @type {?BlockSvg}
- * @private
- */
- this.rootBlock_ = null;
+ /**
+ * The SVG element that is the parent of the mutator workspace, or null if
+ * not created.
+ * @type {?SVGSVGElement}
+ * @private
+ */
+ this.svgDialog_ = null;
- /**
- * Function registered on the main workspace to update the mutator contents
- * when the main workspace changes.
- * @type {?Function}
- * @private
- */
- this.sourceListener_ = null;
-};
-object.inherits(Mutator, Icon);
+ /**
+ * The root block of the mutator workspace, created by decomposing the
+ * source block.
+ * @type {?BlockSvg}
+ * @private
+ */
+ this.rootBlock_ = null;
-/**
- * Set the block this mutator is associated with.
- * @param {!BlockSvg} block The block associated with this mutator.
- * @package
- */
-Mutator.prototype.setBlock = function(block) {
- this.block_ = block;
-};
-
-/**
- * Returns the workspace inside this mutator icon's bubble.
- * @return {?WorkspaceSvg} The workspace inside this mutator icon's
- * bubble or null if the mutator isn't open.
- * @package
- */
-Mutator.prototype.getWorkspace = function() {
- return this.workspace_;
-};
-
-/**
- * Draw the mutator icon.
- * @param {!Element} group The icon group.
- * @protected
- */
-Mutator.prototype.drawIcon_ = function(group) {
- // Square with rounded corners.
- dom.createSvgElement(
- Svg.RECT, {
- 'class': 'blocklyIconShape',
- 'rx': '4',
- 'ry': '4',
- 'height': '16',
- 'width': '16',
- },
- group);
- // Gear teeth.
- dom.createSvgElement(
- Svg.PATH, {
- 'class': 'blocklyIconSymbol',
- 'd': 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,' +
- '0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,' +
- '-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,' +
- '-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,' +
- '-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 ' +
- '-0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,' +
- '0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z',
- },
- group);
- // Axle hole.
- dom.createSvgElement(
- Svg.CIRCLE,
- {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, group);
-};
-
-/**
- * Clicking on the icon toggles if the mutator bubble is visible.
- * Disable if block is uneditable.
- * @param {!Event} e Mouse click event.
- * @protected
- * @override
- */
-Mutator.prototype.iconClick_ = function(e) {
- if (this.block_.isEditable()) {
- Icon.prototype.iconClick_.call(this, e);
+ /**
+ * Function registered on the main workspace to update the mutator contents
+ * when the main workspace changes.
+ * @type {?Function}
+ * @private
+ */
+ this.sourceListener_ = null;
}
-};
-/**
- * Create the editor for the mutator's bubble.
- * @return {!SVGElement} The top-level node of the editor.
- * @private
- */
-Mutator.prototype.createEditor_ = function() {
- /* Create the editor. Here's the markup that will be generated:
-
- */
- this.svgDialog_ = dom.createSvgElement(
- Svg.SVG, {'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH}, null);
- // Convert the list of names into a list of XML objects for the flyout.
- let quarkXml;
- if (this.quarkNames_.length) {
- quarkXml = xml.createElement('xml');
- for (let i = 0, quarkName; (quarkName = this.quarkNames_[i]); i++) {
- const element = xml.createElement('block');
- element.setAttribute('type', quarkName);
- quarkXml.appendChild(element);
- }
- } else {
- quarkXml = null;
+ /**
+ * Set the block this mutator is associated with.
+ * @param {!BlockSvg} block The block associated with this mutator.
+ * @package
+ */
+ setBlock(block) {
+ this.block_ = block;
}
- const workspaceOptions = new Options(
- /** @type {!BlocklyOptions} */
- ({
- // If you want to enable disabling, also remove the
- // event filter from workspaceChanged_ .
- 'disable': false,
- 'parentWorkspace': this.block_.workspace,
- 'media': this.block_.workspace.options.pathToMedia,
- 'rtl': this.block_.RTL,
- 'horizontalLayout': false,
- 'renderer': this.block_.workspace.options.renderer,
- 'rendererOverrides': this.block_.workspace.options.rendererOverrides,
- }));
- workspaceOptions.toolboxPosition =
- this.block_.RTL ? toolbox.Position.RIGHT : toolbox.Position.LEFT;
- const hasFlyout = !!quarkXml;
- if (hasFlyout) {
- workspaceOptions.languageTree = toolbox.convertToolboxDefToJson(quarkXml);
+
+ /**
+ * Returns the workspace inside this mutator icon's bubble.
+ * @return {?WorkspaceSvg} The workspace inside this mutator icon's
+ * bubble or null if the mutator isn't open.
+ * @package
+ */
+ getWorkspace() {
+ return this.workspace_;
}
- this.workspace_ = new WorkspaceSvg(workspaceOptions);
- this.workspace_.isMutator = true;
- this.workspace_.addChangeListener(eventUtils.disableOrphans);
- // Mutator flyouts go inside the mutator workspace's rather than in
- // a top level SVG. Instead of handling scale themselves, mutators
- // inherit scale from the parent workspace.
- // To fix this, scale needs to be applied at a different level in the DOM.
- const flyoutSvg = hasFlyout ? this.workspace_.addFlyout(Svg.G) : null;
- const background = this.workspace_.createDom('blocklyMutatorBackground');
-
- if (flyoutSvg) {
- // Insert the flyout after the but before the block canvas so that
- // the flyout is underneath in z-order. This makes blocks layering during
- // dragging work properly.
- background.insertBefore(flyoutSvg, this.workspace_.svgBlockCanvas_);
+ /**
+ * Draw the mutator icon.
+ * @param {!Element} group The icon group.
+ * @protected
+ */
+ drawIcon_(group) {
+ // Square with rounded corners.
+ dom.createSvgElement(
+ Svg.RECT, {
+ 'class': 'blocklyIconShape',
+ 'rx': '4',
+ 'ry': '4',
+ 'height': '16',
+ 'width': '16',
+ },
+ group);
+ // Gear teeth.
+ dom.createSvgElement(
+ Svg.PATH, {
+ 'class': 'blocklyIconSymbol',
+ 'd': 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,' +
+ '0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,' +
+ '-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,' +
+ '-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,' +
+ '-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 ' +
+ '-0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,' +
+ '0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z',
+ },
+ group);
+ // Axle hole.
+ dom.createSvgElement(
+ Svg.CIRCLE,
+ {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, group);
}
- this.svgDialog_.appendChild(background);
- return this.svgDialog_;
-};
-
-/**
- * Add or remove the UI indicating if this icon may be clicked or not.
- */
-Mutator.prototype.updateEditable = function() {
- Mutator.superClass_.updateEditable.call(this);
- if (!this.block_.isInFlyout) {
+ /**
+ * Clicking on the icon toggles if the mutator bubble is visible.
+ * Disable if block is uneditable.
+ * @param {!Event} e Mouse click event.
+ * @protected
+ * @override
+ */
+ iconClick_(e) {
if (this.block_.isEditable()) {
- if (this.iconGroup_) {
- dom.removeClass(
- /** @type {!Element} */ (this.iconGroup_),
- 'blocklyIconGroupReadonly');
+ Icon.prototype.iconClick_.call(this, e);
+ }
+ }
+
+ /**
+ * Create the editor for the mutator's bubble.
+ * @return {!SVGElement} The top-level node of the editor.
+ * @private
+ */
+ createEditor_() {
+ /* Create the editor. Here's the markup that will be generated:
+
+ */
+ this.svgDialog_ = dom.createSvgElement(
+ Svg.SVG, {'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH}, null);
+ // Convert the list of names into a list of XML objects for the flyout.
+ let quarkXml;
+ if (this.quarkNames_.length) {
+ quarkXml = xml.createElement('xml');
+ for (let i = 0, quarkName; (quarkName = this.quarkNames_[i]); i++) {
+ const element = xml.createElement('block');
+ element.setAttribute('type', quarkName);
+ quarkXml.appendChild(element);
}
} else {
- // Close any mutator bubble. Icon is not clickable.
- this.setVisible(false);
- if (this.iconGroup_) {
- dom.addClass(
- /** @type {!Element} */ (this.iconGroup_),
- 'blocklyIconGroupReadonly');
+ quarkXml = null;
+ }
+ const workspaceOptions = new Options(
+ /** @type {!BlocklyOptions} */
+ ({
+ // If you want to enable disabling, also remove the
+ // event filter from workspaceChanged_ .
+ 'disable': false,
+ 'parentWorkspace': this.block_.workspace,
+ 'media': this.block_.workspace.options.pathToMedia,
+ 'rtl': this.block_.RTL,
+ 'horizontalLayout': false,
+ 'renderer': this.block_.workspace.options.renderer,
+ 'rendererOverrides': this.block_.workspace.options.rendererOverrides,
+ }));
+ workspaceOptions.toolboxPosition =
+ this.block_.RTL ? toolbox.Position.RIGHT : toolbox.Position.LEFT;
+ const hasFlyout = !!quarkXml;
+ if (hasFlyout) {
+ workspaceOptions.languageTree = toolbox.convertToolboxDefToJson(quarkXml);
+ }
+ this.workspace_ = new WorkspaceSvg(workspaceOptions);
+ this.workspace_.isMutator = true;
+ this.workspace_.addChangeListener(eventUtils.disableOrphans);
+
+ // Mutator flyouts go inside the mutator workspace's rather than in
+ // a top level SVG. Instead of handling scale themselves, mutators
+ // inherit scale from the parent workspace.
+ // To fix this, scale needs to be applied at a different level in the DOM.
+ const flyoutSvg = hasFlyout ? this.workspace_.addFlyout(Svg.G) : null;
+ const background = this.workspace_.createDom('blocklyMutatorBackground');
+
+ if (flyoutSvg) {
+ // Insert the flyout after the but before the block canvas so that
+ // the flyout is underneath in z-order. This makes blocks layering during
+ // dragging work properly.
+ background.insertBefore(flyoutSvg, this.workspace_.svgBlockCanvas_);
+ }
+ this.svgDialog_.appendChild(background);
+
+ return this.svgDialog_;
+ }
+
+ /**
+ * Add or remove the UI indicating if this icon may be clicked or not.
+ */
+ updateEditable() {
+ super.updateEditable();
+ if (!this.block_.isInFlyout) {
+ if (this.block_.isEditable()) {
+ if (this.iconGroup_) {
+ dom.removeClass(
+ /** @type {!Element} */ (this.iconGroup_),
+ 'blocklyIconGroupReadonly');
+ }
+ } else {
+ // Close any mutator bubble. Icon is not clickable.
+ this.setVisible(false);
+ if (this.iconGroup_) {
+ dom.addClass(
+ /** @type {!Element} */ (this.iconGroup_),
+ 'blocklyIconGroupReadonly');
+ }
}
}
}
-};
-/**
- * Resize the bubble to match the size of the workspace.
- * @private
- */
-Mutator.prototype.resizeBubble_ = function() {
- const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
- const workspaceSize = this.workspace_.getCanvas().getBBox();
- let width = workspaceSize.width + workspaceSize.x;
- let height = workspaceSize.height + doubleBorderWidth * 3;
- const flyout = this.workspace_.getFlyout();
- if (flyout) {
- const flyoutScrollMetrics =
- flyout.getWorkspace().getMetricsManager().getScrollMetrics();
- height = Math.max(height, flyoutScrollMetrics.height + 20);
- width += flyout.getWidth();
- }
- if (this.block_.RTL) {
- width = -workspaceSize.x;
- }
- width += doubleBorderWidth * 3;
- // Only resize if the size difference is significant. Eliminates shuddering.
- if (Math.abs(this.workspaceWidth_ - width) > doubleBorderWidth ||
- Math.abs(this.workspaceHeight_ - height) > doubleBorderWidth) {
- // Record some layout information for workspace metrics.
- this.workspaceWidth_ = width;
- this.workspaceHeight_ = height;
- // Resize the bubble.
- this.bubble_.setBubbleSize(
- width + doubleBorderWidth, height + doubleBorderWidth);
- this.svgDialog_.setAttribute('width', this.workspaceWidth_);
- this.svgDialog_.setAttribute('height', this.workspaceHeight_);
- this.workspace_.setCachedParentSvgSize(
- this.workspaceWidth_, this.workspaceHeight_);
- }
-
- if (this.block_.RTL) {
- // Scroll the workspace to always left-align.
- const translation = 'translate(' + this.workspaceWidth_ + ',0)';
- this.workspace_.getCanvas().setAttribute('transform', translation);
- }
- this.workspace_.resize();
-};
-
-/**
- * A method handler for when the bubble is moved.
- * @private
- */
-Mutator.prototype.onBubbleMove_ = function() {
- if (this.workspace_) {
- this.workspace_.recordDragTargets();
- }
-};
-
-/**
- * Show or hide the mutator bubble.
- * @param {boolean} visible True if the bubble should be visible.
- */
-Mutator.prototype.setVisible = function(visible) {
- if (visible === this.isVisible()) {
- // No change.
- return;
- }
- eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
- this.block_, visible, 'mutator'));
- if (visible) {
- // Create the bubble.
- this.bubble_ = new Bubble(
- /** @type {!WorkspaceSvg} */ (this.block_.workspace),
- this.createEditor_(), this.block_.pathObject.svgPath,
- /** @type {!Coordinate} */ (this.iconXY_), null, null);
- // Expose this mutator's block's ID on its top-level SVG group.
- this.bubble_.setSvgId(this.block_.id);
- this.bubble_.registerMoveEvent(this.onBubbleMove_.bind(this));
- const tree = this.workspace_.options.languageTree;
+ /**
+ * Resize the bubble to match the size of the workspace.
+ * @private
+ */
+ resizeBubble_() {
+ const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
+ const workspaceSize = this.workspace_.getCanvas().getBBox();
+ let width = workspaceSize.width + workspaceSize.x;
+ let height = workspaceSize.height + doubleBorderWidth * 3;
const flyout = this.workspace_.getFlyout();
- if (tree) {
- flyout.init(this.workspace_);
- flyout.show(tree);
- }
-
- this.rootBlock_ = this.block_.decompose(this.workspace_);
- const blocks = this.rootBlock_.getDescendants(false);
- for (let i = 0, child; (child = blocks[i]); i++) {
- child.render();
- }
- // The root block should not be draggable or deletable.
- this.rootBlock_.setMovable(false);
- this.rootBlock_.setDeletable(false);
- let margin;
- let x;
if (flyout) {
- margin = flyout.CORNER_RADIUS * 2;
- x = this.rootBlock_.RTL ? flyout.getWidth() + margin : margin;
- } else {
- margin = 16;
- x = margin;
+ const flyoutScrollMetrics =
+ flyout.getWorkspace().getMetricsManager().getScrollMetrics();
+ height = Math.max(height, flyoutScrollMetrics.height + 20);
+ width += flyout.getWidth();
}
if (this.block_.RTL) {
- x = -x;
+ width = -workspaceSize.x;
}
- this.rootBlock_.moveBy(x, margin);
- // Save the initial connections, then listen for further changes.
- if (this.block_.saveConnections) {
- const thisRootBlock = this.rootBlock_;
- const mutatorBlock =
- /** @type {{saveConnections: function(!Block)}} */ (this.block_);
- mutatorBlock.saveConnections(this.rootBlock_);
- this.sourceListener_ = function() {
- mutatorBlock.saveConnections(thisRootBlock);
- };
- this.block_.workspace.addChangeListener(this.sourceListener_);
+ width += doubleBorderWidth * 3;
+ // Only resize if the size difference is significant. Eliminates
+ // shuddering.
+ if (Math.abs(this.workspaceWidth_ - width) > doubleBorderWidth ||
+ Math.abs(this.workspaceHeight_ - height) > doubleBorderWidth) {
+ // Record some layout information for workspace metrics.
+ this.workspaceWidth_ = width;
+ this.workspaceHeight_ = height;
+ // Resize the bubble.
+ this.bubble_.setBubbleSize(
+ width + doubleBorderWidth, height + doubleBorderWidth);
+ this.svgDialog_.setAttribute('width', this.workspaceWidth_);
+ this.svgDialog_.setAttribute('height', this.workspaceHeight_);
+ this.workspace_.setCachedParentSvgSize(
+ this.workspaceWidth_, this.workspaceHeight_);
}
- this.resizeBubble_();
- // When the mutator's workspace changes, update the source block.
- this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));
- // Update the source block immediately after the bubble becomes visible.
- this.updateWorkspace_();
- this.applyColour();
- } else {
- // Dispose of the bubble.
- this.svgDialog_ = null;
- this.workspace_.dispose();
- this.workspace_ = null;
- this.rootBlock_ = null;
- this.bubble_.dispose();
- this.bubble_ = null;
- this.workspaceWidth_ = 0;
- this.workspaceHeight_ = 0;
- if (this.sourceListener_) {
- this.block_.workspace.removeChangeListener(this.sourceListener_);
- this.sourceListener_ = null;
+
+ if (this.block_.RTL) {
+ // Scroll the workspace to always left-align.
+ const translation = 'translate(' + this.workspaceWidth_ + ',0)';
+ this.workspace_.getCanvas().setAttribute('transform', translation);
+ }
+ this.workspace_.resize();
+ }
+
+ /**
+ * A method handler for when the bubble is moved.
+ * @private
+ */
+ onBubbleMove_() {
+ if (this.workspace_) {
+ this.workspace_.recordDragTargets();
}
}
-};
-/**
- * Fired whenever a change is made to the mutator's workspace.
- * @param {!Abstract} e Custom data for event.
- * @private
- */
-Mutator.prototype.workspaceChanged_ = function(e) {
- if (!(e.isUiEvent ||
- (e.type === eventUtils.CHANGE && e.element === 'disabled') ||
- e.type === eventUtils.CREATE)) {
- this.updateWorkspace_();
- }
-};
-
-/**
- * Updates the source block when the mutator's blocks are changed.
- * Bump down any block that's too high.
- * @private
- */
-Mutator.prototype.updateWorkspace_ = function() {
- if (!this.workspace_.isDragging()) {
- const blocks = this.workspace_.getTopBlocks(false);
- const MARGIN = 20;
-
- for (let b = 0, block; (block = blocks[b]); b++) {
- const blockXY = block.getRelativeToSurfaceXY();
-
- // Bump any block that's above the top back inside.
- if (blockXY.y < MARGIN) {
- block.moveBy(0, MARGIN - blockXY.y);
+ /**
+ * Show or hide the mutator bubble.
+ * @param {boolean} visible True if the bubble should be visible.
+ */
+ setVisible(visible) {
+ if (visible === this.isVisible()) {
+ // No change.
+ return;
+ }
+ eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
+ this.block_, visible, 'mutator'));
+ if (visible) {
+ // Create the bubble.
+ this.bubble_ = new Bubble(
+ /** @type {!WorkspaceSvg} */ (this.block_.workspace),
+ this.createEditor_(), this.block_.pathObject.svgPath,
+ /** @type {!Coordinate} */ (this.iconXY_), null, null);
+ // Expose this mutator's block's ID on its top-level SVG group.
+ this.bubble_.setSvgId(this.block_.id);
+ this.bubble_.registerMoveEvent(this.onBubbleMove_.bind(this));
+ const tree = this.workspace_.options.languageTree;
+ const flyout = this.workspace_.getFlyout();
+ if (tree) {
+ flyout.init(this.workspace_);
+ flyout.show(tree);
}
- // Bump any block overlapping the flyout back inside.
- if (block.RTL) {
- let right = -MARGIN;
- const flyout = this.workspace_.getFlyout();
- if (flyout) {
- right -= flyout.getWidth();
- }
- if (blockXY.x > right) {
- block.moveBy(right - blockXY.x, 0);
- }
- } else if (blockXY.x < MARGIN) {
- block.moveBy(MARGIN - blockXY.x, 0);
+
+ this.rootBlock_ = this.block_.decompose(this.workspace_);
+ const blocks = this.rootBlock_.getDescendants(false);
+ for (let i = 0, child; (child = blocks[i]); i++) {
+ child.render();
+ }
+ // The root block should not be draggable or deletable.
+ this.rootBlock_.setMovable(false);
+ this.rootBlock_.setDeletable(false);
+ let margin;
+ let x;
+ if (flyout) {
+ margin = flyout.CORNER_RADIUS * 2;
+ x = this.rootBlock_.RTL ? flyout.getWidth() + margin : margin;
+ } else {
+ margin = 16;
+ x = margin;
+ }
+ if (this.block_.RTL) {
+ x = -x;
+ }
+ this.rootBlock_.moveBy(x, margin);
+ // Save the initial connections, then listen for further changes.
+ if (this.block_.saveConnections) {
+ const thisRootBlock = this.rootBlock_;
+ const mutatorBlock =
+ /** @type {{saveConnections: function(!Block)}} */ (this.block_);
+ mutatorBlock.saveConnections(this.rootBlock_);
+ this.sourceListener_ = function() {
+ mutatorBlock.saveConnections(thisRootBlock);
+ };
+ this.block_.workspace.addChangeListener(this.sourceListener_);
}
- }
- }
-
- // When the mutator's workspace changes, update the source block.
- if (this.rootBlock_.workspace === this.workspace_) {
- const existingGroup = eventUtils.getGroup();
- if (!existingGroup) {
- eventUtils.setGroup(true);
- }
- const block = /** @type {!BlockSvg} */ (this.block_);
- const oldExtraState = BlockChange.getExtraBlockState_(block);
-
- // Switch off rendering while the source block is rebuilt.
- const savedRendered = block.rendered;
- // TODO(#4288): We should not be setting the rendered property to false.
- block.rendered = false;
-
- // Allow the source block to rebuild itself.
- block.compose(this.rootBlock_);
- // Restore rendering and show the changes.
- block.rendered = savedRendered;
- // Mutation may have added some elements that need initializing.
- block.initSvg();
-
- if (block.rendered) {
- block.render();
- }
-
- const newExtraState = BlockChange.getExtraBlockState_(block);
- if (oldExtraState !== newExtraState) {
- eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
- block, 'mutation', null, oldExtraState, newExtraState));
- // Ensure that any bump is part of this mutation's event group.
- const mutationGroup = eventUtils.getGroup();
- setTimeout(function() {
- const oldGroup = eventUtils.getGroup();
- eventUtils.setGroup(mutationGroup);
- block.bumpNeighbours();
- eventUtils.setGroup(oldGroup);
- }, internalConstants.BUMP_DELAY);
- }
-
- // Don't update the bubble until the drag has ended, to avoid moving blocks
- // under the cursor.
- if (!this.workspace_.isDragging()) {
this.resizeBubble_();
+ // When the mutator's workspace changes, update the source block.
+ this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));
+ // Update the source block immediately after the bubble becomes visible.
+ this.updateWorkspace_();
+ this.applyColour();
+ } else {
+ // Dispose of the bubble.
+ this.svgDialog_ = null;
+ this.workspace_.dispose();
+ this.workspace_ = null;
+ this.rootBlock_ = null;
+ this.bubble_.dispose();
+ this.bubble_ = null;
+ this.workspaceWidth_ = 0;
+ this.workspaceHeight_ = 0;
+ if (this.sourceListener_) {
+ this.block_.workspace.removeChangeListener(this.sourceListener_);
+ this.sourceListener_ = null;
+ }
}
- eventUtils.setGroup(existingGroup);
}
-};
-/**
- * Dispose of this mutator.
- */
-Mutator.prototype.dispose = function() {
- this.block_.mutator = null;
- Icon.prototype.dispose.call(this);
-};
+ /**
+ * Fired whenever a change is made to the mutator's workspace.
+ * @param {!Abstract} e Custom data for event.
+ * @private
+ */
+ workspaceChanged_(e) {
+ if (!(e.isUiEvent ||
+ (e.type === eventUtils.CHANGE && e.element === 'disabled') ||
+ e.type === eventUtils.CREATE)) {
+ this.updateWorkspace_();
+ }
+ }
-/**
- * Update the styles on all blocks in the mutator.
- * @public
- */
-Mutator.prototype.updateBlockStyle = function() {
- const ws = this.workspace_;
+ /**
+ * Updates the source block when the mutator's blocks are changed.
+ * Bump down any block that's too high.
+ * @private
+ */
+ updateWorkspace_() {
+ if (!this.workspace_.isDragging()) {
+ const blocks = this.workspace_.getTopBlocks(false);
+ const MARGIN = 20;
- if (ws && ws.getAllBlocks(false)) {
- const workspaceBlocks = ws.getAllBlocks(false);
- for (let i = 0, block; (block = workspaceBlocks[i]); i++) {
- block.setStyle(block.getStyleName());
+ for (let b = 0, block; (block = blocks[b]); b++) {
+ const blockXY = block.getRelativeToSurfaceXY();
+
+ // Bump any block that's above the top back inside.
+ if (blockXY.y < MARGIN) {
+ block.moveBy(0, MARGIN - blockXY.y);
+ }
+ // Bump any block overlapping the flyout back inside.
+ if (block.RTL) {
+ let right = -MARGIN;
+ const flyout = this.workspace_.getFlyout();
+ if (flyout) {
+ right -= flyout.getWidth();
+ }
+ if (blockXY.x > right) {
+ block.moveBy(right - blockXY.x, 0);
+ }
+ } else if (blockXY.x < MARGIN) {
+ block.moveBy(MARGIN - blockXY.x, 0);
+ }
+ }
}
- const flyout = ws.getFlyout();
- if (flyout) {
- const flyoutBlocks = flyout.workspace_.getAllBlocks(false);
- for (let i = 0, block; (block = flyoutBlocks[i]); i++) {
+ // When the mutator's workspace changes, update the source block.
+ if (this.rootBlock_.workspace === this.workspace_) {
+ const existingGroup = eventUtils.getGroup();
+ if (!existingGroup) {
+ eventUtils.setGroup(true);
+ }
+ const block = /** @type {!BlockSvg} */ (this.block_);
+ const oldExtraState = BlockChange.getExtraBlockState_(block);
+
+ // Switch off rendering while the source block is rebuilt.
+ const savedRendered = block.rendered;
+ // TODO(#4288): We should not be setting the rendered property to false.
+ block.rendered = false;
+
+ // Allow the source block to rebuild itself.
+ block.compose(this.rootBlock_);
+ // Restore rendering and show the changes.
+ block.rendered = savedRendered;
+ // Mutation may have added some elements that need initializing.
+ block.initSvg();
+
+ if (block.rendered) {
+ block.render();
+ }
+
+ const newExtraState = BlockChange.getExtraBlockState_(block);
+ if (oldExtraState !== newExtraState) {
+ eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
+ block, 'mutation', null, oldExtraState, newExtraState));
+ // Ensure that any bump is part of this mutation's event group.
+ const mutationGroup = eventUtils.getGroup();
+ setTimeout(function() {
+ const oldGroup = eventUtils.getGroup();
+ eventUtils.setGroup(mutationGroup);
+ block.bumpNeighbours();
+ eventUtils.setGroup(oldGroup);
+ }, internalConstants.BUMP_DELAY);
+ }
+
+ // Don't update the bubble until the drag has ended, to avoid moving
+ // blocks under the cursor.
+ if (!this.workspace_.isDragging()) {
+ this.resizeBubble_();
+ }
+ eventUtils.setGroup(existingGroup);
+ }
+ }
+
+ /**
+ * Dispose of this mutator.
+ */
+ dispose() {
+ this.block_.mutator = null;
+ Icon.prototype.dispose.call(this);
+ }
+
+ /**
+ * Update the styles on all blocks in the mutator.
+ * @public
+ */
+ updateBlockStyle() {
+ const ws = this.workspace_;
+
+ if (ws && ws.getAllBlocks(false)) {
+ const workspaceBlocks = ws.getAllBlocks(false);
+ for (let i = 0, block; (block = workspaceBlocks[i]); i++) {
block.setStyle(block.getStyleName());
}
- }
- }
-};
-/**
- * Reconnect an block to a mutated input.
- * @param {Connection} connectionChild Connection on child block.
- * @param {!Block} block Parent block.
- * @param {string} inputName Name of input on parent block.
- * @return {boolean} True iff a reconnection was made, false otherwise.
- */
-Mutator.reconnect = function(connectionChild, block, inputName) {
- if (!connectionChild || !connectionChild.getSourceBlock().workspace) {
- return false; // No connection or block has been deleted.
- }
- const connectionParent = block.getInput(inputName).connection;
- const currentParent = connectionChild.targetBlock();
- if ((!currentParent || currentParent === block) &&
- connectionParent.targetConnection !== connectionChild) {
- if (connectionParent.isConnected()) {
- // There's already something connected here. Get rid of it.
- connectionParent.disconnect();
- }
- connectionParent.connect(connectionChild);
- return true;
- }
- return false;
-};
-
-/**
- * Get the parent workspace of a workspace that is inside a mutator, taking into
- * account whether it is a flyout.
- * @param {Workspace} workspace The workspace that is inside a mutator.
- * @return {?Workspace} The mutator's parent workspace or null.
- * @public
- */
-Mutator.findParentWs = function(workspace) {
- let outerWs = null;
- if (workspace && workspace.options) {
- const parent = workspace.options.parentWorkspace;
- // If we were in a flyout in a mutator, need to go up two levels to find
- // the actual parent.
- if (workspace.isFlyout) {
- if (parent && parent.options) {
- outerWs = parent.options.parentWorkspace;
+ const flyout = ws.getFlyout();
+ if (flyout) {
+ const flyoutBlocks = flyout.workspace_.getAllBlocks(false);
+ for (let i = 0, block; (block = flyoutBlocks[i]); i++) {
+ block.setStyle(block.getStyleName());
+ }
}
- } else if (parent) {
- outerWs = parent;
}
}
- return outerWs;
-};
+
+ /**
+ * Reconnect an block to a mutated input.
+ * @param {Connection} connectionChild Connection on child block.
+ * @param {!Block} block Parent block.
+ * @param {string} inputName Name of input on parent block.
+ * @return {boolean} True iff a reconnection was made, false otherwise.
+ */
+ static reconnect(connectionChild, block, inputName) {
+ if (!connectionChild || !connectionChild.getSourceBlock().workspace) {
+ return false; // No connection or block has been deleted.
+ }
+ const connectionParent = block.getInput(inputName).connection;
+ const currentParent = connectionChild.targetBlock();
+ if ((!currentParent || currentParent === block) &&
+ connectionParent.targetConnection !== connectionChild) {
+ if (connectionParent.isConnected()) {
+ // There's already something connected here. Get rid of it.
+ connectionParent.disconnect();
+ }
+ connectionParent.connect(connectionChild);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get the parent workspace of a workspace that is inside a mutator, taking
+ * into account whether it is a flyout.
+ * @param {Workspace} workspace The workspace that is inside a mutator.
+ * @return {?Workspace} The mutator's parent workspace or null.
+ * @public
+ */
+ static findParentWs(workspace) {
+ let outerWs = null;
+ if (workspace && workspace.options) {
+ const parent = workspace.options.parentWorkspace;
+ // If we were in a flyout in a mutator, need to go up two levels to find
+ // the actual parent.
+ if (workspace.isFlyout) {
+ if (parent && parent.options) {
+ outerWs = parent.options.parentWorkspace;
+ }
+ } else if (parent) {
+ outerWs = parent;
+ }
+ }
+ return outerWs;
+ }
+}
exports.Mutator = Mutator;
diff --git a/core/rendered_connection.js b/core/rendered_connection.js
index 08d6529ec..a85ac3c95 100644
--- a/core/rendered_connection.js
+++ b/core/rendered_connection.js
@@ -19,7 +19,6 @@ const common = goog.require('Blockly.common');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const internalConstants = goog.require('Blockly.internalConstants');
-const object = goog.require('Blockly.utils.object');
const svgPaths = goog.require('Blockly.utils.svgPaths');
const svgMath = goog.require('Blockly.utils.svgMath');
/* eslint-disable-next-line no-unused-vars */
@@ -35,53 +34,527 @@ const {Svg} = goog.require('Blockly.utils.Svg');
/**
- * Class for a connection between blocks that may be rendered on screen.
- * @param {!BlockSvg} source The block establishing this connection.
- * @param {number} type The type of the connection.
- * @extends {Connection}
- * @constructor
- * @alias Blockly.RenderedConnection
+ * Maximum randomness in workspace units for bumping a block.
+ * @const
*/
-const RenderedConnection = function(source, type) {
- RenderedConnection.superClass_.constructor.call(this, source, type);
+const BUMP_RANDOMNESS = 10;
+
+/**
+ * Class for a connection between blocks that may be rendered on screen.
+ * @extends {Connection}
+ */
+class RenderedConnection extends Connection {
+ /**
+ * @param {!BlockSvg} source The block establishing this connection.
+ * @param {number} type The type of the connection.
+ * @alias Blockly.RenderedConnection
+ */
+ constructor(source, type) {
+ super(source, type);
+
+ /**
+ * Connection database for connections of this type on the current
+ * workspace.
+ * @const {!ConnectionDB}
+ * @private
+ */
+ this.db_ = source.workspace.connectionDBList[type];
+
+ /**
+ * Connection database for connections compatible with this type on the
+ * current workspace.
+ * @const {!ConnectionDB}
+ * @private
+ */
+ this.dbOpposite_ =
+ source.workspace
+ .connectionDBList[internalConstants.OPPOSITE_TYPE[type]];
+
+ /**
+ * Workspace units, (0, 0) is top left of block.
+ * @type {!Coordinate}
+ * @private
+ */
+ this.offsetInBlock_ = new Coordinate(0, 0);
+
+ /**
+ * Describes the state of this connection's tracked-ness.
+ * @type {RenderedConnection.TrackedState}
+ * @private
+ */
+ this.trackedState_ = RenderedConnection.TrackedState.WILL_TRACK;
+
+ /**
+ * Connection this connection connects to. Null if not connected.
+ * @type {RenderedConnection}
+ */
+ this.targetConnection = null;
+ }
/**
- * Connection database for connections of this type on the current workspace.
- * @const {!ConnectionDB}
- * @private
+ * Dispose of this connection. Remove it from the database (if it is
+ * tracked) and call the super-function to deal with connected blocks.
+ * @override
+ * @package
*/
- this.db_ = source.workspace.connectionDBList[type];
+ dispose() {
+ super.dispose();
+ if (this.trackedState_ === RenderedConnection.TrackedState.TRACKED) {
+ this.db_.removeConnection(this, this.y);
+ }
+ }
/**
- * Connection database for connections compatible with this type on the
- * current workspace.
- * @const {!ConnectionDB}
- * @private
+ * Get the source block for this connection.
+ * @return {!BlockSvg} The source block.
+ * @override
*/
- this.dbOpposite_ =
- source.workspace.connectionDBList[internalConstants.OPPOSITE_TYPE[type]];
+ getSourceBlock() {
+ return /** @type {!BlockSvg} */ (super.getSourceBlock());
+ }
/**
- * Workspace units, (0, 0) is top left of block.
- * @type {!Coordinate}
- * @private
+ * Returns the block that this connection connects to.
+ * @return {?BlockSvg} The connected block or null if none is connected.
+ * @override
*/
- this.offsetInBlock_ = new Coordinate(0, 0);
+ targetBlock() {
+ return /** @type {BlockSvg} */ (super.targetBlock());
+ }
/**
- * Describes the state of this connection's tracked-ness.
- * @type {RenderedConnection.TrackedState}
- * @private
+ * Returns the distance between this connection and another connection in
+ * workspace units.
+ * @param {!Connection} otherConnection The other connection to measure
+ * the distance to.
+ * @return {number} The distance between connections, in workspace units.
*/
- this.trackedState_ = RenderedConnection.TrackedState.WILL_TRACK;
+ distanceFrom(otherConnection) {
+ const xDiff = this.x - otherConnection.x;
+ const yDiff = this.y - otherConnection.y;
+ return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
+ }
/**
- * Connection this connection connects to. Null if not connected.
- * @type {RenderedConnection}
+ * Move the block(s) belonging to the connection to a point where they don't
+ * visually interfere with the specified connection.
+ * @param {!Connection} staticConnection The connection to move away
+ * from.
+ * @package
*/
- this.targetConnection = null;
-};
-object.inherits(RenderedConnection, Connection);
+ bumpAwayFrom(staticConnection) {
+ if (this.sourceBlock_.workspace.isDragging()) {
+ // Don't move blocks around while the user is doing the same.
+ return;
+ }
+ // Move the root block.
+ let rootBlock = this.sourceBlock_.getRootBlock();
+ if (rootBlock.isInFlyout) {
+ // Don't move blocks around in a flyout.
+ return;
+ }
+ let reverse = false;
+ if (!rootBlock.isMovable()) {
+ // Can't bump an uneditable block away.
+ // Check to see if the other block is movable.
+ rootBlock = staticConnection.getSourceBlock().getRootBlock();
+ if (!rootBlock.isMovable()) {
+ return;
+ }
+ // Swap the connections and move the 'static' connection instead.
+ staticConnection = this;
+ reverse = true;
+ }
+ // Raise it to the top for extra visibility.
+ const selected = common.getSelected() == rootBlock;
+ selected || rootBlock.addSelect();
+ let dx = (staticConnection.x + internalConstants.SNAP_RADIUS +
+ Math.floor(Math.random() * BUMP_RANDOMNESS)) -
+ this.x;
+ let dy = (staticConnection.y + internalConstants.SNAP_RADIUS +
+ Math.floor(Math.random() * BUMP_RANDOMNESS)) -
+ this.y;
+ if (reverse) {
+ // When reversing a bump due to an uneditable block, bump up.
+ dy = -dy;
+ }
+ if (rootBlock.RTL) {
+ dx = (staticConnection.x - internalConstants.SNAP_RADIUS -
+ Math.floor(Math.random() * BUMP_RANDOMNESS)) -
+ this.x;
+ }
+ rootBlock.moveBy(dx, dy);
+ selected || rootBlock.removeSelect();
+ }
+
+ /**
+ * Change the connection's coordinates.
+ * @param {number} x New absolute x coordinate, in workspace coordinates.
+ * @param {number} y New absolute y coordinate, in workspace coordinates.
+ */
+ moveTo(x, y) {
+ if (this.trackedState_ === RenderedConnection.TrackedState.WILL_TRACK) {
+ this.db_.addConnection(this, y);
+ this.trackedState_ = RenderedConnection.TrackedState.TRACKED;
+ } else if (this.trackedState_ === RenderedConnection.TrackedState.TRACKED) {
+ this.db_.removeConnection(this, this.y);
+ this.db_.addConnection(this, y);
+ }
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Change the connection's coordinates.
+ * @param {number} dx Change to x coordinate, in workspace units.
+ * @param {number} dy Change to y coordinate, in workspace units.
+ */
+ moveBy(dx, dy) {
+ this.moveTo(this.x + dx, this.y + dy);
+ }
+
+ /**
+ * Move this connection to the location given by its offset within the block
+ * and the location of the block's top left corner.
+ * @param {!Coordinate} blockTL The location of the top left
+ * corner of the block, in workspace coordinates.
+ */
+ moveToOffset(blockTL) {
+ this.moveTo(
+ blockTL.x + this.offsetInBlock_.x, blockTL.y + this.offsetInBlock_.y);
+ }
+
+ /**
+ * Set the offset of this connection relative to the top left of its block.
+ * @param {number} x The new relative x, in workspace units.
+ * @param {number} y The new relative y, in workspace units.
+ */
+ setOffsetInBlock(x, y) {
+ this.offsetInBlock_.x = x;
+ this.offsetInBlock_.y = y;
+ }
+
+ /**
+ * Get the offset of this connection relative to the top left of its block.
+ * @return {!Coordinate} The offset of the connection.
+ * @package
+ */
+ getOffsetInBlock() {
+ return this.offsetInBlock_;
+ }
+
+ /**
+ * Move the blocks on either side of this connection right next to each other.
+ * @package
+ */
+ tighten() {
+ const dx = this.targetConnection.x - this.x;
+ const dy = this.targetConnection.y - this.y;
+ if (dx !== 0 || dy !== 0) {
+ const block = this.targetBlock();
+ const svgRoot = block.getSvgRoot();
+ if (!svgRoot) {
+ throw Error('block is not rendered.');
+ }
+ // Workspace coordinates.
+ const xy = svgMath.getRelativeXY(svgRoot);
+ block.getSvgRoot().setAttribute(
+ 'transform', 'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')');
+ block.moveConnections(-dx, -dy);
+ }
+ }
+
+ /**
+ * Find the closest compatible connection to this connection.
+ * All parameters are in workspace units.
+ * @param {number} maxLimit The maximum radius to another connection.
+ * @param {!Coordinate} dxy Offset between this connection's location
+ * in the database and the current location (as a result of dragging).
+ * @return {!{connection: ?Connection, radius: number}} Contains two
+ * properties: 'connection' which is either another connection or null,
+ * and 'radius' which is the distance.
+ */
+ closest(maxLimit, dxy) {
+ return this.dbOpposite_.searchForClosest(this, maxLimit, dxy);
+ }
+
+ /**
+ * Add highlighting around this connection.
+ */
+ highlight() {
+ let steps;
+ const sourceBlockSvg = /** @type {!BlockSvg} */ (this.sourceBlock_);
+ const renderConstants =
+ sourceBlockSvg.workspace.getRenderer().getConstants();
+ const shape = renderConstants.shapeFor(this);
+ if (this.type === ConnectionType.INPUT_VALUE ||
+ this.type === ConnectionType.OUTPUT_VALUE) {
+ // Vertical line, puzzle tab, vertical line.
+ const yLen = renderConstants.TAB_OFFSET_FROM_TOP;
+ steps = svgPaths.moveBy(0, -yLen) + svgPaths.lineOnAxis('v', yLen) +
+ shape.pathDown + svgPaths.lineOnAxis('v', yLen);
+ } else {
+ const xLen =
+ renderConstants.NOTCH_OFFSET_LEFT - renderConstants.CORNER_RADIUS;
+ // Horizontal line, notch, horizontal line.
+ steps = svgPaths.moveBy(-xLen, 0) + svgPaths.lineOnAxis('h', xLen) +
+ shape.pathLeft + svgPaths.lineOnAxis('h', xLen);
+ }
+ const xy = this.sourceBlock_.getRelativeToSurfaceXY();
+ const x = this.x - xy.x;
+ const y = this.y - xy.y;
+ Connection.highlightedPath_ = dom.createSvgElement(
+ Svg.PATH, {
+ 'class': 'blocklyHighlightedConnectionPath',
+ 'd': steps,
+ 'transform': 'translate(' + x + ',' + y + ')' +
+ (this.sourceBlock_.RTL ? ' scale(-1 1)' : ''),
+ },
+ this.sourceBlock_.getSvgRoot());
+ }
+
+ /**
+ * Remove the highlighting around this connection.
+ */
+ unhighlight() {
+ dom.removeNode(Connection.highlightedPath_);
+ delete Connection.highlightedPath_;
+ }
+
+ /**
+ * Set whether this connections is tracked in the database or not.
+ * @param {boolean} doTracking If true, start tracking. If false, stop
+ * tracking.
+ * @package
+ */
+ setTracking(doTracking) {
+ if ((doTracking &&
+ this.trackedState_ === RenderedConnection.TrackedState.TRACKED) ||
+ (!doTracking &&
+ this.trackedState_ === RenderedConnection.TrackedState.UNTRACKED)) {
+ return;
+ }
+ if (this.sourceBlock_.isInFlyout) {
+ // Don't bother maintaining a database of connections in a flyout.
+ return;
+ }
+ if (doTracking) {
+ this.db_.addConnection(this, this.y);
+ this.trackedState_ = RenderedConnection.TrackedState.TRACKED;
+ return;
+ }
+ if (this.trackedState_ === RenderedConnection.TrackedState.TRACKED) {
+ this.db_.removeConnection(this, this.y);
+ }
+ this.trackedState_ = RenderedConnection.TrackedState.UNTRACKED;
+ }
+
+ /**
+ * Stop tracking this connection, as well as all down-stream connections on
+ * any block attached to this connection. This happens when a block is
+ * collapsed.
+ *
+ * Also closes down-stream icons/bubbles.
+ * @package
+ */
+ stopTrackingAll() {
+ this.setTracking(false);
+ if (this.targetConnection) {
+ const blocks = this.targetBlock().getDescendants(false);
+ for (let i = 0; i < blocks.length; i++) {
+ const block = blocks[i];
+ // Stop tracking connections of all children.
+ const connections = block.getConnections_(true);
+ for (let j = 0; j < connections.length; j++) {
+ /** @type {!RenderedConnection} */ (connections[j])
+ .setTracking(false);
+ }
+ // Close all bubbles of all children.
+ const icons = block.getIcons();
+ for (let j = 0; j < icons.length; j++) {
+ icons[j].setVisible(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Start tracking this connection, as well as all down-stream connections on
+ * any block attached to this connection. This happens when a block is
+ * expanded.
+ * @return {!Array} List of blocks to render.
+ */
+ startTrackingAll() {
+ this.setTracking(true);
+ // All blocks that are not tracked must start tracking before any
+ // rendering takes place, since rendering requires knowing the dimensions
+ // of lower blocks. Also, since rendering a block renders all its parents,
+ // we only need to render the leaf nodes.
+ let renderList = [];
+ if (this.type !== ConnectionType.INPUT_VALUE &&
+ this.type !== ConnectionType.NEXT_STATEMENT) {
+ // Only spider down.
+ return renderList;
+ }
+ const block = this.targetBlock();
+ if (block) {
+ let connections;
+ if (block.isCollapsed()) {
+ // This block should only be partially revealed since it is collapsed.
+ connections = [];
+ block.outputConnection && connections.push(block.outputConnection);
+ block.nextConnection && connections.push(block.nextConnection);
+ block.previousConnection && connections.push(block.previousConnection);
+ } else {
+ // Show all connections of this block.
+ connections = block.getConnections_(true);
+ }
+ for (let i = 0; i < connections.length; i++) {
+ renderList.push.apply(renderList, connections[i].startTrackingAll());
+ }
+ if (!renderList.length) {
+ // Leaf block.
+ renderList = [block];
+ }
+ }
+ return renderList;
+ }
+
+ /**
+ * Behavior after a connection attempt fails.
+ * Bumps this connection away from the other connection. Called when an
+ * attempted connection fails.
+ * @param {!Connection} otherConnection Connection that this connection
+ * failed to connect to.
+ * @package
+ */
+ onFailedConnect(otherConnection) {
+ const block = this.getSourceBlock();
+ if (eventUtils.getRecordUndo()) {
+ const group = eventUtils.getGroup();
+ setTimeout(function() {
+ if (!block.isDisposed() && !block.getParent()) {
+ eventUtils.setGroup(group);
+ this.bumpAwayFrom(otherConnection);
+ eventUtils.setGroup(false);
+ }
+ }.bind(this), internalConstants.BUMP_DELAY);
+ }
+ }
+
+ /**
+ * Disconnect two blocks that are connected by this connection.
+ * @param {!Block} parentBlock The superior block.
+ * @param {!Block} childBlock The inferior block.
+ * @protected
+ * @override
+ */
+ disconnectInternal_(parentBlock, childBlock) {
+ super.disconnectInternal_(parentBlock, childBlock);
+ // Rerender the parent so that it may reflow.
+ if (parentBlock.rendered) {
+ parentBlock.render();
+ }
+ if (childBlock.rendered) {
+ childBlock.updateDisabled();
+ childBlock.render();
+ // Reset visibility, since the child is now a top block.
+ childBlock.getSvgRoot().style.display = 'block';
+ }
+ }
+
+ /**
+ * Respawn the shadow block if there was one connected to the this connection.
+ * Render/rerender blocks as needed.
+ * @protected
+ * @override
+ */
+ respawnShadow_() {
+ super.respawnShadow_();
+ const blockShadow = this.targetBlock();
+ if (!blockShadow) {
+ return;
+ }
+ blockShadow.initSvg();
+ blockShadow.render(false);
+
+ const parentBlock = this.getSourceBlock();
+ if (parentBlock.rendered) {
+ parentBlock.render();
+ }
+ }
+
+ /**
+ * Find all nearby compatible connections to this connection.
+ * Type checking does not apply, since this function is used for bumping.
+ * @param {number} maxLimit The maximum radius to another connection, in
+ * workspace units.
+ * @return {!Array} List of connections.
+ * @package
+ */
+ neighbours(maxLimit) {
+ return this.dbOpposite_.getNeighbours(this, maxLimit);
+ }
+
+ /**
+ * Connect two connections together. This is the connection on the superior
+ * block. Rerender blocks as needed.
+ * @param {!Connection} childConnection Connection on inferior block.
+ * @protected
+ */
+ connect_(childConnection) {
+ super.connect_(childConnection);
+
+ const parentConnection = this;
+ const parentBlock = parentConnection.getSourceBlock();
+ const childBlock = childConnection.getSourceBlock();
+ const parentRendered = parentBlock.rendered;
+ const childRendered = childBlock.rendered;
+
+ if (parentRendered) {
+ parentBlock.updateDisabled();
+ }
+ if (childRendered) {
+ childBlock.updateDisabled();
+ }
+ if (parentRendered && childRendered) {
+ if (parentConnection.type === ConnectionType.NEXT_STATEMENT ||
+ parentConnection.type === ConnectionType.PREVIOUS_STATEMENT) {
+ // Child block may need to square off its corners if it is in a stack.
+ // Rendering a child will render its parent.
+ childBlock.render();
+ } else {
+ // Child block does not change shape. Rendering the parent node will
+ // move its connected children into position.
+ parentBlock.render();
+ }
+ }
+
+ // The input the child block is connected to (if any).
+ const parentInput = parentBlock.getInputWithBlock(childBlock);
+ if (parentInput) {
+ const visible = parentInput.isVisible();
+ childBlock.getSvgRoot().style.display = visible ? 'block' : 'none';
+ }
+ }
+
+ /**
+ * Function to be called when this connection's compatible types have changed.
+ * @protected
+ */
+ onCheckChanged_() {
+ // The new value type may not be compatible with the existing connection.
+ if (this.isConnected() &&
+ (!this.targetConnection ||
+ !this.getConnectionChecker().canConnect(
+ this, this.targetConnection, false))) {
+ const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
+ child.unplug();
+ // Bump away.
+ this.sourceBlock_.bumpNeighbours();
+ }
+ }
+}
/**
* Enum for different kinds of tracked states.
@@ -101,475 +574,4 @@ RenderedConnection.TrackedState = {
TRACKED: 1,
};
-/**
- * Maximum randomness in workspace units for bumping a block.
- * @const
- */
-const BUMP_RANDOMNESS = 10;
-
-/**
- * Dispose of this connection. Remove it from the database (if it is
- * tracked) and call the super-function to deal with connected blocks.
- * @override
- * @package
- */
-RenderedConnection.prototype.dispose = function() {
- RenderedConnection.superClass_.dispose.call(this);
- if (this.trackedState_ === RenderedConnection.TrackedState.TRACKED) {
- this.db_.removeConnection(this, this.y);
- }
-};
-
-/**
- * Get the source block for this connection.
- * @return {!BlockSvg} The source block.
- * @override
- */
-RenderedConnection.prototype.getSourceBlock = function() {
- return /** @type {!BlockSvg} */ (
- RenderedConnection.superClass_.getSourceBlock.call(this));
-};
-
-/**
- * Returns the block that this connection connects to.
- * @return {?BlockSvg} The connected block or null if none is connected.
- * @override
- */
-RenderedConnection.prototype.targetBlock = function() {
- return /** @type {BlockSvg} */ (
- RenderedConnection.superClass_.targetBlock.call(this));
-};
-
-/**
- * Returns the distance between this connection and another connection in
- * workspace units.
- * @param {!Connection} otherConnection The other connection to measure
- * the distance to.
- * @return {number} The distance between connections, in workspace units.
- */
-RenderedConnection.prototype.distanceFrom = function(otherConnection) {
- const xDiff = this.x - otherConnection.x;
- const yDiff = this.y - otherConnection.y;
- return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
-};
-
-/**
- * Move the block(s) belonging to the connection to a point where they don't
- * visually interfere with the specified connection.
- * @param {!Connection} staticConnection The connection to move away
- * from.
- * @package
- */
-RenderedConnection.prototype.bumpAwayFrom = function(staticConnection) {
- if (this.sourceBlock_.workspace.isDragging()) {
- // Don't move blocks around while the user is doing the same.
- return;
- }
- // Move the root block.
- let rootBlock = this.sourceBlock_.getRootBlock();
- if (rootBlock.isInFlyout) {
- // Don't move blocks around in a flyout.
- return;
- }
- let reverse = false;
- if (!rootBlock.isMovable()) {
- // Can't bump an uneditable block away.
- // Check to see if the other block is movable.
- rootBlock = staticConnection.getSourceBlock().getRootBlock();
- if (!rootBlock.isMovable()) {
- return;
- }
- // Swap the connections and move the 'static' connection instead.
- staticConnection = this;
- reverse = true;
- }
- // Raise it to the top for extra visibility.
- const selected = common.getSelected() == rootBlock;
- selected || rootBlock.addSelect();
- let dx = (staticConnection.x + internalConstants.SNAP_RADIUS +
- Math.floor(Math.random() * BUMP_RANDOMNESS)) -
- this.x;
- let dy = (staticConnection.y + internalConstants.SNAP_RADIUS +
- Math.floor(Math.random() * BUMP_RANDOMNESS)) -
- this.y;
- if (reverse) {
- // When reversing a bump due to an uneditable block, bump up.
- dy = -dy;
- }
- if (rootBlock.RTL) {
- dx = (staticConnection.x - internalConstants.SNAP_RADIUS -
- Math.floor(Math.random() * BUMP_RANDOMNESS)) -
- this.x;
- }
- rootBlock.moveBy(dx, dy);
- selected || rootBlock.removeSelect();
-};
-
-/**
- * Change the connection's coordinates.
- * @param {number} x New absolute x coordinate, in workspace coordinates.
- * @param {number} y New absolute y coordinate, in workspace coordinates.
- */
-RenderedConnection.prototype.moveTo = function(x, y) {
- if (this.trackedState_ === RenderedConnection.TrackedState.WILL_TRACK) {
- this.db_.addConnection(this, y);
- this.trackedState_ = RenderedConnection.TrackedState.TRACKED;
- } else if (this.trackedState_ === RenderedConnection.TrackedState.TRACKED) {
- this.db_.removeConnection(this, this.y);
- this.db_.addConnection(this, y);
- }
- this.x = x;
- this.y = y;
-};
-
-/**
- * Change the connection's coordinates.
- * @param {number} dx Change to x coordinate, in workspace units.
- * @param {number} dy Change to y coordinate, in workspace units.
- */
-RenderedConnection.prototype.moveBy = function(dx, dy) {
- this.moveTo(this.x + dx, this.y + dy);
-};
-
-/**
- * Move this connection to the location given by its offset within the block and
- * the location of the block's top left corner.
- * @param {!Coordinate} blockTL The location of the top left
- * corner of the block, in workspace coordinates.
- */
-RenderedConnection.prototype.moveToOffset = function(blockTL) {
- this.moveTo(
- blockTL.x + this.offsetInBlock_.x, blockTL.y + this.offsetInBlock_.y);
-};
-
-/**
- * Set the offset of this connection relative to the top left of its block.
- * @param {number} x The new relative x, in workspace units.
- * @param {number} y The new relative y, in workspace units.
- */
-RenderedConnection.prototype.setOffsetInBlock = function(x, y) {
- this.offsetInBlock_.x = x;
- this.offsetInBlock_.y = y;
-};
-
-/**
- * Get the offset of this connection relative to the top left of its block.
- * @return {!Coordinate} The offset of the connection.
- * @package
- */
-RenderedConnection.prototype.getOffsetInBlock = function() {
- return this.offsetInBlock_;
-};
-
-/**
- * Move the blocks on either side of this connection right next to each other.
- * @package
- */
-RenderedConnection.prototype.tighten = function() {
- const dx = this.targetConnection.x - this.x;
- const dy = this.targetConnection.y - this.y;
- if (dx !== 0 || dy !== 0) {
- const block = this.targetBlock();
- const svgRoot = block.getSvgRoot();
- if (!svgRoot) {
- throw Error('block is not rendered.');
- }
- // Workspace coordinates.
- const xy = svgMath.getRelativeXY(svgRoot);
- block.getSvgRoot().setAttribute(
- 'transform', 'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')');
- block.moveConnections(-dx, -dy);
- }
-};
-
-/**
- * Find the closest compatible connection to this connection.
- * All parameters are in workspace units.
- * @param {number} maxLimit The maximum radius to another connection.
- * @param {!Coordinate} dxy Offset between this connection's location
- * in the database and the current location (as a result of dragging).
- * @return {!{connection: ?Connection, radius: number}} Contains two
- * properties: 'connection' which is either another connection or null,
- * and 'radius' which is the distance.
- */
-RenderedConnection.prototype.closest = function(maxLimit, dxy) {
- return this.dbOpposite_.searchForClosest(this, maxLimit, dxy);
-};
-
-/**
- * Add highlighting around this connection.
- */
-RenderedConnection.prototype.highlight = function() {
- let steps;
- const sourceBlockSvg = /** @type {!BlockSvg} */ (this.sourceBlock_);
- const renderConstants = sourceBlockSvg.workspace.getRenderer().getConstants();
- const shape = renderConstants.shapeFor(this);
- if (this.type === ConnectionType.INPUT_VALUE ||
- this.type === ConnectionType.OUTPUT_VALUE) {
- // Vertical line, puzzle tab, vertical line.
- const yLen = renderConstants.TAB_OFFSET_FROM_TOP;
- steps = svgPaths.moveBy(0, -yLen) + svgPaths.lineOnAxis('v', yLen) +
- shape.pathDown + svgPaths.lineOnAxis('v', yLen);
- } else {
- const xLen =
- renderConstants.NOTCH_OFFSET_LEFT - renderConstants.CORNER_RADIUS;
- // Horizontal line, notch, horizontal line.
- steps = svgPaths.moveBy(-xLen, 0) + svgPaths.lineOnAxis('h', xLen) +
- shape.pathLeft + svgPaths.lineOnAxis('h', xLen);
- }
- const xy = this.sourceBlock_.getRelativeToSurfaceXY();
- const x = this.x - xy.x;
- const y = this.y - xy.y;
- Connection.highlightedPath_ = dom.createSvgElement(
- Svg.PATH, {
- 'class': 'blocklyHighlightedConnectionPath',
- 'd': steps,
- 'transform': 'translate(' + x + ',' + y + ')' +
- (this.sourceBlock_.RTL ? ' scale(-1 1)' : ''),
- },
- this.sourceBlock_.getSvgRoot());
-};
-
-/**
- * Remove the highlighting around this connection.
- */
-RenderedConnection.prototype.unhighlight = function() {
- dom.removeNode(Connection.highlightedPath_);
- delete Connection.highlightedPath_;
-};
-
-/**
- * Set whether this connections is tracked in the database or not.
- * @param {boolean} doTracking If true, start tracking. If false, stop tracking.
- * @package
- */
-RenderedConnection.prototype.setTracking = function(doTracking) {
- if ((doTracking &&
- this.trackedState_ === RenderedConnection.TrackedState.TRACKED) ||
- (!doTracking &&
- this.trackedState_ === RenderedConnection.TrackedState.UNTRACKED)) {
- return;
- }
- if (this.sourceBlock_.isInFlyout) {
- // Don't bother maintaining a database of connections in a flyout.
- return;
- }
- if (doTracking) {
- this.db_.addConnection(this, this.y);
- this.trackedState_ = RenderedConnection.TrackedState.TRACKED;
- return;
- }
- if (this.trackedState_ === RenderedConnection.TrackedState.TRACKED) {
- this.db_.removeConnection(this, this.y);
- }
- this.trackedState_ = RenderedConnection.TrackedState.UNTRACKED;
-};
-
-/**
- * Stop tracking this connection, as well as all down-stream connections on
- * any block attached to this connection. This happens when a block is
- * collapsed.
- *
- * Also closes down-stream icons/bubbles.
- * @package
- */
-RenderedConnection.prototype.stopTrackingAll = function() {
- this.setTracking(false);
- if (this.targetConnection) {
- const blocks = this.targetBlock().getDescendants(false);
- for (let i = 0; i < blocks.length; i++) {
- const block = blocks[i];
- // Stop tracking connections of all children.
- const connections = block.getConnections_(true);
- for (let j = 0; j < connections.length; j++) {
- connections[j].setTracking(false);
- }
- // Close all bubbles of all children.
- const icons = block.getIcons();
- for (let j = 0; j < icons.length; j++) {
- icons[j].setVisible(false);
- }
- }
- }
-};
-
-/**
- * Start tracking this connection, as well as all down-stream connections on
- * any block attached to this connection. This happens when a block is expanded.
- * @return {!Array} List of blocks to render.
- */
-RenderedConnection.prototype.startTrackingAll = function() {
- this.setTracking(true);
- // All blocks that are not tracked must start tracking before any
- // rendering takes place, since rendering requires knowing the dimensions
- // of lower blocks. Also, since rendering a block renders all its parents,
- // we only need to render the leaf nodes.
- let renderList = [];
- if (this.type !== ConnectionType.INPUT_VALUE &&
- this.type !== ConnectionType.NEXT_STATEMENT) {
- // Only spider down.
- return renderList;
- }
- const block = this.targetBlock();
- if (block) {
- let connections;
- if (block.isCollapsed()) {
- // This block should only be partially revealed since it is collapsed.
- connections = [];
- block.outputConnection && connections.push(block.outputConnection);
- block.nextConnection && connections.push(block.nextConnection);
- block.previousConnection && connections.push(block.previousConnection);
- } else {
- // Show all connections of this block.
- connections = block.getConnections_(true);
- }
- for (let i = 0; i < connections.length; i++) {
- renderList.push.apply(renderList, connections[i].startTrackingAll());
- }
- if (!renderList.length) {
- // Leaf block.
- renderList = [block];
- }
- }
- return renderList;
-};
-
-/**
- * Behavior after a connection attempt fails.
- * Bumps this connection away from the other connection. Called when an
- * attempted connection fails.
- * @param {!Connection} otherConnection Connection that this connection
- * failed to connect to.
- * @package
- */
-RenderedConnection.prototype.onFailedConnect = function(otherConnection) {
- const block = this.getSourceBlock();
- if (eventUtils.getRecordUndo()) {
- const group = eventUtils.getGroup();
- setTimeout(function() {
- if (!block.isDisposed() && !block.getParent()) {
- eventUtils.setGroup(group);
- this.bumpAwayFrom(otherConnection);
- eventUtils.setGroup(false);
- }
- }.bind(this), internalConstants.BUMP_DELAY);
- }
-};
-
-
-/**
- * Disconnect two blocks that are connected by this connection.
- * @param {!Block} parentBlock The superior block.
- * @param {!Block} childBlock The inferior block.
- * @protected
- * @override
- */
-RenderedConnection.prototype.disconnectInternal_ = function(
- parentBlock, childBlock) {
- RenderedConnection.superClass_.disconnectInternal_.call(
- this, parentBlock, childBlock);
- // Rerender the parent so that it may reflow.
- if (parentBlock.rendered) {
- parentBlock.render();
- }
- if (childBlock.rendered) {
- childBlock.updateDisabled();
- childBlock.render();
- // Reset visibility, since the child is now a top block.
- childBlock.getSvgRoot().style.display = 'block';
- }
-};
-
-/**
- * Respawn the shadow block if there was one connected to the this connection.
- * Render/rerender blocks as needed.
- * @protected
- * @override
- */
-RenderedConnection.prototype.respawnShadow_ = function() {
- RenderedConnection.superClass_.respawnShadow_.call(this);
- const blockShadow = this.targetBlock();
- if (!blockShadow) {
- return;
- }
- blockShadow.initSvg();
- blockShadow.render(false);
-
- const parentBlock = this.getSourceBlock();
- if (parentBlock.rendered) {
- parentBlock.render();
- }
-};
-
-/**
- * Find all nearby compatible connections to this connection.
- * Type checking does not apply, since this function is used for bumping.
- * @param {number} maxLimit The maximum radius to another connection, in
- * workspace units.
- * @return {!Array} List of connections.
- * @package
- */
-RenderedConnection.prototype.neighbours = function(maxLimit) {
- return this.dbOpposite_.getNeighbours(this, maxLimit);
-};
-
-/**
- * Connect two connections together. This is the connection on the superior
- * block. Rerender blocks as needed.
- * @param {!Connection} childConnection Connection on inferior block.
- * @protected
- */
-RenderedConnection.prototype.connect_ = function(childConnection) {
- RenderedConnection.superClass_.connect_.call(this, childConnection);
-
- const parentConnection = this;
- const parentBlock = parentConnection.getSourceBlock();
- const childBlock = childConnection.getSourceBlock();
- const parentRendered = parentBlock.rendered;
- const childRendered = childBlock.rendered;
-
- if (parentRendered) {
- parentBlock.updateDisabled();
- }
- if (childRendered) {
- childBlock.updateDisabled();
- }
- if (parentRendered && childRendered) {
- if (parentConnection.type === ConnectionType.NEXT_STATEMENT ||
- parentConnection.type === ConnectionType.PREVIOUS_STATEMENT) {
- // Child block may need to square off its corners if it is in a stack.
- // Rendering a child will render its parent.
- childBlock.render();
- } else {
- // Child block does not change shape. Rendering the parent node will
- // move its connected children into position.
- parentBlock.render();
- }
- }
-
- // The input the child block is connected to (if any).
- const parentInput = parentBlock.getInputWithBlock(childBlock);
- if (parentInput) {
- const visible = parentInput.isVisible();
- childBlock.getSvgRoot().style.display = visible ? 'block' : 'none';
- }
-};
-
-/**
- * Function to be called when this connection's compatible types have changed.
- * @protected
- */
-RenderedConnection.prototype.onCheckChanged_ = function() {
- // The new value type may not be compatible with the existing connection.
- if (this.isConnected() &&
- (!this.targetConnection ||
- !this.getConnectionChecker().canConnect(
- this, this.targetConnection, false))) {
- const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
- child.unplug();
- // Bump away.
- this.sourceBlock_.bumpNeighbours();
- }
-};
-
exports.RenderedConnection = RenderedConnection;
diff --git a/core/renderers/common/constants.js b/core/renderers/common/constants.js
index 295ccf368..1c21b7195 100644
--- a/core/renderers/common/constants.js
+++ b/core/renderers/common/constants.js
@@ -31,1212 +31,1220 @@ const {Theme} = goog.requireType('Blockly.Theme');
/**
* An object that provides constants for rendering blocks.
- * @constructor
- * @package
- * @alias Blockly.blockRendering.ConstantProvider
*/
-const ConstantProvider = function() {
+class ConstantProvider {
/**
- * The size of an empty spacer.
- * @type {number}
- */
- this.NO_PADDING = 0;
-
- /**
- * The size of small padding.
- * @type {number}
- */
- this.SMALL_PADDING = 3;
-
- /**
- * The size of medium padding.
- * @type {number}
- */
- this.MEDIUM_PADDING = 5;
-
- /**
- * The size of medium-large padding.
- * @type {number}
- */
- this.MEDIUM_LARGE_PADDING = 8;
-
- /**
- * The size of large padding.
- * @type {number}
- */
- this.LARGE_PADDING = 10;
-
- /**
- * Offset from the top of the row for placing fields on inline input rows
- * and statement input rows.
- * Matches existing rendering (in 2019).
- * @type {number}
- */
- this.TALL_INPUT_FIELD_OFFSET_Y = this.MEDIUM_PADDING;
-
- /**
- * The height of the puzzle tab used for input and output connections.
- * @type {number}
- */
- this.TAB_HEIGHT = 15;
-
- /**
- * The offset from the top of the block at which a puzzle tab is positioned.
- * @type {number}
- */
- this.TAB_OFFSET_FROM_TOP = 5;
-
- /**
- * Vertical overlap of the puzzle tab, used to make it look more like a puzzle
- * piece.
- * @type {number}
- */
- this.TAB_VERTICAL_OVERLAP = 2.5;
-
- /**
- * The width of the puzzle tab used for input and output connections.
- * @type {number}
- */
- this.TAB_WIDTH = 8;
-
- /**
- * The width of the notch used for previous and next connections.
- * @type {number}
- */
- this.NOTCH_WIDTH = 15;
-
- /**
- * The height of the notch used for previous and next connections.
- * @type {number}
- */
- this.NOTCH_HEIGHT = 4;
-
- /**
- * The minimum width of the block.
- * @type {number}
- */
- this.MIN_BLOCK_WIDTH = 12;
-
- this.EMPTY_BLOCK_SPACER_HEIGHT = 16;
-
- /**
- * The minimum height of a dummy input row.
- * @type {number}
- */
- this.DUMMY_INPUT_MIN_HEIGHT = this.TAB_HEIGHT;
-
- /**
- * The minimum height of a dummy input row in a shadow block.
- * @type {number}
- */
- this.DUMMY_INPUT_SHADOW_MIN_HEIGHT = this.TAB_HEIGHT;
-
- /**
- * Rounded corner radius.
- * @type {number}
- */
- this.CORNER_RADIUS = 8;
-
- /**
- * Offset from the left side of a block or the inside of a statement input to
- * the left side of the notch.
- * @type {number}
- */
- this.NOTCH_OFFSET_LEFT = 15;
-
- /**
- * Additional offset added to the statement input's width to account for the
- * notch.
- * @type {number}
- */
- this.STATEMENT_INPUT_NOTCH_OFFSET = this.NOTCH_OFFSET_LEFT;
-
- this.STATEMENT_BOTTOM_SPACER = 0;
- this.STATEMENT_INPUT_PADDING_LEFT = 20;
-
- /**
- * Vertical padding between consecutive statement inputs.
- * @type {number}
- */
- this.BETWEEN_STATEMENT_PADDING_Y = 4;
-
- /**
- * The top row's minimum height.
- * @type {number}
- */
- this.TOP_ROW_MIN_HEIGHT = this.MEDIUM_PADDING;
-
- /**
- * The top row's minimum height if it precedes a statement.
- * @type {number}
- */
- this.TOP_ROW_PRECEDES_STATEMENT_MIN_HEIGHT = this.LARGE_PADDING;
-
- /**
- * The bottom row's minimum height.
- * @type {number}
- */
- this.BOTTOM_ROW_MIN_HEIGHT = this.MEDIUM_PADDING;
-
- /**
- * The bottom row's minimum height if it follows a statement input.
- * @type {number}
- */
- this.BOTTOM_ROW_AFTER_STATEMENT_MIN_HEIGHT = this.LARGE_PADDING;
-
- /**
- * Whether to add a 'hat' on top of all blocks with no previous or output
- * connections. Can be overridden by 'hat' property on Theme.BlockStyle.
- * @type {boolean}
- */
- this.ADD_START_HATS = false;
-
- /**
- * Height of the top hat.
- * @type {number}
- */
- this.START_HAT_HEIGHT = 15;
-
- /**
- * Width of the top hat.
- * @type {number}
- */
- this.START_HAT_WIDTH = 100;
-
- this.SPACER_DEFAULT_HEIGHT = 15;
-
- this.MIN_BLOCK_HEIGHT = 24;
-
- this.EMPTY_INLINE_INPUT_PADDING = 14.5;
-
- /**
- * The height of an empty inline input.
- * @type {number}
- */
- this.EMPTY_INLINE_INPUT_HEIGHT = this.TAB_HEIGHT + 11;
-
- this.EXTERNAL_VALUE_INPUT_PADDING = 2;
-
- /**
- * The height of an empty statement input. Note that in the old rendering
- * this varies slightly depending on whether the block has external or inline
- * inputs. In the new rendering this is consistent. It seems unlikely that
- * the old behaviour was intentional.
- * @type {number}
- */
- this.EMPTY_STATEMENT_INPUT_HEIGHT = this.MIN_BLOCK_HEIGHT;
-
- this.START_POINT = svgPaths.moveBy(0, 0);
-
- /**
- * Height of SVG path for jagged teeth at the end of collapsed blocks.
- * @type {number}
- */
- this.JAGGED_TEETH_HEIGHT = 12;
-
- /**
- * Width of SVG path for jagged teeth at the end of collapsed blocks.
- * @type {number}
- */
- this.JAGGED_TEETH_WIDTH = 6;
-
- /**
- * Point size of text.
- * @type {number}
- */
- this.FIELD_TEXT_FONTSIZE = 11;
-
- /**
- * Text font weight.
- * @type {string}
- */
- this.FIELD_TEXT_FONTWEIGHT = 'normal';
-
- /**
- * Text font family.
- * @type {string}
- */
- this.FIELD_TEXT_FONTFAMILY = 'sans-serif';
-
- /**
- * Height of text. This constant is dynamically set in ``setFontConstants_``
- * to be the height of the text based on the font used.
- * @type {number}
- */
- this.FIELD_TEXT_HEIGHT = -1; // Dynamically set.
-
- /**
- * Text baseline. This constant is dynamically set in ``setFontConstants_``
- * to be the baseline of the text based on the font used.
- * @type {number}
- */
- this.FIELD_TEXT_BASELINE = -1; // Dynamically set.
-
- /**
- * A field's border rect corner radius.
- * @type {number}
- */
- this.FIELD_BORDER_RECT_RADIUS = 4;
-
- /**
- * A field's border rect default height.
- * @type {number}
- */
- this.FIELD_BORDER_RECT_HEIGHT = 16;
-
- /**
- * A field's border rect X padding.
- * @type {number}
- */
- this.FIELD_BORDER_RECT_X_PADDING = 5;
-
- /**
- * A field's border rect Y padding.
- * @type {number}
- */
- this.FIELD_BORDER_RECT_Y_PADDING = 3;
-
- /**
- * The backing colour of a field's border rect.
- * @type {string}
* @package
+ * @alias Blockly.blockRendering.ConstantProvider
*/
- this.FIELD_BORDER_RECT_COLOUR = '#fff';
+ constructor() {
+ /**
+ * The size of an empty spacer.
+ * @type {number}
+ */
+ this.NO_PADDING = 0;
- /**
- * A field's text element's dominant baseline.
- * @type {boolean}
- */
- this.FIELD_TEXT_BASELINE_CENTER = !userAgent.IE && !userAgent.EDGE;
+ /**
+ * The size of small padding.
+ * @type {number}
+ */
+ this.SMALL_PADDING = 3;
- /**
- * A dropdown field's border rect height.
- * @type {number}
- */
- this.FIELD_DROPDOWN_BORDER_RECT_HEIGHT = this.FIELD_BORDER_RECT_HEIGHT;
+ /**
+ * The size of medium padding.
+ * @type {number}
+ */
+ this.MEDIUM_PADDING = 5;
- /**
- * Whether or not a dropdown field should add a border rect when in a shadow
- * block.
- * @type {boolean}
- */
- this.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW = false;
+ /**
+ * The size of medium-large padding.
+ * @type {number}
+ */
+ this.MEDIUM_LARGE_PADDING = 8;
- /**
- * Whether or not a dropdown field's div should be coloured to match the
- * block colours.
- * @type {boolean}
- */
- this.FIELD_DROPDOWN_COLOURED_DIV = false;
+ /**
+ * The size of large padding.
+ * @type {number}
+ */
+ this.LARGE_PADDING = 10;
- /**
- * Whether or not a dropdown field uses a text or SVG arrow.
- * @type {boolean}
- */
- this.FIELD_DROPDOWN_SVG_ARROW = false;
+ /**
+ * Offset from the top of the row for placing fields on inline input rows
+ * and statement input rows.
+ * Matches existing rendering (in 2019).
+ * @type {number}
+ */
+ this.TALL_INPUT_FIELD_OFFSET_Y = this.MEDIUM_PADDING;
- /**
- * A dropdown field's SVG arrow padding.
- * @type {number}
- */
- this.FIELD_DROPDOWN_SVG_ARROW_PADDING = this.FIELD_BORDER_RECT_X_PADDING;
+ /**
+ * The height of the puzzle tab used for input and output connections.
+ * @type {number}
+ */
+ this.TAB_HEIGHT = 15;
- /**
- * A dropdown field's SVG arrow size.
- * @type {number}
- */
- this.FIELD_DROPDOWN_SVG_ARROW_SIZE = 12;
+ /**
+ * The offset from the top of the block at which a puzzle tab is positioned.
+ * @type {number}
+ */
+ this.TAB_OFFSET_FROM_TOP = 5;
- /**
- * A dropdown field's SVG arrow datauri.
- * @type {string}
- */
- this.FIELD_DROPDOWN_SVG_ARROW_DATAURI =
- 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllci' +
- 'AxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMi43MSIgaG' +
- 'VpZ2h0PSI4Ljc5IiB2aWV3Qm94PSIwIDAgMTIuNzEgOC43OSI+PHRpdGxlPmRyb3Bkb3duLW' +
- 'Fycm93PC90aXRsZT48ZyBvcGFjaXR5PSIwLjEiPjxwYXRoIGQ9Ik0xMi43MSwyLjQ0QTIuND' +
- 'EsMi40MSwwLDAsMSwxMiw0LjE2TDguMDgsOC4wOGEyLjQ1LDIuNDUsMCwwLDEtMy40NSwwTD' +
- 'AuNzIsNC4xNkEyLjQyLDIuNDIsMCwwLDEsMCwyLjQ0LDIuNDgsMi40OCwwLDAsMSwuNzEuNz' +
- 'FDMSwwLjQ3LDEuNDMsMCw2LjM2LDBTMTEuNzUsMC40NiwxMiwuNzFBMi40NCwyLjQ0LDAsMC' +
- 'wxLDEyLjcxLDIuNDRaIiBmaWxsPSIjMjMxZjIwIi8+PC9nPjxwYXRoIGQ9Ik02LjM2LDcuNz' +
- 'lhMS40MywxLjQzLDAsMCwxLTEtLjQyTDEuNDIsMy40NWExLjQ0LDEuNDQsMCwwLDEsMC0yYz' +
- 'AuNTYtLjU2LDkuMzEtMC41Niw5Ljg3LDBhMS40NCwxLjQ0LDAsMCwxLDAsMkw3LjM3LDcuMz' +
- 'dBMS40MywxLjQzLDAsMCwxLDYuMzYsNy43OVoiIGZpbGw9IiNmZmYiLz48L3N2Zz4=';
+ /**
+ * Vertical overlap of the puzzle tab, used to make it look more like a
+ * puzzle piece.
+ * @type {number}
+ */
+ this.TAB_VERTICAL_OVERLAP = 2.5;
- /**
- * Whether or not to show a box shadow around the widget div. This is only a
- * feature of full block fields.
- * @type {boolean}
- */
- this.FIELD_TEXTINPUT_BOX_SHADOW = false;
+ /**
+ * The width of the puzzle tab used for input and output connections.
+ * @type {number}
+ */
+ this.TAB_WIDTH = 8;
- /**
- * Whether or not the colour field should display its colour value on the
- * entire block.
- * @type {boolean}
- */
- this.FIELD_COLOUR_FULL_BLOCK = false;
+ /**
+ * The width of the notch used for previous and next connections.
+ * @type {number}
+ */
+ this.NOTCH_WIDTH = 15;
- /**
- * A colour field's default width.
- * @type {number}
- */
- this.FIELD_COLOUR_DEFAULT_WIDTH = 26;
+ /**
+ * The height of the notch used for previous and next connections.
+ * @type {number}
+ */
+ this.NOTCH_HEIGHT = 4;
- /**
- * A colour field's default height.
- * @type {number}
- */
- this.FIELD_COLOUR_DEFAULT_HEIGHT = this.FIELD_BORDER_RECT_HEIGHT;
+ /**
+ * The minimum width of the block.
+ * @type {number}
+ */
+ this.MIN_BLOCK_WIDTH = 12;
- /**
- * A checkbox field's X offset.
- * @type {number}
- */
- this.FIELD_CHECKBOX_X_OFFSET = this.FIELD_BORDER_RECT_X_PADDING - 3;
+ this.EMPTY_BLOCK_SPACER_HEIGHT = 16;
- /**
- * A random identifier used to ensure a unique ID is used for each
- * filter/pattern for the case of multiple Blockly instances on a page.
- * @type {string}
- * @package
- */
- this.randomIdentifier = String(Math.random()).substring(2);
+ /**
+ * The minimum height of a dummy input row.
+ * @type {number}
+ */
+ this.DUMMY_INPUT_MIN_HEIGHT = this.TAB_HEIGHT;
- /**
- * The defs tag that contains all filters and patterns for this Blockly
- * instance.
- * @type {?SVGElement}
- * @private
- */
- this.defs_ = null;
+ /**
+ * The minimum height of a dummy input row in a shadow block.
+ * @type {number}
+ */
+ this.DUMMY_INPUT_SHADOW_MIN_HEIGHT = this.TAB_HEIGHT;
- /**
- * The ID of the emboss filter, or the empty string if no filter is set.
- * @type {string}
- * @package
- */
- this.embossFilterId = '';
+ /**
+ * Rounded corner radius.
+ * @type {number}
+ */
+ this.CORNER_RADIUS = 8;
- /**
- * The element to use for highlighting, or null if not set.
- * @type {SVGElement}
- * @private
- */
- this.embossFilter_ = null;
+ /**
+ * Offset from the left side of a block or the inside of a statement input
+ * to the left side of the notch.
+ * @type {number}
+ */
+ this.NOTCH_OFFSET_LEFT = 15;
- /**
- * The ID of the disabled pattern, or the empty string if no pattern is set.
- * @type {string}
- * @package
- */
- this.disabledPatternId = '';
+ /**
+ * Additional offset added to the statement input's width to account for the
+ * notch.
+ * @type {number}
+ */
+ this.STATEMENT_INPUT_NOTCH_OFFSET = this.NOTCH_OFFSET_LEFT;
- /**
- * The element to use for disabled blocks, or null if not set.
- * @type {SVGElement}
- * @private
- */
- this.disabledPattern_ = null;
+ this.STATEMENT_BOTTOM_SPACER = 0;
+ this.STATEMENT_INPUT_PADDING_LEFT = 20;
- /**
- * The ID of the debug filter, or the empty string if no pattern is set.
- * @type {string}
- * @package
- */
- this.debugFilterId = '';
+ /**
+ * Vertical padding between consecutive statement inputs.
+ * @type {number}
+ */
+ this.BETWEEN_STATEMENT_PADDING_Y = 4;
- /**
- * The element to use for a debug highlight, or null if not set.
- * @type {SVGElement}
- * @private
- */
- this.debugFilter_ = null;
+ /**
+ * The top row's minimum height.
+ * @type {number}
+ */
+ this.TOP_ROW_MIN_HEIGHT = this.MEDIUM_PADDING;
- /**
- * The