refactor: convert some files to es classes (#5913)

* refactor: update workspace_comment and _svg to es classes

* refactor: update classes that extend icon to es classes

* refactor: update icon to es6 class

* refactor: update connection classes to es6 classes and add casts as needed

* refactor: update scrollbar to es6 class and add casts as needed

* refactor: update workspace_svg to es6 class

* refactor: update several files to es6 classes

* refactor: update several files to es6 classes

* refactor: update renderers/common/info.js to es6 class

* refactor: update several files to es6 classes

* chore: rebuild deps.js

* chore: run format
This commit is contained in:
Rachel Fenichel
2022-02-04 10:58:22 -08:00
committed by GitHub
parent b26a3f8949
commit 4cf1a5c886
32 changed files with 11523 additions and 11206 deletions

View File

@@ -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()) {

View File

@@ -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:
<foreignObject x="8" y="8" width="164" height="164">
<body xmlns="http://www.w3.org/1999/xhtml" class="blocklyMinimalBody">
<textarea xmlns="http://www.w3.org/1999/xhtml"
class="blocklyCommentTextarea"
style="height: 164px; width: 164px;"></textarea>
</body>
</foreignObject>
* 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:
<foreignObject x="8" y="8" width="164" height="164">
<body xmlns="http://www.w3.org/1999/xhtml" class="blocklyMinimalBody">
<textarea xmlns="http://www.w3.org/1999/xhtml"
class="blocklyCommentTextarea"
style="height: 164px; width: 164px;"></textarea>
</body>
</foreignObject>
* 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.

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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<!RenderedConnection>}
* @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<!RenderedConnection>}
* @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<!RenderedConnection>} 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<!RenderedConnection>} 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<!ConnectionDB>} 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<!ConnectionDB>} 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;

View File

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

View File

@@ -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 '&#10'. 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, '&#10;');
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(/&#10;/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 '&#10'. 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, '&#10;');
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(/&#10;/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.

View File

@@ -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:
<g class="blocklyIconGroup">
...
</g>
*/
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:
<g class="blocklyIconGroup">
...
</g>
*/
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;

View File

@@ -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<string, !Marker>}
* @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<string, !Marker>}
* @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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,8 @@ const {Measurable} = goog.requireType('Blockly.blockRendering.Measurable');
/* eslint-disable-next-line no-unused-vars */
const {RenderInfo} = goog.requireType('Blockly.blockRendering.RenderInfo');
/* eslint-disable-next-line no-unused-vars */
const {RenderInfo: ZelosInfo} = goog.requireType('Blockly.zelos.RenderInfo');
/* eslint-disable-next-line no-unused-vars */
const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
/* eslint-disable-next-line no-unused-vars */
const {Row} = goog.requireType('Blockly.blockRendering.Row');
@@ -38,35 +40,398 @@ const {Types} = goog.require('Blockly.blockRendering.Types');
/**
* An object that renders rectangles and dots for debugging rendering code.
* @param {!ConstantProvider} constants The renderer's
* constants.
* @package
* @constructor
* @alias Blockly.blockRendering.Debug
*/
const Debug = function(constants) {
class Debug {
/**
* An array of SVG elements that have been created by this object.
* @type {Array<!SVGElement>}
* @private
* @param {!ConstantProvider} constants The renderer's
* constants.
* @package
* @alias Blockly.blockRendering.Debug
*/
this.debugElements_ = [];
constructor(constants) {
/**
* An array of SVG elements that have been created by this object.
* @type {Array<!SVGElement>}
* @private
*/
this.debugElements_ = [];
/**
* The SVG root of the block that is being rendered. Debug elements will
* be attached to this root.
* @type {SVGElement}
* @private
*/
this.svgRoot_ = null;
/**
* The renderer's constant provider.
* @type {!ConstantProvider}
* @private
*/
this.constants_ = constants;
/**
* @type {string}
* @private
*/
this.randomColour_ = '';
}
/**
* The SVG root of the block that is being rendered. Debug elements will
* be attached to this root.
* @type {SVGElement}
* @private
* Remove all elements the this object created on the last pass.
* @package
*/
this.svgRoot_ = null;
clearElems() {
for (let i = 0; i < this.debugElements_.length; i++) {
const elem = this.debugElements_[i];
dom.removeNode(elem);
}
this.debugElements_ = [];
}
/**
* The renderer's constant provider.
* @type {!ConstantProvider}
* @private
* Draw a debug rectangle for a spacer (empty) row.
* @param {!Row} row The row to render.
* @param {number} cursorY The y position of the top of the row.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
this.constants_ = constants;
};
drawSpacerRow(row, cursorY, isRtl) {
if (!Debug.config.rowSpacers) {
return;
}
const height = Math.abs(row.height);
const isNegativeSpacing = row.height < 0;
if (isNegativeSpacing) {
cursorY -= height;
}
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'rowSpacerRect blockRenderDebug',
'x': isRtl ? -(row.xPos + row.width) : row.xPos,
'y': cursorY,
'width': row.width,
'height': height,
'stroke': isNegativeSpacing ? 'black' : 'blue',
'fill': 'blue',
'fill-opacity': '0.5',
'stroke-width': '1px',
},
this.svgRoot_));
}
/**
* Draw a debug rectangle for a horizontal spacer.
* @param {!InRowSpacer} elem The spacer to render.
* @param {number} rowHeight The height of the container row.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
drawSpacerElem(elem, rowHeight, isRtl) {
if (!Debug.config.elemSpacers) {
return;
}
const width = Math.abs(elem.width);
const isNegativeSpacing = elem.width < 0;
let xPos = isNegativeSpacing ? elem.xPos - width : elem.xPos;
if (isRtl) {
xPos = -(xPos + width);
}
const yPos = elem.centerline - elem.height / 2;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'elemSpacerRect blockRenderDebug',
'x': xPos,
'y': yPos,
'width': width,
'height': elem.height,
'stroke': 'pink',
'fill': isNegativeSpacing ? 'black' : 'pink',
'fill-opacity': '0.5',
'stroke-width': '1px',
},
this.svgRoot_));
}
/**
* Draw a debug rectangle for an in-row element.
* @param {!Measurable} elem The element to render.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
drawRenderedElem(elem, isRtl) {
if (Debug.config.elems) {
let xPos = elem.xPos;
if (isRtl) {
xPos = -(xPos + elem.width);
}
const yPos = elem.centerline - elem.height / 2;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'rowRenderingRect blockRenderDebug',
'x': xPos,
'y': yPos,
'width': elem.width,
'height': elem.height,
'stroke': 'black',
'fill': 'none',
'stroke-width': '1px',
},
this.svgRoot_));
if (Types.isField(elem) && elem.field instanceof FieldLabel) {
const baseline = this.constants_.FIELD_TEXT_BASELINE;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'rowRenderingRect blockRenderDebug',
'x': xPos,
'y': yPos + baseline,
'width': elem.width,
'height': '0.1px',
'stroke': 'red',
'fill': 'none',
'stroke-width': '0.5px',
},
this.svgRoot_));
}
}
if (Types.isInput(elem) && Debug.config.connections) {
this.drawConnection(elem.connectionModel);
}
}
/**
* Draw a circle at the location of the given connection. Inputs and outputs
* share the same colours, as do previous and next. When positioned correctly
* a connected pair will look like a bullseye.
* @param {RenderedConnection} conn The connection to circle.
* @suppress {visibility} Suppress visibility of conn.offsetInBlock_ since
* this is a debug module.
* @package
*/
drawConnection(conn) {
if (!Debug.config.connections) {
return;
}
let colour;
let size;
let fill;
if (conn.type === ConnectionType.INPUT_VALUE) {
size = 4;
colour = 'magenta';
fill = 'none';
} else if (conn.type === ConnectionType.OUTPUT_VALUE) {
size = 2;
colour = 'magenta';
fill = colour;
} else if (conn.type === ConnectionType.NEXT_STATEMENT) {
size = 4;
colour = 'goldenrod';
fill = 'none';
} else if (conn.type === ConnectionType.PREVIOUS_STATEMENT) {
size = 2;
colour = 'goldenrod';
fill = colour;
}
this.debugElements_.push(dom.createSvgElement(
Svg.CIRCLE, {
'class': 'blockRenderDebug',
'cx': conn.offsetInBlock_.x,
'cy': conn.offsetInBlock_.y,
'r': size,
'fill': fill,
'stroke': colour,
},
this.svgRoot_));
}
/**
* Draw a debug rectangle for a non-empty row.
* @param {!Row} row The non-empty row to render.
* @param {number} cursorY The y position of the top of the row.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
drawRenderedRow(row, cursorY, isRtl) {
if (!Debug.config.rows) {
return;
}
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'elemRenderingRect blockRenderDebug',
'x': isRtl ? -(row.xPos + row.width) : row.xPos,
'y': row.yPos,
'width': row.width,
'height': row.height,
'stroke': 'red',
'fill': 'none',
'stroke-width': '1px',
},
this.svgRoot_));
if (Types.isTopOrBottomRow(row)) {
return;
}
if (Debug.config.connectedBlockBounds) {
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'connectedBlockWidth blockRenderDebug',
'x': isRtl ? -(row.xPos + row.widthWithConnectedBlocks) : row.xPos,
'y': row.yPos,
'width': row.widthWithConnectedBlocks,
'height': row.height,
'stroke': this.randomColour_,
'fill': 'none',
'stroke-width': '1px',
'stroke-dasharray': '3,3',
},
this.svgRoot_));
}
}
/**
* Draw debug rectangles for a non-empty row and all of its subcomponents.
* @param {!Row} row The non-empty row to render.
* @param {number} cursorY The y position of the top of the row.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
drawRowWithElements(row, cursorY, isRtl) {
for (let i = 0; i < row.elements.length; i++) {
const elem = row.elements[i];
if (!elem) {
console.warn('A row has an undefined or null element.', row, elem);
continue;
}
if (Types.isSpacer(elem)) {
this.drawSpacerElem(
/** @type {!InRowSpacer} */ (elem), row.height, isRtl);
} else {
this.drawRenderedElem(elem, isRtl);
}
}
this.drawRenderedRow(row, cursorY, isRtl);
}
/**
* Draw a debug rectangle around the entire block.
* @param {!RenderInfo} info Rendering information about
* the block to debug.
* @package
*/
drawBoundingBox(info) {
if (!Debug.config.blockBounds) {
return;
}
// Bounding box without children.
let xPos = info.RTL ? -info.width : 0;
const yPos = 0;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'blockBoundingBox blockRenderDebug',
'x': xPos,
'y': yPos,
'width': info.width,
'height': info.height,
'stroke': 'black',
'fill': 'none',
'stroke-width': '1px',
'stroke-dasharray': '5,5',
},
this.svgRoot_));
if (Debug.config.connectedBlockBounds) {
// Bounding box with children.
xPos = info.RTL ? -info.widthWithChildren : 0;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'blockRenderDebug',
'x': xPos,
'y': yPos,
'width': info.widthWithChildren,
'height': info.height,
'stroke': '#DF57BC',
'fill': 'none',
'stroke-width': '1px',
'stroke-dasharray': '3,3',
},
this.svgRoot_));
}
}
/**
* Do all of the work to draw debug information for the whole block.
* @param {!BlockSvg} block The block to draw debug information for.
* @param {!RenderInfo} info Rendering information about
* the block to debug.
* @package
*/
drawDebug(block, info) {
this.clearElems();
this.svgRoot_ = block.getSvgRoot();
this.randomColour_ =
'#' + Math.floor(Math.random() * 16777215).toString(16);
let cursorY = 0;
for (let i = 0; i < info.rows.length; i++) {
const row = info.rows[i];
if (Types.isBetweenRowSpacer(row)) {
this.drawSpacerRow(row, cursorY, info.RTL);
} else {
this.drawRowWithElements(row, cursorY, info.RTL);
}
cursorY += row.height;
}
if (block.previousConnection) {
this.drawConnection(block.previousConnection);
}
if (block.nextConnection) {
this.drawConnection(block.nextConnection);
}
if (block.outputConnection) {
this.drawConnection(block.outputConnection);
}
/**
* TODO: Find a better way to do this check without pulling in all of
* zelos, or just delete this line or the whole debug renderer.
*/
const maybeZelosInfo = /** @type {!ZelosInfo} */ (info);
if (maybeZelosInfo.rightSide) {
this.drawRenderedElem(maybeZelosInfo.rightSide, info.RTL);
}
this.drawBoundingBox(info);
this.drawRender(block.pathObject.svgPath);
}
/**
* Show a debug filter to highlight that a block has been rendered.
* @param {!SVGElement} svgPath The block's SVG path.
* @package
*/
drawRender(svgPath) {
if (!Debug.config.render) {
return;
}
svgPath.setAttribute(
'filter', 'url(#' + this.constants_.debugFilterId + ')');
setTimeout(function() {
svgPath.setAttribute('filter', '');
}, 100);
}
}
/**
* Configuration object containing booleans to enable and disable debug
@@ -84,352 +449,4 @@ Debug.config = {
render: true,
};
/**
* Remove all elements the this object created on the last pass.
* @package
*/
Debug.prototype.clearElems = function() {
for (let i = 0; i < this.debugElements_.length; i++) {
const elem = this.debugElements_[i];
dom.removeNode(elem);
}
this.debugElements_ = [];
};
/**
* Draw a debug rectangle for a spacer (empty) row.
* @param {!Row} row The row to render.
* @param {number} cursorY The y position of the top of the row.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
Debug.prototype.drawSpacerRow = function(row, cursorY, isRtl) {
if (!Debug.config.rowSpacers) {
return;
}
const height = Math.abs(row.height);
const isNegativeSpacing = row.height < 0;
if (isNegativeSpacing) {
cursorY -= height;
}
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'rowSpacerRect blockRenderDebug',
'x': isRtl ? -(row.xPos + row.width) : row.xPos,
'y': cursorY,
'width': row.width,
'height': height,
'stroke': isNegativeSpacing ? 'black' : 'blue',
'fill': 'blue',
'fill-opacity': '0.5',
'stroke-width': '1px',
},
this.svgRoot_));
};
/**
* Draw a debug rectangle for a horizontal spacer.
* @param {!InRowSpacer} elem The spacer to render.
* @param {number} rowHeight The height of the container row.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
Debug.prototype.drawSpacerElem = function(elem, rowHeight, isRtl) {
if (!Debug.config.elemSpacers) {
return;
}
const width = Math.abs(elem.width);
const isNegativeSpacing = elem.width < 0;
let xPos = isNegativeSpacing ? elem.xPos - width : elem.xPos;
if (isRtl) {
xPos = -(xPos + width);
}
const yPos = elem.centerline - elem.height / 2;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'elemSpacerRect blockRenderDebug',
'x': xPos,
'y': yPos,
'width': width,
'height': elem.height,
'stroke': 'pink',
'fill': isNegativeSpacing ? 'black' : 'pink',
'fill-opacity': '0.5',
'stroke-width': '1px',
},
this.svgRoot_));
};
/**
* Draw a debug rectangle for an in-row element.
* @param {!Measurable} elem The element to render.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
Debug.prototype.drawRenderedElem = function(elem, isRtl) {
if (Debug.config.elems) {
let xPos = elem.xPos;
if (isRtl) {
xPos = -(xPos + elem.width);
}
const yPos = elem.centerline - elem.height / 2;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'rowRenderingRect blockRenderDebug',
'x': xPos,
'y': yPos,
'width': elem.width,
'height': elem.height,
'stroke': 'black',
'fill': 'none',
'stroke-width': '1px',
},
this.svgRoot_));
if (Types.isField(elem) && elem.field instanceof FieldLabel) {
const baseline = this.constants_.FIELD_TEXT_BASELINE;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'rowRenderingRect blockRenderDebug',
'x': xPos,
'y': yPos + baseline,
'width': elem.width,
'height': '0.1px',
'stroke': 'red',
'fill': 'none',
'stroke-width': '0.5px',
},
this.svgRoot_));
}
}
if (Types.isInput(elem) && Debug.config.connections) {
this.drawConnection(elem.connectionModel);
}
};
/**
* Draw a circle at the location of the given connection. Inputs and outputs
* share the same colours, as do previous and next. When positioned correctly
* a connected pair will look like a bullseye.
* @param {RenderedConnection} conn The connection to circle.
* @suppress {visibility} Suppress visibility of conn.offsetInBlock_ since this
* is a debug module.
* @package
*/
Debug.prototype.drawConnection = function(conn) {
if (!Debug.config.connections) {
return;
}
let colour;
let size;
let fill;
if (conn.type === ConnectionType.INPUT_VALUE) {
size = 4;
colour = 'magenta';
fill = 'none';
} else if (conn.type === ConnectionType.OUTPUT_VALUE) {
size = 2;
colour = 'magenta';
fill = colour;
} else if (conn.type === ConnectionType.NEXT_STATEMENT) {
size = 4;
colour = 'goldenrod';
fill = 'none';
} else if (conn.type === ConnectionType.PREVIOUS_STATEMENT) {
size = 2;
colour = 'goldenrod';
fill = colour;
}
this.debugElements_.push(dom.createSvgElement(
Svg.CIRCLE, {
'class': 'blockRenderDebug',
'cx': conn.offsetInBlock_.x,
'cy': conn.offsetInBlock_.y,
'r': size,
'fill': fill,
'stroke': colour,
},
this.svgRoot_));
};
/**
* Draw a debug rectangle for a non-empty row.
* @param {!Row} row The non-empty row to render.
* @param {number} cursorY The y position of the top of the row.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
Debug.prototype.drawRenderedRow = function(row, cursorY, isRtl) {
if (!Debug.config.rows) {
return;
}
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'elemRenderingRect blockRenderDebug',
'x': isRtl ? -(row.xPos + row.width) : row.xPos,
'y': row.yPos,
'width': row.width,
'height': row.height,
'stroke': 'red',
'fill': 'none',
'stroke-width': '1px',
},
this.svgRoot_));
if (Types.isTopOrBottomRow(row)) {
return;
}
if (Debug.config.connectedBlockBounds) {
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'connectedBlockWidth blockRenderDebug',
'x': isRtl ? -(row.xPos + row.widthWithConnectedBlocks) : row.xPos,
'y': row.yPos,
'width': row.widthWithConnectedBlocks,
'height': row.height,
'stroke': this.randomColour_,
'fill': 'none',
'stroke-width': '1px',
'stroke-dasharray': '3,3',
},
this.svgRoot_));
}
};
/**
* Draw debug rectangles for a non-empty row and all of its subcomponents.
* @param {!Row} row The non-empty row to render.
* @param {number} cursorY The y position of the top of the row.
* @param {boolean} isRtl Whether the block is rendered RTL.
* @package
*/
Debug.prototype.drawRowWithElements = function(row, cursorY, isRtl) {
for (let i = 0; i < row.elements.length; i++) {
const elem = row.elements[i];
if (!elem) {
console.warn('A row has an undefined or null element.', row, elem);
continue;
}
if (Types.isSpacer(elem)) {
this.drawSpacerElem(
/** @type {!InRowSpacer} */ (elem), row.height, isRtl);
} else {
this.drawRenderedElem(elem, isRtl);
}
}
this.drawRenderedRow(row, cursorY, isRtl);
};
/**
* Draw a debug rectangle around the entire block.
* @param {!RenderInfo} info Rendering information about
* the block to debug.
* @package
*/
Debug.prototype.drawBoundingBox = function(info) {
if (!Debug.config.blockBounds) {
return;
}
// Bounding box without children.
let xPos = info.RTL ? -info.width : 0;
const yPos = 0;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'blockBoundingBox blockRenderDebug',
'x': xPos,
'y': yPos,
'width': info.width,
'height': info.height,
'stroke': 'black',
'fill': 'none',
'stroke-width': '1px',
'stroke-dasharray': '5,5',
},
this.svgRoot_));
if (Debug.config.connectedBlockBounds) {
// Bounding box with children.
xPos = info.RTL ? -info.widthWithChildren : 0;
this.debugElements_.push(dom.createSvgElement(
Svg.RECT, {
'class': 'blockRenderDebug',
'x': xPos,
'y': yPos,
'width': info.widthWithChildren,
'height': info.height,
'stroke': '#DF57BC',
'fill': 'none',
'stroke-width': '1px',
'stroke-dasharray': '3,3',
},
this.svgRoot_));
}
};
/**
* Do all of the work to draw debug information for the whole block.
* @param {!BlockSvg} block The block to draw debug information for.
* @param {!RenderInfo} info Rendering information about
* the block to debug.
* @package
*/
Debug.prototype.drawDebug = function(block, info) {
this.clearElems();
this.svgRoot_ = block.getSvgRoot();
this.randomColour_ = '#' + Math.floor(Math.random() * 16777215).toString(16);
let cursorY = 0;
for (let i = 0; i < info.rows.length; i++) {
const row = info.rows[i];
if (Types.isBetweenRowSpacer(row)) {
this.drawSpacerRow(row, cursorY, info.RTL);
} else {
this.drawRowWithElements(row, cursorY, info.RTL);
}
cursorY += row.height;
}
if (block.previousConnection) {
this.drawConnection(block.previousConnection);
}
if (block.nextConnection) {
this.drawConnection(block.nextConnection);
}
if (block.outputConnection) {
this.drawConnection(block.outputConnection);
}
if (info.rightSide) {
this.drawRenderedElem(info.rightSide, info.RTL);
}
this.drawBoundingBox(info);
this.drawRender(block.pathObject.svgPath);
};
/**
* Show a debug filter to highlight that a block has been rendered.
* @param {!SVGElement} svgPath The block's SVG path.
* @package
*/
Debug.prototype.drawRender = function(svgPath) {
if (!Debug.config.render) {
return;
}
svgPath.setAttribute('filter', 'url(#' + this.constants_.debugFilterId + ')');
setTimeout(function() {
svgPath.setAttribute('filter', '');
}, 100);
};
exports.Debug = Debug;

View File

@@ -38,431 +38,436 @@ const {Types} = goog.require('Blockly.blockRendering.Types');
/**
* An object that draws a block based on the given rendering information.
* @param {!BlockSvg} block The block to render.
* @param {!RenderInfo} info An object containing all
* information needed to render this block.
* @package
* @constructor
* @alias Blockly.blockRendering.Drawer
*/
const Drawer = function(block, info) {
this.block_ = block;
this.info_ = info;
this.topLeft_ = block.getRelativeToSurfaceXY();
this.outlinePath_ = '';
this.inlinePath_ = '';
class Drawer {
/**
* @param {!BlockSvg} block The block to render.
* @param {!RenderInfo} info An object containing all
* information needed to render this block.
* @package
* @alias Blockly.blockRendering.Drawer
*/
constructor(block, info) {
this.block_ = block;
this.info_ = info;
this.topLeft_ = block.getRelativeToSurfaceXY();
this.outlinePath_ = '';
this.inlinePath_ = '';
/**
* The renderer's constant provider.
* @type {!ConstantProvider}
* @protected
*/
this.constants_ = info.getRenderer().getConstants();
}
/**
* The renderer's constant provider.
* @type {!ConstantProvider}
* Draw the block to the workspace. Here "drawing" means setting SVG path
* elements and moving fields, icons, and connections on the screen.
*
* The pieces of the paths are pushed into arrays of "steps", which are then
* joined with spaces and set directly on the block. This guarantees that
* the steps are separated by spaces for improved readability, but isn't
* required.
* @package
*/
draw() {
this.hideHiddenIcons_();
this.drawOutline_();
this.drawInternals_();
this.block_.pathObject.setPath(this.outlinePath_ + '\n' + this.inlinePath_);
if (this.info_.RTL) {
this.block_.pathObject.flipRTL();
}
if (debug.isDebuggerEnabled()) {
this.block_.renderingDebugger.drawDebug(this.block_, this.info_);
}
this.recordSizeOnBlock_();
}
/**
* Save sizing information back to the block
* Most of the rendering information can be thrown away at the end of the
* render. Anything that needs to be kept around should be set in this
* function.
* @protected
*/
this.constants_ = info.getRenderer().getConstants();
};
/**
* Draw the block to the workspace. Here "drawing" means setting SVG path
* elements and moving fields, icons, and connections on the screen.
*
* The pieces of the paths are pushed into arrays of "steps", which are then
* joined with spaces and set directly on the block. This guarantees that
* the steps are separated by spaces for improved readability, but isn't
* required.
* @package
*/
Drawer.prototype.draw = function() {
this.hideHiddenIcons_();
this.drawOutline_();
this.drawInternals_();
this.block_.pathObject.setPath(this.outlinePath_ + '\n' + this.inlinePath_);
if (this.info_.RTL) {
this.block_.pathObject.flipRTL();
recordSizeOnBlock_() {
// This is used when the block is reporting its size to anyone else.
// The dark path adds to the size of the block in both X and Y.
this.block_.height = this.info_.height;
this.block_.width = this.info_.widthWithChildren;
}
if (debug.isDebuggerEnabled()) {
this.block_.renderingDebugger.drawDebug(this.block_, this.info_);
}
this.recordSizeOnBlock_();
};
/**
* Save sizing information back to the block
* Most of the rendering information can be thrown away at the end of the
* render. Anything that needs to be kept around should be set in this function.
* @protected
*/
Drawer.prototype.recordSizeOnBlock_ = function() {
// This is used when the block is reporting its size to anyone else.
// The dark path adds to the size of the block in both X and Y.
this.block_.height = this.info_.height;
this.block_.width = this.info_.widthWithChildren;
};
/**
* Hide icons that were marked as hidden.
* @protected
*/
Drawer.prototype.hideHiddenIcons_ = function() {
for (let i = 0, iconInfo; (iconInfo = this.info_.hiddenIcons[i]); i++) {
iconInfo.icon.iconGroup_.setAttribute('display', 'none');
}
};
/**
* Create the outline of the block. This is a single continuous path.
* @protected
*/
Drawer.prototype.drawOutline_ = function() {
this.drawTop_();
for (let r = 1; r < this.info_.rows.length - 1; r++) {
const row = this.info_.rows[r];
if (row.hasJaggedEdge) {
this.drawJaggedEdge_(row);
} else if (row.hasStatement) {
this.drawStatementInput_(row);
} else if (row.hasExternalInput) {
this.drawValueInput_(row);
} else {
this.drawRightSideRow_(row);
}
}
this.drawBottom_();
this.drawLeft_();
};
/**
* Add steps for the top corner of the block, taking into account
* details such as hats and rounded corners.
* @protected
*/
Drawer.prototype.drawTop_ = function() {
const topRow = this.info_.topRow;
const elements = topRow.elements;
this.positionPreviousConnection_();
this.outlinePath_ += svgPaths.moveBy(topRow.xPos, this.info_.startY);
for (let i = 0, elem; (elem = elements[i]); i++) {
if (Types.isLeftRoundedCorner(elem)) {
this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topLeft;
} else if (Types.isRightRoundedCorner(elem)) {
this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topRight;
} else if (Types.isPreviousConnection(elem)) {
this.outlinePath_ += elem.shape.pathLeft;
} else if (Types.isHat(elem)) {
this.outlinePath_ += this.constants_.START_HAT.path;
} else if (Types.isSpacer(elem)) {
this.outlinePath_ += svgPaths.lineOnAxis('h', elem.width);
}
// No branch for a square corner, because it's a no-op.
}
this.outlinePath_ += svgPaths.lineOnAxis('v', topRow.height);
};
/**
* Add steps for the jagged edge of a row on a collapsed block.
* @param {!Row} row The row to draw the side of.
* @protected
*/
Drawer.prototype.drawJaggedEdge_ = function(row) {
const remainder = row.height - this.constants_.JAGGED_TEETH.height;
this.outlinePath_ +=
this.constants_.JAGGED_TEETH.path + svgPaths.lineOnAxis('v', remainder);
};
/**
* Add steps for an external value input, rendered as a notch in the side
* of the block.
* @param {!Row} row The row that this input belongs to.
* @protected
*/
Drawer.prototype.drawValueInput_ = function(row) {
const input =
/** @type {ExternalValueInput|InlineInput} */ (row.getLastInput());
this.positionExternalValueConnection_(row);
const pathDown = (typeof input.shape.pathDown === 'function') ?
input.shape.pathDown(input.height) :
input.shape.pathDown;
this.outlinePath_ += svgPaths.lineOnAxis('H', input.xPos + input.width) +
pathDown + svgPaths.lineOnAxis('v', row.height - input.connectionHeight);
};
/**
* Add steps for a statement input.
* @param {!Row} row The row that this input belongs to.
* @protected
*/
Drawer.prototype.drawStatementInput_ = function(row) {
const input = row.getLastInput();
// Where to start drawing the notch, which is on the right side in LTR.
const x = input.xPos + input.notchOffset + input.shape.width;
const innerTopLeftCorner = input.shape.pathRight +
svgPaths.lineOnAxis(
'h', -(input.notchOffset - this.constants_.INSIDE_CORNERS.width)) +
this.constants_.INSIDE_CORNERS.pathTop;
const innerHeight = row.height - (2 * this.constants_.INSIDE_CORNERS.height);
this.outlinePath_ += svgPaths.lineOnAxis('H', x) + innerTopLeftCorner +
svgPaths.lineOnAxis('v', innerHeight) +
this.constants_.INSIDE_CORNERS.pathBottom +
svgPaths.lineOnAxis('H', row.xPos + row.width);
this.positionStatementInputConnection_(row);
};
/**
* Add steps for the right side of a row that does not have value or
* statement input connections.
* @param {!Row} row The row to draw the side of.
* @protected
*/
Drawer.prototype.drawRightSideRow_ = function(row) {
this.outlinePath_ += svgPaths.lineOnAxis('V', row.yPos + row.height);
};
/**
* Add steps for the bottom edge of a block, possibly including a notch
* for the next connection.
* @protected
*/
Drawer.prototype.drawBottom_ = function() {
const bottomRow = this.info_.bottomRow;
const elems = bottomRow.elements;
this.positionNextConnection_();
let rightCornerYOffset = 0;
let outlinePath = '';
for (let i = elems.length - 1, elem; (elem = elems[i]); i--) {
if (Types.isNextConnection(elem)) {
outlinePath += elem.shape.pathRight;
} else if (Types.isLeftSquareCorner(elem)) {
outlinePath += svgPaths.lineOnAxis('H', bottomRow.xPos);
} else if (Types.isLeftRoundedCorner(elem)) {
outlinePath += this.constants_.OUTSIDE_CORNERS.bottomLeft;
} else if (Types.isRightRoundedCorner(elem)) {
outlinePath += this.constants_.OUTSIDE_CORNERS.bottomRight;
rightCornerYOffset = this.constants_.OUTSIDE_CORNERS.rightHeight;
} else if (Types.isSpacer(elem)) {
outlinePath += svgPaths.lineOnAxis('h', elem.width * -1);
/**
* Hide icons that were marked as hidden.
* @protected
*/
hideHiddenIcons_() {
for (let i = 0, iconInfo; (iconInfo = this.info_.hiddenIcons[i]); i++) {
iconInfo.icon.iconGroup_.setAttribute('display', 'none');
}
}
this.outlinePath_ +=
svgPaths.lineOnAxis('V', bottomRow.baseline - rightCornerYOffset);
this.outlinePath_ += outlinePath;
};
/**
* Add steps for the left side of the block, which may include an output
* connection
* @protected
*/
Drawer.prototype.drawLeft_ = function() {
const outputConnection = this.info_.outputConnection;
this.positionOutputConnection_();
if (outputConnection) {
const tabBottom =
outputConnection.connectionOffsetY + outputConnection.height;
const pathUp = (typeof outputConnection.shape.pathUp === 'function') ?
outputConnection.shape.pathUp(outputConnection.height) :
outputConnection.shape.pathUp;
// Draw a line up to the bottom of the tab.
this.outlinePath_ += svgPaths.lineOnAxis('V', tabBottom) + pathUp;
/**
* Create the outline of the block. This is a single continuous path.
* @protected
*/
drawOutline_() {
this.drawTop_();
for (let r = 1; r < this.info_.rows.length - 1; r++) {
const row = this.info_.rows[r];
if (row.hasJaggedEdge) {
this.drawJaggedEdge_(row);
} else if (row.hasStatement) {
this.drawStatementInput_(row);
} else if (row.hasExternalInput) {
this.drawValueInput_(row);
} else {
this.drawRightSideRow_(row);
}
}
this.drawBottom_();
this.drawLeft_();
}
// Close off the path. This draws a vertical line up to the start of the
// block's path, which may be either a rounded or a sharp corner.
this.outlinePath_ += 'z';
};
/**
* Draw the internals of the block: inline inputs, fields, and icons. These do
* not depend on the outer path for placement.
* @protected
*/
Drawer.prototype.drawInternals_ = function() {
for (let i = 0, row; (row = this.info_.rows[i]); i++) {
for (let j = 0, elem; (elem = row.elements[j]); j++) {
if (Types.isInlineInput(elem)) {
this.drawInlineInput_(
/** @type {!InlineInput} */ (elem));
} else if (Types.isIcon(elem) || Types.isField(elem)) {
this.layoutField_(
/** @type {!Field|!Icon} */
(elem));
/**
* Add steps for the top corner of the block, taking into account
* details such as hats and rounded corners.
* @protected
*/
drawTop_() {
const topRow = this.info_.topRow;
const elements = topRow.elements;
this.positionPreviousConnection_();
this.outlinePath_ += svgPaths.moveBy(topRow.xPos, this.info_.startY);
for (let i = 0, elem; (elem = elements[i]); i++) {
if (Types.isLeftRoundedCorner(elem)) {
this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topLeft;
} else if (Types.isRightRoundedCorner(elem)) {
this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topRight;
} else if (Types.isPreviousConnection(elem)) {
this.outlinePath_ += elem.shape.pathLeft;
} else if (Types.isHat(elem)) {
this.outlinePath_ += this.constants_.START_HAT.path;
} else if (Types.isSpacer(elem)) {
this.outlinePath_ += svgPaths.lineOnAxis('h', elem.width);
}
// No branch for a square corner, because it's a no-op.
}
this.outlinePath_ += svgPaths.lineOnAxis('v', topRow.height);
}
/**
* Add steps for the jagged edge of a row on a collapsed block.
* @param {!Row} row The row to draw the side of.
* @protected
*/
drawJaggedEdge_(row) {
const remainder = row.height - this.constants_.JAGGED_TEETH.height;
this.outlinePath_ +=
this.constants_.JAGGED_TEETH.path + svgPaths.lineOnAxis('v', remainder);
}
/**
* Add steps for an external value input, rendered as a notch in the side
* of the block.
* @param {!Row} row The row that this input belongs to.
* @protected
*/
drawValueInput_(row) {
const input =
/** @type {ExternalValueInput|InlineInput} */ (row.getLastInput());
this.positionExternalValueConnection_(row);
const pathDown = (typeof input.shape.pathDown === 'function') ?
input.shape.pathDown(input.height) :
input.shape.pathDown;
this.outlinePath_ += svgPaths.lineOnAxis('H', input.xPos + input.width) +
pathDown +
svgPaths.lineOnAxis('v', row.height - input.connectionHeight);
}
/**
* Add steps for a statement input.
* @param {!Row} row The row that this input belongs to.
* @protected
*/
drawStatementInput_(row) {
const input = row.getLastInput();
// Where to start drawing the notch, which is on the right side in LTR.
const x = input.xPos + input.notchOffset + input.shape.width;
const innerTopLeftCorner = input.shape.pathRight +
svgPaths.lineOnAxis(
'h', -(input.notchOffset - this.constants_.INSIDE_CORNERS.width)) +
this.constants_.INSIDE_CORNERS.pathTop;
const innerHeight =
row.height - (2 * this.constants_.INSIDE_CORNERS.height);
this.outlinePath_ += svgPaths.lineOnAxis('H', x) + innerTopLeftCorner +
svgPaths.lineOnAxis('v', innerHeight) +
this.constants_.INSIDE_CORNERS.pathBottom +
svgPaths.lineOnAxis('H', row.xPos + row.width);
this.positionStatementInputConnection_(row);
}
/**
* Add steps for the right side of a row that does not have value or
* statement input connections.
* @param {!Row} row The row to draw the side of.
* @protected
*/
drawRightSideRow_(row) {
this.outlinePath_ += svgPaths.lineOnAxis('V', row.yPos + row.height);
}
/**
* Add steps for the bottom edge of a block, possibly including a notch
* for the next connection.
* @protected
*/
drawBottom_() {
const bottomRow = this.info_.bottomRow;
const elems = bottomRow.elements;
this.positionNextConnection_();
let rightCornerYOffset = 0;
let outlinePath = '';
for (let i = elems.length - 1, elem; (elem = elems[i]); i--) {
if (Types.isNextConnection(elem)) {
outlinePath += elem.shape.pathRight;
} else if (Types.isLeftSquareCorner(elem)) {
outlinePath += svgPaths.lineOnAxis('H', bottomRow.xPos);
} else if (Types.isLeftRoundedCorner(elem)) {
outlinePath += this.constants_.OUTSIDE_CORNERS.bottomLeft;
} else if (Types.isRightRoundedCorner(elem)) {
outlinePath += this.constants_.OUTSIDE_CORNERS.bottomRight;
rightCornerYOffset = this.constants_.OUTSIDE_CORNERS.rightHeight;
} else if (Types.isSpacer(elem)) {
outlinePath += svgPaths.lineOnAxis('h', elem.width * -1);
}
}
this.outlinePath_ +=
svgPaths.lineOnAxis('V', bottomRow.baseline - rightCornerYOffset);
this.outlinePath_ += outlinePath;
}
/**
* Add steps for the left side of the block, which may include an output
* connection
* @protected
*/
drawLeft_() {
const outputConnection = this.info_.outputConnection;
this.positionOutputConnection_();
if (outputConnection) {
const tabBottom =
outputConnection.connectionOffsetY + outputConnection.height;
const pathUp = (typeof outputConnection.shape.pathUp === 'function') ?
outputConnection.shape.pathUp(outputConnection.height) :
outputConnection.shape.pathUp;
// Draw a line up to the bottom of the tab.
this.outlinePath_ += svgPaths.lineOnAxis('V', tabBottom) + pathUp;
}
// Close off the path. This draws a vertical line up to the start of the
// block's path, which may be either a rounded or a sharp corner.
this.outlinePath_ += 'z';
}
/**
* Draw the internals of the block: inline inputs, fields, and icons. These
* do not depend on the outer path for placement.
* @protected
*/
drawInternals_() {
for (let i = 0, row; (row = this.info_.rows[i]); i++) {
for (let j = 0, elem; (elem = row.elements[j]); j++) {
if (Types.isInlineInput(elem)) {
this.drawInlineInput_(
/** @type {!InlineInput} */ (elem));
} else if (Types.isIcon(elem) || Types.isField(elem)) {
this.layoutField_(
/** @type {!Field|!Icon} */
(elem));
}
}
}
}
};
/**
* Push a field or icon's new position to its SVG root.
* @param {!Icon|!Field} fieldInfo
* The rendering information for the field or icon.
* @protected
*/
Drawer.prototype.layoutField_ = function(fieldInfo) {
let svgGroup;
if (Types.isField(fieldInfo)) {
svgGroup = fieldInfo.field.getSvgRoot();
} else if (Types.isIcon(fieldInfo)) {
svgGroup = fieldInfo.icon.iconGroup_;
}
const yPos = fieldInfo.centerline - fieldInfo.height / 2;
let xPos = fieldInfo.xPos;
let scale = '';
if (this.info_.RTL) {
xPos = -(xPos + fieldInfo.width);
if (fieldInfo.flipRtl) {
xPos += fieldInfo.width;
scale = 'scale(-1 1)';
/**
* Push a field or icon's new position to its SVG root.
* @param {!Icon|!Field} fieldInfo
* The rendering information for the field or icon.
* @protected
*/
layoutField_(fieldInfo) {
let svgGroup;
if (Types.isField(fieldInfo)) {
svgGroup = fieldInfo.field.getSvgRoot();
} else if (Types.isIcon(fieldInfo)) {
svgGroup = fieldInfo.icon.iconGroup_;
}
}
if (Types.isIcon(fieldInfo)) {
svgGroup.setAttribute('display', 'block');
svgGroup.setAttribute('transform', 'translate(' + xPos + ',' + yPos + ')');
fieldInfo.icon.computeIconLocation();
} else {
svgGroup.setAttribute(
'transform', 'translate(' + xPos + ',' + yPos + ')' + scale);
}
if (this.info_.isInsertionMarker) {
// Fields and icons are invisible on insertion marker. They still have to
// be rendered so that the block can be sized correctly.
svgGroup.setAttribute('display', 'none');
}
};
/**
* Add steps for an inline input.
* @param {!InlineInput} input The information about the
* input to render.
* @protected
*/
Drawer.prototype.drawInlineInput_ = function(input) {
const width = input.width;
const height = input.height;
const yPos = input.centerline - height / 2;
const connectionTop = input.connectionOffsetY;
const connectionBottom = input.connectionHeight + connectionTop;
const connectionRight = input.xPos + input.connectionWidth;
this.inlinePath_ += svgPaths.moveTo(connectionRight, yPos) +
svgPaths.lineOnAxis('v', connectionTop) + input.shape.pathDown +
svgPaths.lineOnAxis('v', height - connectionBottom) +
svgPaths.lineOnAxis('h', width - input.connectionWidth) +
svgPaths.lineOnAxis('v', -height) + 'z';
this.positionInlineInputConnection_(input);
};
/**
* Position the connection on an inline value input, taking into account
* RTL and the small gap between the parent block and child block which lets the
* parent block's dark path show through.
* @param {InlineInput} input The information about
* the input that the connection is on.
* @protected
*/
Drawer.prototype.positionInlineInputConnection_ = function(input) {
const yPos = input.centerline - input.height / 2;
// Move the connection.
if (input.connectionModel) {
// xPos already contains info about startX
let connX = input.xPos + input.connectionWidth + input.connectionOffsetX;
const yPos = fieldInfo.centerline - fieldInfo.height / 2;
let xPos = fieldInfo.xPos;
let scale = '';
if (this.info_.RTL) {
connX *= -1;
xPos = -(xPos + fieldInfo.width);
if (fieldInfo.flipRtl) {
xPos += fieldInfo.width;
scale = 'scale(-1 1)';
}
}
input.connectionModel.setOffsetInBlock(
connX, yPos + input.connectionOffsetY);
}
};
/**
* Position the connection on a statement input, taking into account
* RTL and the small gap between the parent block and child block which lets the
* parent block's dark path show through.
* @param {!Row} row The row that the connection is on.
* @protected
*/
Drawer.prototype.positionStatementInputConnection_ = function(row) {
const input = row.getLastInput();
if (input.connectionModel) {
let connX = row.xPos + row.statementEdge + input.notchOffset;
if (this.info_.RTL) {
connX *= -1;
if (Types.isIcon(fieldInfo)) {
svgGroup.setAttribute('display', 'block');
svgGroup.setAttribute(
'transform', 'translate(' + xPos + ',' + yPos + ')');
fieldInfo.icon.computeIconLocation();
} else {
svgGroup.setAttribute(
'transform', 'translate(' + xPos + ',' + yPos + ')' + scale);
}
input.connectionModel.setOffsetInBlock(connX, row.yPos);
}
};
/**
* Position the connection on an external value input, taking into account
* RTL and the small gap between the parent block and child block which lets the
* parent block's dark path show through.
* @param {!Row} row The row that the connection is on.
* @protected
*/
Drawer.prototype.positionExternalValueConnection_ = function(row) {
const input = row.getLastInput();
if (input.connectionModel) {
let connX = row.xPos + row.width;
if (this.info_.RTL) {
connX *= -1;
if (this.info_.isInsertionMarker) {
// Fields and icons are invisible on insertion marker. They still have to
// be rendered so that the block can be sized correctly.
svgGroup.setAttribute('display', 'none');
}
input.connectionModel.setOffsetInBlock(connX, row.yPos);
}
};
/**
* Position the previous connection on a block.
* @protected
*/
Drawer.prototype.positionPreviousConnection_ = function() {
const topRow = this.info_.topRow;
if (topRow.connection) {
const x = topRow.xPos + topRow.notchOffset;
const connX = (this.info_.RTL ? -x : x);
topRow.connection.connectionModel.setOffsetInBlock(connX, 0);
/**
* Add steps for an inline input.
* @param {!InlineInput} input The information about the
* input to render.
* @protected
*/
drawInlineInput_(input) {
const width = input.width;
const height = input.height;
const yPos = input.centerline - height / 2;
const connectionTop = input.connectionOffsetY;
const connectionBottom = input.connectionHeight + connectionTop;
const connectionRight = input.xPos + input.connectionWidth;
this.inlinePath_ += svgPaths.moveTo(connectionRight, yPos) +
svgPaths.lineOnAxis('v', connectionTop) + input.shape.pathDown +
svgPaths.lineOnAxis('v', height - connectionBottom) +
svgPaths.lineOnAxis('h', width - input.connectionWidth) +
svgPaths.lineOnAxis('v', -height) + 'z';
this.positionInlineInputConnection_(input);
}
};
/**
* Position the next connection on a block.
* @protected
*/
Drawer.prototype.positionNextConnection_ = function() {
const bottomRow = this.info_.bottomRow;
if (bottomRow.connection) {
const connInfo = bottomRow.connection;
const x = connInfo.xPos; // Already contains info about startX.
const connX = (this.info_.RTL ? -x : x);
connInfo.connectionModel.setOffsetInBlock(connX, bottomRow.baseline);
/**
* Position the connection on an inline value input, taking into account
* RTL and the small gap between the parent block and child block which lets
* the parent block's dark path show through.
* @param {InlineInput} input The information about
* the input that the connection is on.
* @protected
*/
positionInlineInputConnection_(input) {
const yPos = input.centerline - input.height / 2;
// Move the connection.
if (input.connectionModel) {
// xPos already contains info about startX
let connX = input.xPos + input.connectionWidth + input.connectionOffsetX;
if (this.info_.RTL) {
connX *= -1;
}
input.connectionModel.setOffsetInBlock(
connX, yPos + input.connectionOffsetY);
}
}
};
/**
* Position the output connection on a block.
* @protected
*/
Drawer.prototype.positionOutputConnection_ = function() {
if (this.info_.outputConnection) {
const x = this.info_.startX + this.info_.outputConnection.connectionOffsetX;
const connX = this.info_.RTL ? -x : x;
this.block_.outputConnection.setOffsetInBlock(
connX, this.info_.outputConnection.connectionOffsetY);
/**
* Position the connection on a statement input, taking into account
* RTL and the small gap between the parent block and child block which lets
* the parent block's dark path show through.
* @param {!Row} row The row that the connection is on.
* @protected
*/
positionStatementInputConnection_(row) {
const input = row.getLastInput();
if (input.connectionModel) {
let connX = row.xPos + row.statementEdge + input.notchOffset;
if (this.info_.RTL) {
connX *= -1;
}
input.connectionModel.setOffsetInBlock(connX, row.yPos);
}
}
};
/**
* Position the connection on an external value input, taking into account
* RTL and the small gap between the parent block and child block which lets
* the parent block's dark path show through.
* @param {!Row} row The row that the connection is on.
* @protected
*/
positionExternalValueConnection_(row) {
const input = row.getLastInput();
if (input.connectionModel) {
let connX = row.xPos + row.width;
if (this.info_.RTL) {
connX *= -1;
}
input.connectionModel.setOffsetInBlock(connX, row.yPos);
}
}
/**
* Position the previous connection on a block.
* @protected
*/
positionPreviousConnection_() {
const topRow = this.info_.topRow;
if (topRow.connection) {
const x = topRow.xPos + topRow.notchOffset;
const connX = (this.info_.RTL ? -x : x);
topRow.connection.connectionModel.setOffsetInBlock(connX, 0);
}
}
/**
* Position the next connection on a block.
* @protected
*/
positionNextConnection_() {
const bottomRow = this.info_.bottomRow;
if (bottomRow.connection) {
const connInfo = bottomRow.connection;
const x = connInfo.xPos; // Already contains info about startX.
const connX = (this.info_.RTL ? -x : x);
connInfo.connectionModel.setOffsetInBlock(connX, bottomRow.baseline);
}
}
/**
* Position the output connection on a block.
* @protected
*/
positionOutputConnection_() {
if (this.info_.outputConnection) {
const x =
this.info_.startX + this.info_.outputConnection.connectionOffsetX;
const connX = this.info_.RTL ? -x : x;
this.block_.outputConnection.setOffsetInBlock(
connX, this.info_.outputConnection.connectionOffsetY);
}
}
}
exports.Drawer = Drawer;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,8 @@ const debug = goog.require('Blockly.blockRendering.debug');
const svgPaths = goog.require('Blockly.utils.svgPaths');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {ConstantProvider} = goog.requireType('Blockly.geras.ConstantProvider');
const {Drawer: BaseDrawer} = goog.require('Blockly.blockRendering.Drawer');
const {Highlighter} = goog.require('Blockly.geras.Highlighter');
/* eslint-disable-next-line no-unused-vars */
@@ -45,6 +47,9 @@ class Drawer extends BaseDrawer {
super(block, info);
// Unlike Thrasos, Geras has highlights and drop shadows.
this.highlighter_ = new Highlighter(info);
/** @type {!ConstantProvider} */
this.constants_;
}
/**

View File

@@ -21,6 +21,8 @@ goog.module('Blockly.geras.RenderInfo');
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {BottomRow} = goog.requireType('Blockly.blockRendering.BottomRow');
/* eslint-disable-next-line no-unused-vars */
const {ConstantProvider} = goog.requireType('Blockly.geras.ConstantProvider');
const {ExternalValueInput} = goog.require('Blockly.blockRendering.ExternalValueInput');
/* eslint-disable-next-line no-unused-vars */
const {Field} = goog.requireType('Blockly.blockRendering.Field');
@@ -55,6 +57,9 @@ class RenderInfo extends BaseRenderInfo {
*/
constructor(renderer, block) {
super(renderer, block);
/** @type {!ConstantProvider} */
this.constants_;
}
/**

View File

@@ -18,7 +18,9 @@
goog.module('Blockly.geras.InlineInput');
/* eslint-disable-next-line no-unused-vars */
const {ConstantProvider} = goog.requireType('Blockly.blockRendering.ConstantProvider');
const {ConstantProvider: BaseConstantProvider} = goog.requireType('Blockly.blockRendering.ConstantProvider');
/* eslint-disable-next-line no-unused-vars */
const {ConstantProvider: GerasConstantProvider} = goog.requireType('Blockly.geras.ConstantProvider');
const {InlineInput: BaseInlineInput} = goog.require('Blockly.blockRendering.InlineInput');
/* eslint-disable-next-line no-unused-vars */
const {Input} = goog.requireType('Blockly.Input');
@@ -31,7 +33,7 @@ const {Input} = goog.requireType('Blockly.Input');
*/
class InlineInput extends BaseInlineInput {
/**
* @param {!ConstantProvider} constants The rendering
* @param {!BaseConstantProvider} constants The rendering
* constants provider.
* @param {!Input} input The inline input to measure and store
* information for.
@@ -41,6 +43,9 @@ class InlineInput extends BaseInlineInput {
constructor(constants, input) {
super(constants, input);
/** @type {!GerasConstantProvider} */
this.constants_;
if (this.connectedBlock) {
// We allow the dark path to show on the parent block so that the child
// block looks embossed. This takes up an extra pixel in both x and y.

View File

@@ -18,7 +18,9 @@
goog.module('Blockly.geras.StatementInput');
/* eslint-disable-next-line no-unused-vars */
const {ConstantProvider} = goog.requireType('Blockly.blockRendering.ConstantProvider');
const {ConstantProvider: BaseConstantProvider} = goog.requireType('Blockly.blockRendering.ConstantProvider');
/* eslint-disable-next-line no-unused-vars */
const {ConstantProvider: GerasConstantProvider} = goog.requireType('Blockly.geras.ConstantProvider');
/* eslint-disable-next-line no-unused-vars */
const {Input} = goog.requireType('Blockly.Input');
const {StatementInput: BaseStatementInput} = goog.require('Blockly.blockRendering.StatementInput');
@@ -31,7 +33,7 @@ const {StatementInput: BaseStatementInput} = goog.require('Blockly.blockRenderin
*/
class StatementInput extends BaseStatementInput {
/**
* @param {!ConstantProvider} constants The rendering
* @param {!BaseConstantProvider} constants The rendering
* constants provider.
* @param {!Input} input The statement input to measure and store
* information for.
@@ -41,6 +43,9 @@ class StatementInput extends BaseStatementInput {
constructor(constants, input) {
super(constants, input);
/** @type {!GerasConstantProvider} */
this.constants_;
if (this.connectedBlock) {
// We allow the dark path to show on the parent block so that the child
// block looks embossed. This takes up an extra pixel in both x and y.

View File

@@ -47,6 +47,11 @@ class Drawer extends BaseDrawer {
*/
constructor(block, info) {
super(block, info);
/**
* @type {!RenderInfo}
*/
this.info_;
}
/**

View File

@@ -58,6 +58,9 @@ class RenderInfo extends BaseRenderInfo {
constructor(renderer, block) {
super(renderer, block);
/** @type {!ConstantProvider} */
this.constants_;
/**
* An object with rendering information about the top row of the block.
* @type {!TopRow}

View File

@@ -21,15 +21,17 @@ const {ASTNode} = goog.requireType('Blockly.ASTNode');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {Connection} = goog.requireType('Blockly.Connection');
/* eslint-disable-next-line no-unused-vars */
const {ConstantProvider} = goog.requireType('Blockly.blockRendering.ConstantProvider');
const {ConstantProvider: BaseConstantProvider} = goog.requireType('Blockly.blockRendering.ConstantProvider');
const {MarkerSvg: BaseMarkerSvg} = goog.require('Blockly.blockRendering.MarkerSvg');
/* eslint-disable-next-line no-unused-vars */
const {Marker} = goog.requireType('Blockly.Marker');
/* eslint-disable-next-line no-unused-vars */
const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
const {Svg} = goog.require('Blockly.utils.Svg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/* eslint-disable-next-line no-unused-vars */
const {ConstantProvider: ZelosConstantProvider} = goog.requireType('Blockly.zelos.ConstantProvider');
/**
@@ -39,7 +41,7 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
class MarkerSvg extends BaseMarkerSvg {
/**
* @param {!WorkspaceSvg} workspace The workspace the marker belongs to.
* @param {!ConstantProvider} constants The constants for
* @param {!BaseConstantProvider} constants The constants for
* the renderer.
* @param {!Marker} marker The marker to draw.
* @alias Blockly.zelos.MarkerSvg
@@ -47,6 +49,9 @@ class MarkerSvg extends BaseMarkerSvg {
constructor(workspace, constants, marker) {
super(workspace, constants, marker);
/** @type {!ZelosConstantProvider} */
this.constants_;
/**
* @type {SVGCircleElement}
* @private
@@ -61,7 +66,8 @@ class MarkerSvg extends BaseMarkerSvg {
*/
showWithInputOutput_(curNode) {
const block = /** @type {!BlockSvg} */ (curNode.getSourceBlock());
const connection = /** @type {!Connection} */ (curNode.getLocation());
const connection =
/** @type {!RenderedConnection} */ (curNode.getLocation());
const offsetInBlock = connection.getOffsetInBlock();
this.positionCircle_(offsetInBlock.x, offsetInBlock.y);

File diff suppressed because it is too large Load Diff

View File

@@ -29,41 +29,175 @@ const {Workspace} = goog.requireType('Blockly.Workspace');
/**
* Class for storing and updating a workspace's theme and UI components.
* @param {!WorkspaceSvg} workspace The main workspace.
* @param {!Theme} theme The workspace theme.
* @constructor
* @package
* @alias Blockly.ThemeManager
*/
const ThemeManager = function(workspace, theme) {
class ThemeManager {
/**
* The main workspace.
* @type {!WorkspaceSvg}
* @private
* @param {!WorkspaceSvg} workspace The main workspace.
* @param {!Theme} theme The workspace theme.
* @package
* @alias Blockly.ThemeManager
*/
this.workspace_ = workspace;
constructor(workspace, theme) {
/**
* The main workspace.
* @type {!WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* The Blockly theme to use.
* @type {!Theme}
* @private
*/
this.theme_ = theme;
/**
* A list of workspaces that are subscribed to this theme.
* @type {!Array<Workspace>}
* @private
*/
this.subscribedWorkspaces_ = [];
/**
* A map of subscribed UI components, keyed by component name.
* @type {!Object<string, !Array<!ThemeManager.Component>>}
* @private
*/
this.componentDB_ = Object.create(null);
}
/**
* The Blockly theme to use.
* @type {!Theme}
* @private
* Get the workspace theme.
* @return {!Theme} The workspace theme.
* @package
*/
this.theme_ = theme;
getTheme() {
return this.theme_;
}
/**
* A list of workspaces that are subscribed to this theme.
* @type {!Array<Workspace>}
* @private
* Set the workspace theme, and refresh the workspace and all components.
* @param {!Theme} theme The workspace theme.
* @package
*/
this.subscribedWorkspaces_ = [];
setTheme(theme) {
const prevTheme = this.theme_;
this.theme_ = theme;
// Set the theme name onto the injection div.
const injectionDiv = this.workspace_.getInjectionDiv();
if (injectionDiv) {
if (prevTheme) {
dom.removeClass(injectionDiv, prevTheme.getClassName());
}
dom.addClass(injectionDiv, this.theme_.getClassName());
}
// Refresh all subscribed workspaces.
for (let i = 0, workspace; (workspace = this.subscribedWorkspaces_[i]);
i++) {
workspace.refreshTheme();
}
// Refresh all registered Blockly UI components.
for (let i = 0, keys = Object.keys(this.componentDB_), key; (key = keys[i]);
i++) {
for (let j = 0, component; (component = this.componentDB_[key][j]); j++) {
const element = component.element;
const propertyName = component.propertyName;
const style = this.theme_ && this.theme_.getComponentStyle(key);
element.style[propertyName] = style || '';
}
}
for (const workspace of this.subscribedWorkspaces_) {
workspace.hideChaff();
}
}
/**
* A map of subscribed UI components, keyed by component name.
* @type {!Object<string, !Array<!ThemeManager.Component>>}
* @private
* Subscribe a workspace to changes to the selected theme. If a new theme is
* set, the workspace is called to refresh its blocks.
* @param {!Workspace} workspace The workspace to subscribe.
* @package
*/
this.componentDB_ = Object.create(null);
};
subscribeWorkspace(workspace) {
this.subscribedWorkspaces_.push(workspace);
}
/**
* Unsubscribe a workspace to changes to the selected theme.
* @param {!Workspace} workspace The workspace to unsubscribe.
* @package
*/
unsubscribeWorkspace(workspace) {
if (!arrayUtils.removeElem(this.subscribedWorkspaces_, workspace)) {
throw Error(
'Cannot unsubscribe a workspace that hasn\'t been subscribed.');
}
}
/**
* Subscribe an element to changes to the selected theme. If a new theme is
* selected, the element's style is refreshed with the new theme's style.
* @param {!Element} element The element to subscribe.
* @param {string} componentName The name used to identify the component. This
* must be the same name used to configure the style in the Theme object.
* @param {string} propertyName The inline style property name to update.
* @package
*/
subscribe(element, componentName, propertyName) {
if (!this.componentDB_[componentName]) {
this.componentDB_[componentName] = [];
}
// Add the element to our component map.
this.componentDB_[componentName].push(
{element: element, propertyName: propertyName});
// Initialize the element with its corresponding theme style.
const style = this.theme_ && this.theme_.getComponentStyle(componentName);
element.style[propertyName] = style || '';
}
/**
* Unsubscribe an element to changes to the selected theme.
* @param {Element} element The element to unsubscribe.
* @package
*/
unsubscribe(element) {
if (!element) {
return;
}
// Go through all component, and remove any references to this element.
const componentNames = Object.keys(this.componentDB_);
for (let c = 0, componentName; (componentName = componentNames[c]); c++) {
const elements = this.componentDB_[componentName];
for (let i = elements.length - 1; i >= 0; i--) {
if (elements[i].element === element) {
elements.splice(i, 1);
}
}
// Clean up the component map entry if the list is empty.
if (!this.componentDB_[componentName].length) {
delete this.componentDB_[componentName];
}
}
}
/**
* Dispose of this theme manager.
* @package
* @suppress {checkTypes}
*/
dispose() {
this.owner_ = null;
this.theme_ = null;
this.subscribedWorkspaces_ = null;
this.componentDB_ = null;
}
}
/**
* A Blockly UI component type.
@@ -74,134 +208,4 @@ const ThemeManager = function(workspace, theme) {
*/
ThemeManager.Component;
/**
* Get the workspace theme.
* @return {!Theme} The workspace theme.
* @package
*/
ThemeManager.prototype.getTheme = function() {
return this.theme_;
};
/**
* Set the workspace theme, and refresh the workspace and all components.
* @param {!Theme} theme The workspace theme.
* @package
*/
ThemeManager.prototype.setTheme = function(theme) {
const prevTheme = this.theme_;
this.theme_ = theme;
// Set the theme name onto the injection div.
const injectionDiv = this.workspace_.getInjectionDiv();
if (injectionDiv) {
if (prevTheme) {
dom.removeClass(injectionDiv, prevTheme.getClassName());
}
dom.addClass(injectionDiv, this.theme_.getClassName());
}
// Refresh all subscribed workspaces.
for (let i = 0, workspace; (workspace = this.subscribedWorkspaces_[i]); i++) {
workspace.refreshTheme();
}
// Refresh all registered Blockly UI components.
for (let i = 0, keys = Object.keys(this.componentDB_), key; (key = keys[i]);
i++) {
for (let j = 0, component; (component = this.componentDB_[key][j]); j++) {
const element = component.element;
const propertyName = component.propertyName;
const style = this.theme_ && this.theme_.getComponentStyle(key);
element.style[propertyName] = style || '';
}
}
for (const workspace of this.subscribedWorkspaces_) {
workspace.hideChaff();
}
};
/**
* Subscribe a workspace to changes to the selected theme. If a new theme is
* set, the workspace is called to refresh its blocks.
* @param {!Workspace} workspace The workspace to subscribe.
* @package
*/
ThemeManager.prototype.subscribeWorkspace = function(workspace) {
this.subscribedWorkspaces_.push(workspace);
};
/**
* Unsubscribe a workspace to changes to the selected theme.
* @param {!Workspace} workspace The workspace to unsubscribe.
* @package
*/
ThemeManager.prototype.unsubscribeWorkspace = function(workspace) {
if (!arrayUtils.removeElem(this.subscribedWorkspaces_, workspace)) {
throw Error('Cannot unsubscribe a workspace that hasn\'t been subscribed.');
}
};
/**
* Subscribe an element to changes to the selected theme. If a new theme is
* selected, the element's style is refreshed with the new theme's style.
* @param {!Element} element The element to subscribe.
* @param {string} componentName The name used to identify the component. This
* must be the same name used to configure the style in the Theme object.
* @param {string} propertyName The inline style property name to update.
* @package
*/
ThemeManager.prototype.subscribe = function(
element, componentName, propertyName) {
if (!this.componentDB_[componentName]) {
this.componentDB_[componentName] = [];
}
// Add the element to our component map.
this.componentDB_[componentName].push(
{element: element, propertyName: propertyName});
// Initialize the element with its corresponding theme style.
const style = this.theme_ && this.theme_.getComponentStyle(componentName);
element.style[propertyName] = style || '';
};
/**
* Unsubscribe an element to changes to the selected theme.
* @param {Element} element The element to unsubscribe.
* @package
*/
ThemeManager.prototype.unsubscribe = function(element) {
if (!element) {
return;
}
// Go through all component, and remove any references to this element.
const componentNames = Object.keys(this.componentDB_);
for (let c = 0, componentName; (componentName = componentNames[c]); c++) {
const elements = this.componentDB_[componentName];
for (let i = elements.length - 1; i >= 0; i--) {
if (elements[i].element === element) {
elements.splice(i, 1);
}
}
// Clean up the component map entry if the list is empty.
if (!this.componentDB_[componentName].length) {
delete this.componentDB_[componentName];
}
}
};
/**
* Dispose of this theme manager.
* @package
* @suppress {checkTypes}
*/
ThemeManager.prototype.dispose = function() {
this.owner_ = null;
this.theme_ = null;
this.subscribedWorkspaces_ = null;
this.componentDB_ = null;
};
exports.ThemeManager = ThemeManager;

View File

@@ -29,26 +29,67 @@ const {ToolboxItem} = goog.require('Blockly.ToolboxItem');
/**
* Class for a toolbox separator. This is the thin visual line that appears on
* the toolbox. This item is not interactable.
* @param {!toolbox.SeparatorInfo} separatorDef The information
* needed to create a separator.
* @param {!IToolbox} toolbox The parent toolbox for the separator.
* @constructor
* @extends {ToolboxItem}
* @alias Blockly.ToolboxSeparator
*/
const ToolboxSeparator = function(separatorDef, toolbox) {
ToolboxSeparator.superClass_.constructor.call(this, separatorDef, toolbox);
class ToolboxSeparator extends ToolboxItem {
/**
* All the CSS class names that are used to create a separator.
* @type {!ToolboxSeparator.CssConfig}
* @param {!toolbox.SeparatorInfo} separatorDef The information
* needed to create a separator.
* @param {!IToolbox} toolbox The parent toolbox for the separator.
* @alias Blockly.ToolboxSeparator
*/
constructor(separatorDef, toolbox) {
super(separatorDef, toolbox);
/**
* All the CSS class names that are used to create a separator.
* @type {!ToolboxSeparator.CssConfig}
* @protected
*/
this.cssConfig_ = {'container': 'blocklyTreeSeparator'};
/**
* @type {?Element}
* @private
*/
this.htmlDiv_ = null;
const cssConfig = separatorDef['cssconfig'] || separatorDef['cssConfig'];
object.mixin(this.cssConfig_, cssConfig);
}
/**
* @override
*/
init() {
this.createDom_();
}
/**
* Creates the DOM for a separator.
* @return {!Element} The parent element for the separator.
* @protected
*/
this.cssConfig_ = {'container': 'blocklyTreeSeparator'};
createDom_() {
const container = document.createElement('div');
dom.addClass(container, this.cssConfig_['container']);
this.htmlDiv_ = container;
return container;
}
const cssConfig = separatorDef['cssconfig'] || separatorDef['cssConfig'];
object.mixin(this.cssConfig_, cssConfig);
};
object.inherits(ToolboxSeparator, ToolboxItem);
/**
* @override
*/
getDiv() {
return /** @type {!Element} */ (this.htmlDiv_);
}
/**
* @override
*/
dispose() {
dom.removeNode(/** @type {!Element} */ (this.htmlDiv_));
}
}
/**
* All the CSS class names that are used to create a separator.
@@ -60,43 +101,10 @@ ToolboxSeparator.CssConfig;
/**
* Name used for registering a toolbox separator.
* @const {string}
* @type {string}
*/
ToolboxSeparator.registrationName = 'sep';
/**
* @override
*/
ToolboxSeparator.prototype.init = function() {
this.createDom_();
};
/**
* Creates the DOM for a separator.
* @return {!Element} The parent element for the separator.
* @protected
*/
ToolboxSeparator.prototype.createDom_ = function() {
const container = document.createElement('div');
dom.addClass(container, this.cssConfig_['container']);
this.htmlDiv_ = container;
return container;
};
/**
* @override
*/
ToolboxSeparator.prototype.getDiv = function() {
return this.htmlDiv_;
};
/**
* @override
*/
ToolboxSeparator.prototype.dispose = function() {
dom.removeNode(this.htmlDiv_);
};
/**
* CSS for Toolbox. See css.js for use.
*/

View File

@@ -17,11 +17,8 @@ goog.module('Blockly.Warning');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const object = goog.require('Blockly.utils.object');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {Bubble} = goog.require('Blockly.Bubble');
/* eslint-disable-next-line no-unused-vars */
const {Coordinate} = goog.requireType('Blockly.utils.Coordinate');
@@ -33,145 +30,146 @@ goog.require('Blockly.Events.BubbleOpen');
/**
* Class for a warning.
* @param {!Block} block The block associated with this warning.
* @extends {Icon}
* @constructor
* @alias Blockly.Warning
*/
const Warning = function(block) {
Warning.superClass_.constructor.call(this, block);
this.createIcon();
// The text_ object can contain multiple warnings.
this.text_ = Object.create(null);
class Warning extends Icon {
/**
* @param {!BlockSvg} block The block associated with this warning.
* @alias Blockly.Warning
*/
constructor(block) {
super(block);
this.createIcon();
// The text_ object can contain multiple warnings.
this.text_ = Object.create(null);
/**
* The top-level node of the warning text, or null if not created.
* @type {?SVGTextElement}
* @private
*/
this.paragraphElement_ = null;
/**
* Does this icon get hidden when the block is collapsed?
* @type {boolean}
*/
this.collapseHidden = false;
}
/**
* The top-level node of the warning text, or null if not created.
* @type {?SVGTextElement}
* Draw the warning icon.
* @param {!Element} group The icon group.
* @protected
*/
drawIcon_(group) {
// Triangle with rounded corners.
dom.createSvgElement(
Svg.PATH, {
'class': 'blocklyIconShape',
'd': 'M2,15Q-1,15 0.5,12L6.5,1.7Q8,-1 9.5,1.7L15.5,12Q17,15 14,15z',
},
group);
// Can't use a real '!' text character since different browsers and
// operating systems render it differently. Body of exclamation point.
dom.createSvgElement(
Svg.PATH, {
'class': 'blocklyIconSymbol',
'd': 'm7,4.8v3.16l0.27,2.27h1.46l0.27,-2.27v-3.16z',
},
group);
// Dot of exclamation point.
dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyIconSymbol',
'x': '7',
'y': '11',
'height': '2',
'width': '2',
},
group);
}
/**
* Show or hide the warning 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, 'warning'));
if (visible) {
this.createBubble_();
} else {
this.disposeBubble_();
}
}
/**
* Show the bubble.
* @private
*/
this.paragraphElement_ = null;
createBubble_() {
this.paragraphElement_ = Bubble.textToDom(this.getText());
this.bubble_ = Bubble.createNonEditableBubble(
this.paragraphElement_, /** @type {!BlockSvg} */ (this.block_),
/** @type {!Coordinate} */ (this.iconXY_));
this.applyColour();
}
/**
* Does this icon get hidden when the block is collapsed?
* @type {boolean}
* Dispose of the bubble and references to it.
* @private
*/
this.collapseHidden = false;
};
object.inherits(Warning, Icon);
/**
* Draw the warning icon.
* @param {!Element} group The icon group.
* @protected
*/
Warning.prototype.drawIcon_ = function(group) {
// Triangle with rounded corners.
dom.createSvgElement(
Svg.PATH, {
'class': 'blocklyIconShape',
'd': 'M2,15Q-1,15 0.5,12L6.5,1.7Q8,-1 9.5,1.7L15.5,12Q17,15 14,15z',
},
group);
// Can't use a real '!' text character since different browsers and operating
// systems render it differently.
// Body of exclamation point.
dom.createSvgElement(
Svg.PATH, {
'class': 'blocklyIconSymbol',
'd': 'm7,4.8v3.16l0.27,2.27h1.46l0.27,-2.27v-3.16z',
},
group);
// Dot of exclamation point.
dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyIconSymbol',
'x': '7',
'y': '11',
'height': '2',
'width': '2',
},
group);
};
/**
* Show or hide the warning bubble.
* @param {boolean} visible True if the bubble should be visible.
*/
Warning.prototype.setVisible = function(visible) {
if (visible === this.isVisible()) {
return;
disposeBubble_() {
this.bubble_.dispose();
this.bubble_ = null;
this.paragraphElement_ = null;
}
eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
this.block_, visible, 'warning'));
if (visible) {
this.createBubble_();
} else {
this.disposeBubble_();
}
};
/**
* Show the bubble.
* @private
*/
Warning.prototype.createBubble_ = function() {
this.paragraphElement_ = Bubble.textToDom(this.getText());
this.bubble_ = Bubble.createNonEditableBubble(
this.paragraphElement_, /** @type {!BlockSvg} */ (this.block_),
/** @type {!Coordinate} */ (this.iconXY_));
this.applyColour();
};
/**
* Dispose of the bubble and references to it.
* @private
*/
Warning.prototype.disposeBubble_ = function() {
this.bubble_.dispose();
this.bubble_ = null;
this.paragraphElement_ = null;
};
/**
* Set this warning's text.
* @param {string} text Warning text (or '' to delete). This supports
* linebreaks.
* @param {string} id An ID for this text entry to be able to maintain
* multiple warnings.
*/
Warning.prototype.setText = function(text, id) {
if (this.text_[id] === text) {
return;
/**
* Set this warning's text.
* @param {string} text Warning text (or '' to delete). This supports
* linebreaks.
* @param {string} id An ID for this text entry to be able to maintain
* multiple warnings.
*/
setText(text, id) {
if (this.text_[id] === text) {
return;
}
if (text) {
this.text_[id] = text;
} else {
delete this.text_[id];
}
if (this.isVisible()) {
this.setVisible(false);
this.setVisible(true);
}
}
if (text) {
this.text_[id] = text;
} else {
delete this.text_[id];
}
if (this.isVisible()) {
this.setVisible(false);
this.setVisible(true);
}
};
/**
* Get this warning's texts.
* @return {string} All texts concatenated into one string.
*/
Warning.prototype.getText = function() {
const allWarnings = [];
for (const id in this.text_) {
allWarnings.push(this.text_[id]);
/**
* Get this warning's texts.
* @return {string} All texts concatenated into one string.
*/
getText() {
const allWarnings = [];
for (const id in this.text_) {
allWarnings.push(this.text_[id]);
}
return allWarnings.join('\n');
}
return allWarnings.join('\n');
};
/**
* Dispose of this warning.
*/
Warning.prototype.dispose = function() {
this.block_.warning = null;
Icon.prototype.dispose.call(this);
};
/**
* Dispose of this warning.
*/
dispose() {
this.block_.warning = null;
Icon.prototype.dispose.call(this);
}
}
exports.Warning = Warning;

View File

@@ -33,358 +33,367 @@ goog.require('Blockly.Events.CommentMove');
/**
* Class for a workspace comment.
* @param {!Workspace} workspace The block's workspace.
* @param {string} content The content of this workspace comment.
* @param {number} height Height of the comment.
* @param {number} width Width of the comment.
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
* create a new ID.
* @constructor
* @alias Blockly.WorkspaceComment
*/
const WorkspaceComment = function(workspace, content, height, width, opt_id) {
/** @type {string} */
this.id = (opt_id && !workspace.getCommentById(opt_id)) ?
opt_id :
idGenerator.genUid();
workspace.addTopComment(this);
class WorkspaceComment {
/**
* The comment's position in workspace units. (0, 0) is at the workspace's
* origin; scale does not change this value.
* @type {!Coordinate}
* @protected
* @param {!Workspace} workspace The block's workspace.
* @param {string} content The content of this workspace comment.
* @param {number} height Height of the comment.
* @param {number} width Width of the comment.
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
* create a new ID.
* @alias Blockly.WorkspaceComment
*/
this.xy_ = new Coordinate(0, 0);
constructor(workspace, content, height, width, opt_id) {
/** @type {string} */
this.id = (opt_id && !workspace.getCommentById(opt_id)) ?
opt_id :
idGenerator.genUid();
/**
* The comment's height in workspace units. Scale does not change this value.
* @type {number}
* @protected
*/
this.height_ = height;
workspace.addTopComment(this);
/**
* The comment's width in workspace units. Scale does not change this value.
* @type {number}
* @protected
*/
this.width_ = width;
/**
* The comment's position in workspace units. (0, 0) is at the workspace's
* origin; scale does not change this value.
* @type {!Coordinate}
* @protected
*/
this.xy_ = new Coordinate(0, 0);
/**
* @type {!Workspace}
*/
this.workspace = workspace;
/**
* The comment's height in workspace units. Scale does not change this
* value.
* @type {number}
* @protected
*/
this.height_ = height;
/**
* @protected
* @type {boolean}
*/
this.RTL = workspace.RTL;
/**
* The comment's width in workspace units. Scale does not change this
* value.
* @type {number}
* @protected
*/
this.width_ = width;
/**
* @type {boolean}
* @private
*/
this.deletable_ = true;
/**
* @type {!Workspace}
*/
this.workspace = workspace;
/**
* @type {boolean}
* @private
*/
this.movable_ = true;
/**
* @protected
* @type {boolean}
*/
this.RTL = workspace.RTL;
/**
* @type {boolean}
* @private
*/
this.editable_ = true;
/**
* @type {boolean}
* @private
*/
this.deletable_ = true;
/**
* @protected
* @type {string}
*/
this.content_ = content;
/**
* @type {boolean}
* @private
*/
this.movable_ = true;
/**
* Whether this comment has been disposed.
* @protected
* @type {boolean}
*/
this.disposed_ = false;
/**
* @type {boolean}
* @private
*/
this.editable_ = true;
/**
* @package
* @type {boolean}
*/
this.isComment = true;
WorkspaceComment.fireCreateEvent(this);
};
/**
* Dispose of this comment.
* @package
*/
WorkspaceComment.prototype.dispose = function() {
if (this.disposed_) {
return;
}
if (eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_DELETE))(this));
}
// Remove from the list of top comments and the comment database.
this.workspace.removeTopComment(this);
this.disposed_ = true;
};
// Height, width, x, and y are all stored on even non-rendered comments, to
// preserve state if you pass the contents through a headless workspace.
/**
* Get comment height.
* @return {number} Comment height.
* @package
*/
WorkspaceComment.prototype.getHeight = function() {
return this.height_;
};
/**
* Set comment height.
* @param {number} height Comment height.
* @package
*/
WorkspaceComment.prototype.setHeight = function(height) {
this.height_ = height;
};
/**
* Get comment width.
* @return {number} Comment width.
* @package
*/
WorkspaceComment.prototype.getWidth = function() {
return this.width_;
};
/**
* Set comment width.
* @param {number} width comment width.
* @package
*/
WorkspaceComment.prototype.setWidth = function(width) {
this.width_ = width;
};
/**
* Get stored location.
* @return {!Coordinate} The comment's stored location.
* This is not valid if the comment is currently being dragged.
* @package
*/
WorkspaceComment.prototype.getXY = function() {
return new Coordinate(this.xy_.x, this.xy_.y);
};
/**
* Move a comment by a relative offset.
* @param {number} dx Horizontal offset, in workspace units.
* @param {number} dy Vertical offset, in workspace units.
* @package
*/
WorkspaceComment.prototype.moveBy = function(dx, dy) {
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(this);
this.xy_.translate(dx, dy);
event.recordNew();
eventUtils.fire(event);
};
/**
* Get whether this comment is deletable or not.
* @return {boolean} True if deletable.
* @package
*/
WorkspaceComment.prototype.isDeletable = function() {
return this.deletable_ &&
!(this.workspace && this.workspace.options.readOnly);
};
/**
* Set whether this comment is deletable or not.
* @param {boolean} deletable True if deletable.
* @package
*/
WorkspaceComment.prototype.setDeletable = function(deletable) {
this.deletable_ = deletable;
};
/**
* Get whether this comment is movable or not.
* @return {boolean} True if movable.
* @package
*/
WorkspaceComment.prototype.isMovable = function() {
return this.movable_ && !(this.workspace && this.workspace.options.readOnly);
};
/**
* Set whether this comment is movable or not.
* @param {boolean} movable True if movable.
* @package
*/
WorkspaceComment.prototype.setMovable = function(movable) {
this.movable_ = movable;
};
/**
* Get whether this comment is editable or not.
* @return {boolean} True if editable.
*/
WorkspaceComment.prototype.isEditable = function() {
return this.editable_ && !(this.workspace && this.workspace.options.readOnly);
};
/**
* Set whether this comment is editable or not.
* @param {boolean} editable True if editable.
*/
WorkspaceComment.prototype.setEditable = function(editable) {
this.editable_ = editable;
};
/**
* Returns this comment's text.
* @return {string} Comment text.
* @package
*/
WorkspaceComment.prototype.getContent = function() {
return this.content_;
};
/**
* Set this comment's content.
* @param {string} content Comment content.
* @package
*/
WorkspaceComment.prototype.setContent = function(content) {
if (this.content_ !== content) {
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_CHANGE))(
this, this.content_, content));
/**
* @protected
* @type {string}
*/
this.content_ = content;
/**
* Whether this comment has been disposed.
* @protected
* @type {boolean}
*/
this.disposed_ = false;
/**
* @package
* @type {boolean}
*/
this.isComment = true;
WorkspaceComment.fireCreateEvent(this);
}
};
/**
* Encode a comment subtree as XML with XY coordinates.
* @param {boolean=} opt_noId True if the encoder should skip the comment ID.
* @return {!Element} Tree of XML elements.
* @package
*/
WorkspaceComment.prototype.toXmlWithXY = function(opt_noId) {
const element = this.toXml(opt_noId);
element.setAttribute('x', Math.round(this.xy_.x));
element.setAttribute('y', Math.round(this.xy_.y));
element.setAttribute('h', this.height_);
element.setAttribute('w', this.width_);
return element;
};
/**
* Encode a comment subtree as XML, but don't serialize the XY coordinates.
* This method avoids some expensive metrics-related calls that are made in
* toXmlWithXY().
* @param {boolean=} opt_noId True if the encoder should skip the comment ID.
* @return {!Element} Tree of XML elements.
* @package
*/
WorkspaceComment.prototype.toXml = function(opt_noId) {
const commentElement = xml.createElement('comment');
if (!opt_noId) {
commentElement.id = this.id;
}
commentElement.textContent = this.getContent();
return commentElement;
};
/**
* Fire a create event for the given workspace comment, if comments are enabled.
* @param {!WorkspaceComment} comment The comment that was just created.
* @package
*/
WorkspaceComment.fireCreateEvent = function(comment) {
if (eventUtils.isEnabled()) {
const existingGroup = eventUtils.getGroup();
if (!existingGroup) {
eventUtils.setGroup(true);
/**
* Dispose of this comment.
* @package
*/
dispose() {
if (this.disposed_) {
return;
}
try {
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_CREATE))(comment));
} finally {
if (eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_DELETE))(this));
}
// Remove from the list of top comments and the comment database.
this.workspace.removeTopComment(this);
this.disposed_ = true;
}
// Height, width, x, and y are all stored on even non-rendered comments, to
// preserve state if you pass the contents through a headless workspace.
/**
* Get comment height.
* @return {number} Comment height.
* @package
*/
getHeight() {
return this.height_;
}
/**
* Set comment height.
* @param {number} height Comment height.
* @package
*/
setHeight(height) {
this.height_ = height;
}
/**
* Get comment width.
* @return {number} Comment width.
* @package
*/
getWidth() {
return this.width_;
}
/**
* Set comment width.
* @param {number} width comment width.
* @package
*/
setWidth(width) {
this.width_ = width;
}
/**
* Get stored location.
* @return {!Coordinate} The comment's stored location.
* This is not valid if the comment is currently being dragged.
* @package
*/
getXY() {
return new Coordinate(this.xy_.x, this.xy_.y);
}
/**
* Move a comment by a relative offset.
* @param {number} dx Horizontal offset, in workspace units.
* @param {number} dy Vertical offset, in workspace units.
* @package
*/
moveBy(dx, dy) {
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(this);
this.xy_.translate(dx, dy);
event.recordNew();
eventUtils.fire(event);
}
/**
* Get whether this comment is deletable or not.
* @return {boolean} True if deletable.
* @package
*/
isDeletable() {
return this.deletable_ &&
!(this.workspace && this.workspace.options.readOnly);
}
/**
* Set whether this comment is deletable or not.
* @param {boolean} deletable True if deletable.
* @package
*/
setDeletable(deletable) {
this.deletable_ = deletable;
}
/**
* Get whether this comment is movable or not.
* @return {boolean} True if movable.
* @package
*/
isMovable() {
return this.movable_ &&
!(this.workspace && this.workspace.options.readOnly);
}
/**
* Set whether this comment is movable or not.
* @param {boolean} movable True if movable.
* @package
*/
setMovable(movable) {
this.movable_ = movable;
}
/**
* Get whether this comment is editable or not.
* @return {boolean} True if editable.
*/
isEditable() {
return this.editable_ &&
!(this.workspace && this.workspace.options.readOnly);
}
/**
* Set whether this comment is editable or not.
* @param {boolean} editable True if editable.
*/
setEditable(editable) {
this.editable_ = editable;
}
/**
* Returns this comment's text.
* @return {string} Comment text.
* @package
*/
getContent() {
return this.content_;
}
/**
* Set this comment's content.
* @param {string} content Comment content.
* @package
*/
setContent(content) {
if (this.content_ !== content) {
eventUtils.fire(new (eventUtils.get(eventUtils.COMMENT_CHANGE))(
this, this.content_, content));
this.content_ = content;
}
}
/**
* Encode a comment subtree as XML with XY coordinates.
* @param {boolean=} opt_noId True if the encoder should skip the comment ID.
* @return {!Element} Tree of XML elements.
* @package
*/
toXmlWithXY(opt_noId) {
const element = this.toXml(opt_noId);
element.setAttribute('x', Math.round(this.xy_.x));
element.setAttribute('y', Math.round(this.xy_.y));
element.setAttribute('h', this.height_);
element.setAttribute('w', this.width_);
return element;
}
/**
* Encode a comment subtree as XML, but don't serialize the XY coordinates.
* This method avoids some expensive metrics-related calls that are made in
* toXmlWithXY().
* @param {boolean=} opt_noId True if the encoder should skip the comment ID.
* @return {!Element} Tree of XML elements.
* @package
*/
toXml(opt_noId) {
const commentElement = xml.createElement('comment');
if (!opt_noId) {
commentElement.id = this.id;
}
commentElement.textContent = this.getContent();
return commentElement;
}
/**
* Fire a create event for the given workspace comment, if comments are
* enabled.
* @param {!WorkspaceComment} comment The comment that was just created.
* @package
*/
static fireCreateEvent(comment) {
if (eventUtils.isEnabled()) {
const existingGroup = eventUtils.getGroup();
if (!existingGroup) {
eventUtils.setGroup(false);
eventUtils.setGroup(true);
}
try {
eventUtils.fire(
new (eventUtils.get(eventUtils.COMMENT_CREATE))(comment));
} finally {
if (!existingGroup) {
eventUtils.setGroup(false);
}
}
}
}
};
/**
* Decode an XML comment tag and create a comment on the workspace.
* @param {!Element} xmlComment XML comment element.
* @param {!Workspace} workspace The workspace.
* @return {!WorkspaceComment} The created workspace comment.
* @package
*/
WorkspaceComment.fromXml = function(xmlComment, workspace) {
const info = WorkspaceComment.parseAttributes(xmlComment);
/**
* Decode an XML comment tag and create a comment on the workspace.
* @param {!Element} xmlComment XML comment element.
* @param {!Workspace} workspace The workspace.
* @return {!WorkspaceComment} The created workspace comment.
* @package
*/
static fromXml(xmlComment, workspace) {
const info = WorkspaceComment.parseAttributes(xmlComment);
const comment =
new WorkspaceComment(workspace, info.content, info.h, info.w, info.id);
const comment =
new WorkspaceComment(workspace, info.content, info.h, info.w, info.id);
const commentX = parseInt(xmlComment.getAttribute('x'), 10);
const commentY = parseInt(xmlComment.getAttribute('y'), 10);
if (!isNaN(commentX) && !isNaN(commentY)) {
comment.moveBy(commentX, commentY);
const commentX = parseInt(xmlComment.getAttribute('x'), 10);
const commentY = parseInt(xmlComment.getAttribute('y'), 10);
if (!isNaN(commentX) && !isNaN(commentY)) {
comment.moveBy(commentX, commentY);
}
WorkspaceComment.fireCreateEvent(comment);
return comment;
}
WorkspaceComment.fireCreateEvent(comment);
return comment;
};
/**
* Decode an XML comment tag and return the results in an object.
* @param {!Element} xml XML comment element.
* @return {{w: number, h: number, x: number, y: number, content: string}} An
* object containing the id, size, position, and comment string.
* @package
*/
static parseAttributes(xml) {
const xmlH = xml.getAttribute('h');
const xmlW = xml.getAttribute('w');
/**
* Decode an XML comment tag and return the results in an object.
* @param {!Element} xml XML comment element.
* @return {{w: number, h: number, x: number, y: number, content: string}} An
* object containing the id, size, position, and comment string.
* @package
*/
WorkspaceComment.parseAttributes = function(xml) {
const xmlH = xml.getAttribute('h');
const xmlW = xml.getAttribute('w');
return {
// @type {string}
id: xml.getAttribute('id'),
// The height of the comment in workspace units, or 100 if not specified.
// @type {number}
h: xmlH ? parseInt(xmlH, 10) : 100,
// The width of the comment in workspace units, or 100 if not specified.
// @type {number}
w: xmlW ? parseInt(xmlW, 10) : 100,
// The x position of the comment in workspace coordinates, or NaN if not
// specified in the XML.
// @type {number}
x: parseInt(xml.getAttribute('x'), 10),
// The y position of the comment in workspace coordinates, or NaN if not
// specified in the XML.
// @type {number}
y: parseInt(xml.getAttribute('y'), 10),
// @type {string}
content: xml.textContent,
};
};
return {
// @type {string}
id: xml.getAttribute('id'),
// The height of the comment in workspace units, or 100 if not specified.
// @type {number}
h: xmlH ? parseInt(xmlH, 10) : 100,
// The width of the comment in workspace units, or 100 if not specified.
// @type {number}
w: xmlW ? parseInt(xmlW, 10) : 100,
// The x position of the comment in workspace coordinates, or NaN if not
// specified in the XML.
// @type {number}
x: parseInt(xml.getAttribute('x'), 10),
// The y position of the comment in workspace coordinates, or NaN if not
// specified in the XML.
// @type {number}
y: parseInt(xml.getAttribute('y'), 10),
// @type {string}
content: xml.textContent,
};
}
}
exports.WorkspaceComment = WorkspaceComment;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -479,7 +479,7 @@ const domToWorkspace = function(xml, workspace) {
'Missing require for Blockly.WorkspaceCommentSvg, ' +
'ignoring workspace comment.');
} else {
WorkspaceCommentSvg.fromXml(
WorkspaceCommentSvg.fromXmlRendered(
xmlChildElement,
/** @type {!WorkspaceSvg} */ (workspace), width);
}

View File

@@ -21,7 +21,7 @@ goog.addDependency('../../core/bubble.js', ['Blockly.Bubble'], ['Blockly.IBubble
goog.addDependency('../../core/bubble_dragger.js', ['Blockly.BubbleDragger'], ['Blockly.Bubble', 'Blockly.ComponentManager', 'Blockly.Events.CommentMove', 'Blockly.Events.utils', 'Blockly.constants', 'Blockly.utils.Coordinate', 'Blockly.utils.svgMath'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/bump_objects.js', ['Blockly.bumpObjects'], ['Blockly.Events.utils', 'Blockly.utils.math'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/clipboard.js', ['Blockly.clipboard'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/comment.js', ['Blockly.Comment'], ['Blockly.Bubble', 'Blockly.Css', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Events.utils', 'Blockly.Icon', 'Blockly.Warning', 'Blockly.browserEvents', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/comment.js', ['Blockly.Comment'], ['Blockly.Bubble', 'Blockly.Css', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Events.utils', 'Blockly.Icon', 'Blockly.Warning', 'Blockly.browserEvents', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/common.js', ['Blockly.common'], ['Blockly.blocks'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/component_manager.js', ['Blockly.ComponentManager'], ['Blockly.utils.array'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/connection.js', ['Blockly.Connection'], ['Blockly.ConnectionType', 'Blockly.Events.BlockMove', 'Blockly.Events.utils', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.Xml', 'Blockly.constants', 'Blockly.serialization.blocks'], {'lang': 'es6', 'module': 'goog'});
@@ -74,8 +74,8 @@ goog.addDependency('../../core/field_colour.js', ['Blockly.FieldColour'], ['Bloc
goog.addDependency('../../core/field_dropdown.js', ['Blockly.FieldDropdown'], ['Blockly.DropDownDiv', 'Blockly.Field', 'Blockly.Menu', 'Blockly.MenuItem', 'Blockly.fieldRegistry', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing', 'Blockly.utils.string', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_image.js', ['Blockly.FieldImage'], ['Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_label.js', ['Blockly.FieldLabel'], ['Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_label_serializable.js', ['Blockly.FieldLabelSerializable'], ['Blockly.FieldLabel', 'Blockly.fieldRegistry', 'Blockly.utils.object', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_multilineinput.js', ['Blockly.FieldMultilineInput'], ['Blockly.Css', 'Blockly.Field', 'Blockly.FieldTextInput', 'Blockly.WidgetDiv', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Svg', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_label_serializable.js', ['Blockly.FieldLabelSerializable'], ['Blockly.FieldLabel', 'Blockly.fieldRegistry', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_multilineinput.js', ['Blockly.FieldMultilineInput'], ['Blockly.Css', 'Blockly.Field', 'Blockly.FieldTextInput', 'Blockly.WidgetDiv', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Svg', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.parsing', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_number.js', ['Blockly.FieldNumber'], ['Blockly.FieldTextInput', 'Blockly.fieldRegistry', 'Blockly.utils.aria', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_registry.js', ['Blockly.fieldRegistry'], ['Blockly.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_textinput.js', ['Blockly.FieldTextInput'], ['Blockly.DropDownDiv', 'Blockly.Events.BlockChange', 'Blockly.Events.utils', 'Blockly.Field', 'Blockly.Msg', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.dialog', 'Blockly.fieldRegistry', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
@@ -133,13 +133,13 @@ goog.addDependency('../../core/menu.js', ['Blockly.Menu'], ['Blockly.browserEven
goog.addDependency('../../core/menuitem.js', ['Blockly.MenuItem'], ['Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.idGenerator'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/metrics_manager.js', ['Blockly.MetricsManager'], ['Blockly.IMetricsManager', 'Blockly.registry', 'Blockly.utils.Size', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/msg.js', ['Blockly.Msg'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/mutator.js', ['Blockly.Mutator'], ['Blockly.Bubble', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Events.utils', 'Blockly.Icon', 'Blockly.Options', 'Blockly.WorkspaceSvg', 'Blockly.internalConstants', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/mutator.js', ['Blockly.Mutator'], ['Blockly.Bubble', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Events.utils', 'Blockly.Icon', 'Blockly.Options', 'Blockly.WorkspaceSvg', 'Blockly.internalConstants', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.toolbox', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/names.js', ['Blockly.Names'], ['Blockly.Msg', 'Blockly.Variables'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/options.js', ['Blockly.Options'], ['Blockly.Theme', 'Blockly.Themes.Classic', 'Blockly.registry', 'Blockly.utils.idGenerator', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/positionable_helpers.js', ['Blockly.uiPosition'], ['Blockly.Scrollbar', 'Blockly.utils.Rect', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/procedures.js', ['Blockly.Procedures'], ['Blockly.Events.BlockChange', 'Blockly.Events.utils', 'Blockly.Msg', 'Blockly.Names', 'Blockly.Variables', 'Blockly.Workspace', 'Blockly.Xml', 'Blockly.blocks', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/registry.js', ['Blockly.registry'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/rendered_connection.js', ['Blockly.RenderedConnection'], ['Blockly.Connection', 'Blockly.ConnectionType', 'Blockly.Events.utils', 'Blockly.common', 'Blockly.internalConstants', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.svgMath', 'Blockly.utils.svgPaths'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/rendered_connection.js', ['Blockly.RenderedConnection'], ['Blockly.Connection', 'Blockly.ConnectionType', 'Blockly.Events.utils', 'Blockly.common', 'Blockly.internalConstants', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.svgMath', 'Blockly.utils.svgPaths'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/common/block_rendering.js', ['Blockly.blockRendering'], ['Blockly.blockRendering.BottomRow', 'Blockly.blockRendering.Connection', 'Blockly.blockRendering.ConstantProvider', 'Blockly.blockRendering.Debug', 'Blockly.blockRendering.Drawer', 'Blockly.blockRendering.ExternalValueInput', 'Blockly.blockRendering.Field', 'Blockly.blockRendering.Hat', 'Blockly.blockRendering.IPathObject', 'Blockly.blockRendering.Icon', 'Blockly.blockRendering.InRowSpacer', 'Blockly.blockRendering.InlineInput', 'Blockly.blockRendering.InputConnection', 'Blockly.blockRendering.InputRow', 'Blockly.blockRendering.JaggedEdge', 'Blockly.blockRendering.MarkerSvg', 'Blockly.blockRendering.Measurable', 'Blockly.blockRendering.NextConnection', 'Blockly.blockRendering.OutputConnection', 'Blockly.blockRendering.PathObject', 'Blockly.blockRendering.PreviousConnection', 'Blockly.blockRendering.RenderInfo', 'Blockly.blockRendering.Renderer', 'Blockly.blockRendering.RoundCorner', 'Blockly.blockRendering.Row', 'Blockly.blockRendering.SpacerRow', 'Blockly.blockRendering.SquareCorner', 'Blockly.blockRendering.StatementInput', 'Blockly.blockRendering.TopRow', 'Blockly.blockRendering.Types', 'Blockly.blockRendering.debug', 'Blockly.registry', 'Blockly.utils.deprecation'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/common/constants.js', ['Blockly.blockRendering.ConstantProvider'], ['Blockly.ConnectionType', 'Blockly.utils.Svg', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing', 'Blockly.utils.svgPaths', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/common/debug.js', ['Blockly.blockRendering.debug'], [], {'lang': 'es6', 'module': 'goog'});
@@ -253,15 +253,15 @@ goog.addDependency('../../core/variable_map.js', ['Blockly.VariableMap'], ['Bloc
goog.addDependency('../../core/variable_model.js', ['Blockly.VariableModel'], ['Blockly.Events.VarCreate', 'Blockly.Events.utils', 'Blockly.utils.idGenerator'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/variables.js', ['Blockly.Variables'], ['Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Xml', 'Blockly.blocks', 'Blockly.dialog', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/variables_dynamic.js', ['Blockly.VariablesDynamic'], ['Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.blocks', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/warning.js', ['Blockly.Warning'], ['Blockly.Bubble', 'Blockly.Events.BubbleOpen', 'Blockly.Events.utils', 'Blockly.Icon', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/warning.js', ['Blockly.Warning'], ['Blockly.Bubble', 'Blockly.Events.BubbleOpen', 'Blockly.Events.utils', 'Blockly.Icon', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/widgetdiv.js', ['Blockly.WidgetDiv'], ['Blockly.common', 'Blockly.utils.deprecation', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/workspace.js', ['Blockly.Workspace'], ['Blockly.ConnectionChecker', 'Blockly.Events.utils', 'Blockly.IASTNodeLocation', 'Blockly.Options', 'Blockly.VariableMap', 'Blockly.registry', 'Blockly.utils.array', 'Blockly.utils.idGenerator', 'Blockly.utils.math'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/workspace_audio.js', ['Blockly.WorkspaceAudio'], ['Blockly.utils.global', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/workspace_comment.js', ['Blockly.WorkspaceComment'], ['Blockly.Events.CommentChange', 'Blockly.Events.CommentCreate', 'Blockly.Events.CommentDelete', 'Blockly.Events.CommentMove', 'Blockly.Events.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.idGenerator', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/workspace_comment_svg.js', ['Blockly.WorkspaceCommentSvg'], ['Blockly.ContextMenu', 'Blockly.Css', 'Blockly.Events.CommentCreate', 'Blockly.Events.CommentDelete', 'Blockly.Events.CommentMove', 'Blockly.Events.Selected', 'Blockly.Events.utils', 'Blockly.IBoundedElement', 'Blockly.IBubble', 'Blockly.ICopyable', 'Blockly.Touch', 'Blockly.WorkspaceComment', 'Blockly.browserEvents', 'Blockly.common', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.svgMath'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/workspace_comment_svg.js', ['Blockly.WorkspaceCommentSvg'], ['Blockly.ContextMenu', 'Blockly.Css', 'Blockly.Events.CommentCreate', 'Blockly.Events.CommentDelete', 'Blockly.Events.CommentMove', 'Blockly.Events.Selected', 'Blockly.Events.utils', 'Blockly.IBoundedElement', 'Blockly.IBubble', 'Blockly.ICopyable', 'Blockly.Touch', 'Blockly.WorkspaceComment', 'Blockly.browserEvents', 'Blockly.common', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.svgMath'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/workspace_drag_surface_svg.js', ['Blockly.WorkspaceDragSurfaceSvg'], ['Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.svgMath'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/workspace_dragger.js', ['Blockly.WorkspaceDragger'], ['Blockly.common', 'Blockly.utils.Coordinate'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/workspace_svg.js', ['Blockly.WorkspaceSvg'], ['Blockly.BlockSvg', 'Blockly.ComponentManager', 'Blockly.ConnectionDB', 'Blockly.ContextMenu', 'Blockly.ContextMenuRegistry', 'Blockly.DropDownDiv', 'Blockly.Events.BlockCreate', 'Blockly.Events.ThemeChange', 'Blockly.Events.ViewportChange', 'Blockly.Events.utils', 'Blockly.Gesture', 'Blockly.Grid', 'Blockly.IASTNodeLocationSvg', 'Blockly.MarkerManager', 'Blockly.MetricsManager', 'Blockly.Msg', 'Blockly.Options', 'Blockly.ThemeManager', 'Blockly.Themes.Classic', 'Blockly.Tooltip', 'Blockly.TouchGesture', 'Blockly.WidgetDiv', 'Blockly.Workspace', 'Blockly.WorkspaceAudio', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.common', 'Blockly.internalConstants', 'Blockly.registry', 'Blockly.serialization.blocks', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.array', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.svgMath', 'Blockly.utils.toolbox', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/workspace_svg.js', ['Blockly.WorkspaceSvg'], ['Blockly.BlockSvg', 'Blockly.ComponentManager', 'Blockly.ConnectionDB', 'Blockly.ContextMenu', 'Blockly.ContextMenuRegistry', 'Blockly.DropDownDiv', 'Blockly.Events.BlockCreate', 'Blockly.Events.ThemeChange', 'Blockly.Events.ViewportChange', 'Blockly.Events.utils', 'Blockly.Gesture', 'Blockly.Grid', 'Blockly.IASTNodeLocationSvg', 'Blockly.MarkerManager', 'Blockly.MetricsManager', 'Blockly.Msg', 'Blockly.Options', 'Blockly.ThemeManager', 'Blockly.Themes.Classic', 'Blockly.Tooltip', 'Blockly.TouchGesture', 'Blockly.WidgetDiv', 'Blockly.Workspace', 'Blockly.WorkspaceAudio', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.common', 'Blockly.internalConstants', 'Blockly.registry', 'Blockly.serialization.blocks', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.array', 'Blockly.utils.dom', 'Blockly.utils.svgMath', 'Blockly.utils.toolbox', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/xml.js', ['Blockly.Xml'], ['Blockly.Events.utils', 'Blockly.inputTypes', 'Blockly.utils.Size', 'Blockly.utils.dom', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/zoom_controls.js', ['Blockly.ZoomControls'], ['Blockly.ComponentManager', 'Blockly.Css', 'Blockly.Events.Click', 'Blockly.Events.utils', 'Blockly.IPositionable', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.internalConstants', 'Blockly.uiPosition', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../generators/dart.js', ['Blockly.Dart'], ['Blockly.Generator', 'Blockly.Names', 'Blockly.Variables', 'Blockly.inputTypes', 'Blockly.utils.string'], {'lang': 'es6', 'module': 'goog'});