mirror of
https://github.com/google/blockly.git
synced 2026-03-18 19:20:11 +01:00
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:
@@ -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()) {
|
||||
|
||||
736
core/comment.js
736
core/comment.js
@@ -19,7 +19,6 @@ const Css = goog.require('Blockly.Css');
|
||||
const browserEvents = goog.require('Blockly.browserEvents');
|
||||
const dom = goog.require('Blockly.utils.dom');
|
||||
const eventUtils = goog.require('Blockly.Events.utils');
|
||||
const object = goog.require('Blockly.utils.object');
|
||||
const userAgent = goog.require('Blockly.utils.userAgent');
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
|
||||
@@ -44,391 +43,392 @@ goog.require('Blockly.Warning');
|
||||
|
||||
/**
|
||||
* Class for a comment.
|
||||
* @param {!Block} block The block associated with this comment.
|
||||
* @extends {Icon}
|
||||
* @constructor
|
||||
* @alias Blockly.Comment
|
||||
*/
|
||||
const Comment = function(block) {
|
||||
Comment.superClass_.constructor.call(this, block);
|
||||
|
||||
class Comment extends Icon {
|
||||
/**
|
||||
* The model for this comment.
|
||||
* @type {!Block.CommentModel}
|
||||
* @private
|
||||
* @param {!BlockSvg} block The block associated with this comment.
|
||||
* @alias Blockly.Comment
|
||||
*/
|
||||
this.model_ = block.commentModel;
|
||||
// If someone creates the comment directly instead of calling
|
||||
// block.setCommentText we want to make sure the text is non-null;
|
||||
this.model_.text = this.model_.text || '';
|
||||
constructor(block) {
|
||||
super(block);
|
||||
|
||||
/**
|
||||
* The model's text value at the start of an edit.
|
||||
* Used to tell if an event should be fired at the end of an edit.
|
||||
* @type {?string}
|
||||
* @private
|
||||
*/
|
||||
this.cachedText_ = '';
|
||||
/**
|
||||
* The model for this comment.
|
||||
* @type {!Block.CommentModel}
|
||||
* @private
|
||||
*/
|
||||
this.model_ = block.commentModel;
|
||||
// If someone creates the comment directly instead of calling
|
||||
// block.setCommentText we want to make sure the text is non-null;
|
||||
this.model_.text = this.model_.text || '';
|
||||
|
||||
/**
|
||||
* Mouse up event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.onMouseUpWrapper_ = null;
|
||||
/**
|
||||
* The model's text value at the start of an edit.
|
||||
* Used to tell if an event should be fired at the end of an edit.
|
||||
* @type {?string}
|
||||
* @private
|
||||
*/
|
||||
this.cachedText_ = '';
|
||||
|
||||
/**
|
||||
* Wheel event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.onWheelWrapper_ = null;
|
||||
|
||||
/**
|
||||
* Change event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.onChangeWrapper_ = null;
|
||||
|
||||
/**
|
||||
* Input event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.onInputWrapper_ = null;
|
||||
|
||||
/**
|
||||
* The SVG element that contains the text edit area, or null if not created.
|
||||
* @type {?SVGForeignObjectElement}
|
||||
* @private
|
||||
*/
|
||||
this.foreignObject_ = null;
|
||||
|
||||
/**
|
||||
* The editable text area, or null if not created.
|
||||
* @type {?Element}
|
||||
* @private
|
||||
*/
|
||||
this.textarea_ = null;
|
||||
|
||||
/**
|
||||
* The top-level node of the comment text, or null if not created.
|
||||
* @type {?SVGTextElement}
|
||||
* @private
|
||||
*/
|
||||
this.paragraphElement_ = null;
|
||||
|
||||
|
||||
this.createIcon();
|
||||
};
|
||||
object.inherits(Comment, Icon);
|
||||
|
||||
/**
|
||||
* Draw the comment icon.
|
||||
* @param {!Element} group The icon group.
|
||||
* @protected
|
||||
*/
|
||||
Comment.prototype.drawIcon_ = function(group) {
|
||||
// Circle.
|
||||
dom.createSvgElement(
|
||||
Svg.CIRCLE, {'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'},
|
||||
group);
|
||||
// Can't use a real '?' text character since different browsers and operating
|
||||
// systems render it differently.
|
||||
// Body of question mark.
|
||||
dom.createSvgElement(
|
||||
Svg.PATH, {
|
||||
'class': 'blocklyIconSymbol',
|
||||
'd': 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' +
|
||||
'0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' +
|
||||
'-1.201,0.998 -1.201,1.528 -1.204,2.19z',
|
||||
},
|
||||
group);
|
||||
// Dot of question mark.
|
||||
dom.createSvgElement(
|
||||
Svg.RECT, {
|
||||
'class': 'blocklyIconSymbol',
|
||||
'x': '6.8',
|
||||
'y': '10.78',
|
||||
'height': '2',
|
||||
'width': '2',
|
||||
},
|
||||
group);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the editor for the comment's bubble.
|
||||
* @return {!SVGElement} The top-level node of the editor.
|
||||
* @private
|
||||
*/
|
||||
Comment.prototype.createEditor_ = function() {
|
||||
/* Create the editor. Here's the markup that will be generated in
|
||||
* editable mode:
|
||||
<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.
|
||||
|
||||
1188
core/connection.js
1188
core/connection.js
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ const WidgetDiv = goog.require('Blockly.WidgetDiv');
|
||||
const aria = goog.require('Blockly.utils.aria');
|
||||
const dom = goog.require('Blockly.utils.dom');
|
||||
const fieldRegistry = goog.require('Blockly.fieldRegistry');
|
||||
const object = goog.require('Blockly.utils.object');
|
||||
const parsing = goog.require('Blockly.utils.parsing');
|
||||
const userAgent = goog.require('Blockly.utils.userAgent');
|
||||
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
|
||||
@@ -31,401 +30,419 @@ const {Svg} = goog.require('Blockly.utils.Svg');
|
||||
|
||||
/**
|
||||
* Class for an editable text area field.
|
||||
* @param {string=} opt_value The initial content of the field. Should cast to a
|
||||
* string. Defaults to an empty string if null or undefined.
|
||||
* @param {Function=} opt_validator An optional function that is called
|
||||
* to validate any constraints on what the user entered. Takes the new
|
||||
* text as an argument and returns either the accepted text, a replacement
|
||||
* text, or null to abort the change.
|
||||
* @param {Object=} opt_config A map of options used to configure the field.
|
||||
* See the [field creation documentation]{@link
|
||||
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
|
||||
* for a list of properties this parameter supports.
|
||||
* @extends {FieldTextInput}
|
||||
* @constructor
|
||||
* @alias Blockly.FieldMultilineInput
|
||||
*/
|
||||
const FieldMultilineInput = function(opt_value, opt_validator, opt_config) {
|
||||
FieldMultilineInput.superClass_.constructor.call(
|
||||
this, opt_value, opt_validator, opt_config);
|
||||
|
||||
class FieldMultilineInput extends FieldTextInput {
|
||||
/**
|
||||
* The SVG group element that will contain a text element for each text row
|
||||
* when initialized.
|
||||
* @type {SVGGElement}
|
||||
* @param {string=} opt_value The initial content of the field. Should cast to
|
||||
* a
|
||||
* string. Defaults to an empty string if null or undefined.
|
||||
* @param {Function=} opt_validator An optional function that is called
|
||||
* to validate any constraints on what the user entered. Takes the new
|
||||
* text as an argument and returns either the accepted text, a replacement
|
||||
* text, or null to abort the change.
|
||||
* @param {Object=} opt_config A map of options used to configure the field.
|
||||
* See the [field creation documentation]{@link
|
||||
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
|
||||
* for a list of properties this parameter supports.
|
||||
* @alias Blockly.FieldMultilineInput
|
||||
*/
|
||||
this.textGroup_ = null;
|
||||
constructor(opt_value, opt_validator, opt_config) {
|
||||
const stringValue = opt_value == undefined ? '' : String(opt_value);
|
||||
super(stringValue, opt_validator, opt_config);
|
||||
|
||||
/**
|
||||
* The SVG group element that will contain a text element for each text row
|
||||
* when initialized.
|
||||
* @type {SVGGElement}
|
||||
*/
|
||||
this.textGroup_ = null;
|
||||
|
||||
/**
|
||||
* Defines the maximum number of lines of field.
|
||||
* If exceeded, scrolling functionality is enabled.
|
||||
* @type {number}
|
||||
* @protected
|
||||
*/
|
||||
this.maxLines_ = Infinity;
|
||||
|
||||
/**
|
||||
* Whether Y overflow is currently occurring.
|
||||
* @type {boolean}
|
||||
* @protected
|
||||
*/
|
||||
this.isOverflowedY_ = false;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.isBeingEdited_ = false;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.isTextValid_ = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the maximum number of lines of field.
|
||||
* If exceeded, scrolling functionality is enabled.
|
||||
* @type {number}
|
||||
* @override
|
||||
*/
|
||||
configure_(config) {
|
||||
super.configure_(config);
|
||||
config.maxLines && this.setMaxLines(config.maxLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this field's value to XML. Should only be called by Blockly.Xml.
|
||||
* @param {!Element} fieldElement The element to populate with info about the
|
||||
* field's state.
|
||||
* @return {!Element} The element containing info about the field's state.
|
||||
* @package
|
||||
*/
|
||||
toXml(fieldElement) {
|
||||
// Replace '\n' characters with HTML-escaped equivalent '
'. This is
|
||||
// needed so the plain-text representation of the XML produced by
|
||||
// `Blockly.Xml.domToText` will appear on a single line (this is a
|
||||
// limitation of the plain-text format).
|
||||
fieldElement.textContent = this.getValue().replace(/\n/g, ' ');
|
||||
return fieldElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field's value based on the given XML element. Should only be
|
||||
* called by Blockly.Xml.
|
||||
* @param {!Element} fieldElement The element containing info about the
|
||||
* field's state.
|
||||
* @package
|
||||
*/
|
||||
fromXml(fieldElement) {
|
||||
this.setValue(fieldElement.textContent.replace(/ /g, '\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this field's value.
|
||||
* @return {*} The state of this field.
|
||||
* @package
|
||||
*/
|
||||
saveState() {
|
||||
const legacyState = this.saveLegacyState(FieldMultilineInput);
|
||||
if (legacyState !== null) {
|
||||
return legacyState;
|
||||
}
|
||||
return this.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field's value based on the given state.
|
||||
* @param {*} state The state of the variable to assign to this variable
|
||||
* field.
|
||||
* @override
|
||||
* @package
|
||||
*/
|
||||
loadState(state) {
|
||||
if (this.loadLegacyState(Field, state)) {
|
||||
return;
|
||||
}
|
||||
this.setValue(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the block UI for this field.
|
||||
* @package
|
||||
*/
|
||||
initView() {
|
||||
this.createBorderRect_();
|
||||
this.textGroup_ = dom.createSvgElement(
|
||||
Svg.G, {
|
||||
'class': 'blocklyEditableText',
|
||||
},
|
||||
this.fieldGroup_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text from this field as displayed on screen. May differ from
|
||||
* getText due to ellipsis, and other formatting.
|
||||
* @return {string} Currently displayed text.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
getDisplayText_() {
|
||||
let textLines = this.getText();
|
||||
if (!textLines) {
|
||||
// Prevent the field from disappearing if empty.
|
||||
return Field.NBSP;
|
||||
}
|
||||
const lines = textLines.split('\n');
|
||||
textLines = '';
|
||||
const displayLinesNumber =
|
||||
this.isOverflowedY_ ? this.maxLines_ : lines.length;
|
||||
for (let i = 0; i < displayLinesNumber; i++) {
|
||||
let text = lines[i];
|
||||
if (text.length > this.maxDisplayLength) {
|
||||
// Truncate displayed string and add an ellipsis ('...').
|
||||
text = text.substring(0, this.maxDisplayLength - 4) + '...';
|
||||
} else if (this.isOverflowedY_ && i === displayLinesNumber - 1) {
|
||||
text = text.substring(0, text.length - 3) + '...';
|
||||
}
|
||||
// Replace whitespace with non-breaking spaces so the text doesn't
|
||||
// collapse.
|
||||
text = text.replace(/\s/g, Field.NBSP);
|
||||
|
||||
textLines += text;
|
||||
if (i !== displayLinesNumber - 1) {
|
||||
textLines += '\n';
|
||||
}
|
||||
}
|
||||
if (this.sourceBlock_.RTL) {
|
||||
// The SVG is LTR, force value to be RTL.
|
||||
textLines += '\u200F';
|
||||
}
|
||||
return textLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by setValue if the text input is valid. Updates the value of the
|
||||
* field, and updates the text of the field if it is not currently being
|
||||
* edited (i.e. handled by the htmlInput_). Is being redefined here to update
|
||||
* overflow state of the field.
|
||||
* @param {*} newValue The value to be saved. The default validator guarantees
|
||||
* that this is a string.
|
||||
* @protected
|
||||
*/
|
||||
this.maxLines_ = Infinity;
|
||||
doValueUpdate_(newValue) {
|
||||
super.doValueUpdate_(newValue);
|
||||
this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether Y overflow is currently occurring.
|
||||
* @type {boolean}
|
||||
* Updates the text of the textElement.
|
||||
* @protected
|
||||
*/
|
||||
this.isOverflowedY_ = false;
|
||||
};
|
||||
object.inherits(FieldMultilineInput, FieldTextInput);
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
FieldMultilineInput.prototype.configure_ = function(config) {
|
||||
FieldMultilineInput.superClass_.configure_.call(this, config);
|
||||
config.maxLines && this.setMaxLines(config.maxLines);
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a FieldMultilineInput from a JSON arg object,
|
||||
* dereferencing any string table references.
|
||||
* @param {!Object} options A JSON object with options (text, and spellcheck).
|
||||
* @return {!FieldMultilineInput} The new field instance.
|
||||
* @package
|
||||
* @nocollapse
|
||||
*/
|
||||
FieldMultilineInput.fromJson = function(options) {
|
||||
const text = parsing.replaceMessageReferences(options['text']);
|
||||
// `this` might be a subclass of FieldMultilineInput if that class doesn't
|
||||
// override the static fromJson method.
|
||||
return new this(text, undefined, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializes this field's value to XML. Should only be called by Blockly.Xml.
|
||||
* @param {!Element} fieldElement The element to populate with info about the
|
||||
* field's state.
|
||||
* @return {!Element} The element containing info about the field's state.
|
||||
* @package
|
||||
*/
|
||||
FieldMultilineInput.prototype.toXml = function(fieldElement) {
|
||||
// Replace '\n' characters with HTML-escaped equivalent '
'. This is
|
||||
// needed so the plain-text representation of the XML produced by
|
||||
// `Blockly.Xml.domToText` will appear on a single line (this is a limitation
|
||||
// of the plain-text format).
|
||||
fieldElement.textContent = this.getValue().replace(/\n/g, ' ');
|
||||
return fieldElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the field's value based on the given XML element. Should only be
|
||||
* called by Blockly.Xml.
|
||||
* @param {!Element} fieldElement The element containing info about the
|
||||
* field's state.
|
||||
* @package
|
||||
*/
|
||||
FieldMultilineInput.prototype.fromXml = function(fieldElement) {
|
||||
this.setValue(fieldElement.textContent.replace(/ /g, '\n'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves this field's value.
|
||||
* @return {*} The state of this field.
|
||||
* @package
|
||||
*/
|
||||
FieldMultilineInput.prototype.saveState = function() {
|
||||
const legacyState = this.saveLegacyState(FieldMultilineInput);
|
||||
if (legacyState !== null) {
|
||||
return legacyState;
|
||||
}
|
||||
return this.getValue();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the field's value based on the given state.
|
||||
* @param {*} state The state of the variable to assign to this variable field.
|
||||
* @override
|
||||
* @package
|
||||
*/
|
||||
FieldMultilineInput.prototype.loadState = function(state) {
|
||||
if (this.loadLegacyState(Field, state)) {
|
||||
return;
|
||||
}
|
||||
this.setValue(state);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the block UI for this field.
|
||||
* @package
|
||||
*/
|
||||
FieldMultilineInput.prototype.initView = function() {
|
||||
this.createBorderRect_();
|
||||
this.textGroup_ = dom.createSvgElement(
|
||||
Svg.G, {
|
||||
'class': 'blocklyEditableText',
|
||||
},
|
||||
this.fieldGroup_);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the text from this field as displayed on screen. May differ from getText
|
||||
* due to ellipsis, and other formatting.
|
||||
* @return {string} Currently displayed text.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
FieldMultilineInput.prototype.getDisplayText_ = function() {
|
||||
let textLines = this.getText();
|
||||
if (!textLines) {
|
||||
// Prevent the field from disappearing if empty.
|
||||
return Field.NBSP;
|
||||
}
|
||||
const lines = textLines.split('\n');
|
||||
textLines = '';
|
||||
const displayLinesNumber =
|
||||
this.isOverflowedY_ ? this.maxLines_ : lines.length;
|
||||
for (let i = 0; i < displayLinesNumber; i++) {
|
||||
let text = lines[i];
|
||||
if (text.length > this.maxDisplayLength) {
|
||||
// Truncate displayed string and add an ellipsis ('...').
|
||||
text = text.substring(0, this.maxDisplayLength - 4) + '...';
|
||||
} else if (this.isOverflowedY_ && i === displayLinesNumber - 1) {
|
||||
text = text.substring(0, text.length - 3) + '...';
|
||||
render_() {
|
||||
// Remove all text group children.
|
||||
let currentChild;
|
||||
while ((currentChild = this.textGroup_.firstChild)) {
|
||||
this.textGroup_.removeChild(currentChild);
|
||||
}
|
||||
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
|
||||
text = text.replace(/\s/g, Field.NBSP);
|
||||
|
||||
textLines += text;
|
||||
if (i !== displayLinesNumber - 1) {
|
||||
textLines += '\n';
|
||||
// Add in text elements into the group.
|
||||
const lines = this.getDisplayText_().split('\n');
|
||||
let y = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
|
||||
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
|
||||
const span = dom.createSvgElement(
|
||||
Svg.TEXT, {
|
||||
'class': 'blocklyText blocklyMultilineText',
|
||||
'x': this.getConstants().FIELD_BORDER_RECT_X_PADDING,
|
||||
'y': y + this.getConstants().FIELD_BORDER_RECT_Y_PADDING,
|
||||
'dy': this.getConstants().FIELD_TEXT_BASELINE,
|
||||
},
|
||||
this.textGroup_);
|
||||
span.appendChild(document.createTextNode(lines[i]));
|
||||
y += lineHeight;
|
||||
}
|
||||
|
||||
if (this.isBeingEdited_) {
|
||||
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
|
||||
if (this.isOverflowedY_) {
|
||||
dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
|
||||
} else {
|
||||
dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSize_();
|
||||
|
||||
if (this.isBeingEdited_) {
|
||||
if (this.sourceBlock_.RTL) {
|
||||
// in RTL, we need to let the browser reflow before resizing
|
||||
// in order to get the correct bounding box of the borderRect
|
||||
// avoiding issue #2777.
|
||||
setTimeout(this.resizeEditor_.bind(this), 0);
|
||||
} else {
|
||||
this.resizeEditor_();
|
||||
}
|
||||
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
|
||||
if (!this.isTextValid_) {
|
||||
dom.addClass(htmlInput, 'blocklyInvalidInput');
|
||||
aria.setState(htmlInput, aria.State.INVALID, true);
|
||||
} else {
|
||||
dom.removeClass(htmlInput, 'blocklyInvalidInput');
|
||||
aria.setState(htmlInput, aria.State.INVALID, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.sourceBlock_.RTL) {
|
||||
// The SVG is LTR, force value to be RTL.
|
||||
textLines += '\u200F';
|
||||
}
|
||||
return textLines;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by setValue if the text input is valid. Updates the value of the
|
||||
* field, and updates the text of the field if it is not currently being
|
||||
* edited (i.e. handled by the htmlInput_). Is being redefined here to update
|
||||
* overflow state of the field.
|
||||
* @param {*} newValue The value to be saved. The default validator guarantees
|
||||
* that this is a string.
|
||||
* @protected
|
||||
*/
|
||||
FieldMultilineInput.prototype.doValueUpdate_ = function(newValue) {
|
||||
FieldMultilineInput.superClass_.doValueUpdate_.call(this, newValue);
|
||||
this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
|
||||
};
|
||||
/**
|
||||
* Updates the size of the field based on the text.
|
||||
* @protected
|
||||
*/
|
||||
updateSize_() {
|
||||
const nodes = this.textGroup_.childNodes;
|
||||
let totalWidth = 0;
|
||||
let totalHeight = 0;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const tspan = /** @type {!Element} */ (nodes[i]);
|
||||
const textWidth = dom.getTextWidth(tspan);
|
||||
if (textWidth > totalWidth) {
|
||||
totalWidth = textWidth;
|
||||
}
|
||||
totalHeight += this.getConstants().FIELD_TEXT_HEIGHT +
|
||||
(i > 0 ? this.getConstants().FIELD_BORDER_RECT_Y_PADDING : 0);
|
||||
}
|
||||
if (this.isBeingEdited_) {
|
||||
// The default width is based on the longest line in the display text,
|
||||
// but when it's being edited, width should be calculated based on the
|
||||
// absolute longest line, even if it would be truncated after editing.
|
||||
// Otherwise we would get wrong editor width when there are more
|
||||
// lines than this.maxLines_.
|
||||
const actualEditorLines = this.value_.split('\n');
|
||||
const dummyTextElement = dom.createSvgElement(
|
||||
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
|
||||
const fontSize = this.getConstants().FIELD_TEXT_FONTSIZE;
|
||||
const fontWeight = this.getConstants().FIELD_TEXT_FONTWEIGHT;
|
||||
const fontFamily = this.getConstants().FIELD_TEXT_FONTFAMILY;
|
||||
|
||||
/**
|
||||
* Updates the text of the textElement.
|
||||
* @protected
|
||||
*/
|
||||
FieldMultilineInput.prototype.render_ = function() {
|
||||
// Remove all text group children.
|
||||
let currentChild;
|
||||
while ((currentChild = this.textGroup_.firstChild)) {
|
||||
this.textGroup_.removeChild(currentChild);
|
||||
for (let i = 0; i < actualEditorLines.length; i++) {
|
||||
if (actualEditorLines[i].length > this.maxDisplayLength) {
|
||||
actualEditorLines[i] =
|
||||
actualEditorLines[i].substring(0, this.maxDisplayLength);
|
||||
}
|
||||
dummyTextElement.textContent = actualEditorLines[i];
|
||||
const lineWidth = dom.getFastTextWidth(
|
||||
dummyTextElement, fontSize, fontWeight, fontFamily);
|
||||
if (lineWidth > totalWidth) {
|
||||
totalWidth = lineWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const scrollbarWidth =
|
||||
this.htmlInput_.offsetWidth - this.htmlInput_.clientWidth;
|
||||
totalWidth += scrollbarWidth;
|
||||
}
|
||||
if (this.borderRect_) {
|
||||
totalHeight += this.getConstants().FIELD_BORDER_RECT_Y_PADDING * 2;
|
||||
totalWidth += this.getConstants().FIELD_BORDER_RECT_X_PADDING * 2;
|
||||
this.borderRect_.setAttribute('width', totalWidth);
|
||||
this.borderRect_.setAttribute('height', totalHeight);
|
||||
}
|
||||
this.size_.width = totalWidth;
|
||||
this.size_.height = totalHeight;
|
||||
|
||||
this.positionBorderRect_();
|
||||
}
|
||||
|
||||
// Add in text elements into the group.
|
||||
const lines = this.getDisplayText_().split('\n');
|
||||
let y = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
/**
|
||||
* Show the inline free-text editor on top of the text.
|
||||
* Overrides the default behaviour to force rerender in order to
|
||||
* correct block size, based on editor text.
|
||||
* @param {Event=} _opt_e Optional mouse event that triggered the field to
|
||||
* open, or undefined if triggered programmatically.
|
||||
* @param {boolean=} opt_quietInput True if editor should be created without
|
||||
* focus. Defaults to false.
|
||||
* @override
|
||||
*/
|
||||
showEditor_(_opt_e, opt_quietInput) {
|
||||
super.showEditor_(_opt_e, opt_quietInput);
|
||||
this.forceRerender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the text input editor widget.
|
||||
* @return {!HTMLTextAreaElement} The newly created text input editor.
|
||||
* @protected
|
||||
*/
|
||||
widgetCreate_() {
|
||||
const div = WidgetDiv.getDiv();
|
||||
const scale = this.workspace_.getScale();
|
||||
|
||||
const htmlInput =
|
||||
/** @type {HTMLTextAreaElement} */ (document.createElement('textarea'));
|
||||
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
|
||||
htmlInput.setAttribute('spellcheck', this.spellcheck_);
|
||||
const fontSize = (this.getConstants().FIELD_TEXT_FONTSIZE * scale) + 'pt';
|
||||
div.style.fontSize = fontSize;
|
||||
htmlInput.style.fontSize = fontSize;
|
||||
const borderRadius = (FieldTextInput.BORDERRADIUS * scale) + 'px';
|
||||
htmlInput.style.borderRadius = borderRadius;
|
||||
const paddingX = this.getConstants().FIELD_BORDER_RECT_X_PADDING * scale;
|
||||
const paddingY =
|
||||
this.getConstants().FIELD_BORDER_RECT_Y_PADDING * scale / 2;
|
||||
htmlInput.style.padding = paddingY + 'px ' + paddingX + 'px ' + paddingY +
|
||||
'px ' + paddingX + 'px';
|
||||
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
|
||||
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
|
||||
const span = dom.createSvgElement(
|
||||
Svg.TEXT, {
|
||||
'class': 'blocklyText blocklyMultilineText',
|
||||
'x': this.getConstants().FIELD_BORDER_RECT_X_PADDING,
|
||||
'y': y + this.getConstants().FIELD_BORDER_RECT_Y_PADDING,
|
||||
'dy': this.getConstants().FIELD_TEXT_BASELINE,
|
||||
},
|
||||
this.textGroup_);
|
||||
span.appendChild(document.createTextNode(lines[i]));
|
||||
y += lineHeight;
|
||||
}
|
||||
htmlInput.style.lineHeight = (lineHeight * scale) + 'px';
|
||||
|
||||
if (this.isBeingEdited_) {
|
||||
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
|
||||
if (this.isOverflowedY_) {
|
||||
dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
|
||||
} else {
|
||||
dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
|
||||
}
|
||||
}
|
||||
div.appendChild(htmlInput);
|
||||
|
||||
this.updateSize_();
|
||||
|
||||
if (this.isBeingEdited_) {
|
||||
if (this.sourceBlock_.RTL) {
|
||||
// in RTL, we need to let the browser reflow before resizing
|
||||
// in order to get the correct bounding box of the borderRect
|
||||
// avoiding issue #2777.
|
||||
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
|
||||
htmlInput.untypedDefaultValue_ = this.value_;
|
||||
htmlInput.oldValue_ = null;
|
||||
if (userAgent.GECKO) {
|
||||
// In FF, ensure the browser reflows before resizing to avoid issue #2777.
|
||||
setTimeout(this.resizeEditor_.bind(this), 0);
|
||||
} else {
|
||||
this.resizeEditor_();
|
||||
}
|
||||
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
|
||||
if (!this.isTextValid_) {
|
||||
dom.addClass(htmlInput, 'blocklyInvalidInput');
|
||||
aria.setState(htmlInput, aria.State.INVALID, true);
|
||||
} else {
|
||||
dom.removeClass(htmlInput, 'blocklyInvalidInput');
|
||||
aria.setState(htmlInput, aria.State.INVALID, false);
|
||||
|
||||
this.bindInputEvents_(htmlInput);
|
||||
|
||||
return htmlInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maxLines config for this field.
|
||||
* @param {number} maxLines Defines the maximum number of lines allowed,
|
||||
* before scrolling functionality is enabled.
|
||||
*/
|
||||
setMaxLines(maxLines) {
|
||||
if (typeof maxLines === 'number' && maxLines > 0 &&
|
||||
maxLines !== this.maxLines_) {
|
||||
this.maxLines_ = maxLines;
|
||||
this.forceRerender();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the size of the field based on the text.
|
||||
* @protected
|
||||
*/
|
||||
FieldMultilineInput.prototype.updateSize_ = function() {
|
||||
const nodes = this.textGroup_.childNodes;
|
||||
let totalWidth = 0;
|
||||
let totalHeight = 0;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const tspan = /** @type {!Element} */ (nodes[i]);
|
||||
const textWidth = dom.getTextWidth(tspan);
|
||||
if (textWidth > totalWidth) {
|
||||
totalWidth = textWidth;
|
||||
/**
|
||||
* Returns the maxLines config of this field.
|
||||
* @return {number} The maxLines config value.
|
||||
*/
|
||||
getMaxLines() {
|
||||
return this.maxLines_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key down to the editor. Override the text input definition of this
|
||||
* so as to not close the editor when enter is typed in.
|
||||
* @param {!Event} e Keyboard event.
|
||||
* @protected
|
||||
*/
|
||||
onHtmlInputKeyDown_(e) {
|
||||
if (e.keyCode !== KeyCodes.ENTER) {
|
||||
super.onHtmlInputKeyDown_(e);
|
||||
}
|
||||
totalHeight += this.getConstants().FIELD_TEXT_HEIGHT +
|
||||
(i > 0 ? this.getConstants().FIELD_BORDER_RECT_Y_PADDING : 0);
|
||||
}
|
||||
if (this.isBeingEdited_) {
|
||||
// The default width is based on the longest line in the display text,
|
||||
// but when it's being edited, width should be calculated based on the
|
||||
// absolute longest line, even if it would be truncated after editing.
|
||||
// Otherwise we would get wrong editor width when there are more
|
||||
// lines than this.maxLines_.
|
||||
const actualEditorLines = this.value_.split('\n');
|
||||
const dummyTextElement = dom.createSvgElement(
|
||||
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
|
||||
const fontSize = this.getConstants().FIELD_TEXT_FONTSIZE;
|
||||
const fontWeight = this.getConstants().FIELD_TEXT_FONTWEIGHT;
|
||||
const fontFamily = this.getConstants().FIELD_TEXT_FONTFAMILY;
|
||||
|
||||
for (let i = 0; i < actualEditorLines.length; i++) {
|
||||
if (actualEditorLines[i].length > this.maxDisplayLength) {
|
||||
actualEditorLines[i] =
|
||||
actualEditorLines[i].substring(0, this.maxDisplayLength);
|
||||
}
|
||||
dummyTextElement.textContent = actualEditorLines[i];
|
||||
const lineWidth = dom.getFastTextWidth(
|
||||
dummyTextElement, fontSize, fontWeight, fontFamily);
|
||||
if (lineWidth > totalWidth) {
|
||||
totalWidth = lineWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const scrollbarWidth =
|
||||
this.htmlInput_.offsetWidth - this.htmlInput_.clientWidth;
|
||||
totalWidth += scrollbarWidth;
|
||||
}
|
||||
if (this.borderRect_) {
|
||||
totalHeight += this.getConstants().FIELD_BORDER_RECT_Y_PADDING * 2;
|
||||
totalWidth += this.getConstants().FIELD_BORDER_RECT_X_PADDING * 2;
|
||||
this.borderRect_.setAttribute('width', totalWidth);
|
||||
this.borderRect_.setAttribute('height', totalHeight);
|
||||
}
|
||||
this.size_.width = totalWidth;
|
||||
this.size_.height = totalHeight;
|
||||
|
||||
this.positionBorderRect_();
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the inline free-text editor on top of the text.
|
||||
* Overrides the default behaviour to force rerender in order to
|
||||
* correct block size, based on editor text.
|
||||
* @param {Event=} _opt_e Optional mouse event that triggered the field to open,
|
||||
* or undefined if triggered programmatically.
|
||||
* @param {boolean=} opt_quietInput True if editor should be created without
|
||||
* focus. Defaults to false.
|
||||
* @override
|
||||
*/
|
||||
FieldMultilineInput.prototype.showEditor_ = function(_opt_e, opt_quietInput) {
|
||||
FieldMultilineInput.superClass_.showEditor_.call(
|
||||
this, _opt_e, opt_quietInput);
|
||||
this.forceRerender();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the text input editor widget.
|
||||
* @return {!HTMLTextAreaElement} The newly created text input editor.
|
||||
* @protected
|
||||
*/
|
||||
FieldMultilineInput.prototype.widgetCreate_ = function() {
|
||||
const div = WidgetDiv.getDiv();
|
||||
const scale = this.workspace_.getScale();
|
||||
|
||||
const htmlInput =
|
||||
/** @type {HTMLTextAreaElement} */ (document.createElement('textarea'));
|
||||
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
|
||||
htmlInput.setAttribute('spellcheck', this.spellcheck_);
|
||||
const fontSize = (this.getConstants().FIELD_TEXT_FONTSIZE * scale) + 'pt';
|
||||
div.style.fontSize = fontSize;
|
||||
htmlInput.style.fontSize = fontSize;
|
||||
const borderRadius = (FieldTextInput.BORDERRADIUS * scale) + 'px';
|
||||
htmlInput.style.borderRadius = borderRadius;
|
||||
const paddingX = this.getConstants().FIELD_BORDER_RECT_X_PADDING * scale;
|
||||
const paddingY = this.getConstants().FIELD_BORDER_RECT_Y_PADDING * scale / 2;
|
||||
htmlInput.style.padding =
|
||||
paddingY + 'px ' + paddingX + 'px ' + paddingY + 'px ' + paddingX + 'px';
|
||||
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
|
||||
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
|
||||
htmlInput.style.lineHeight = (lineHeight * scale) + 'px';
|
||||
|
||||
div.appendChild(htmlInput);
|
||||
|
||||
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
|
||||
htmlInput.untypedDefaultValue_ = this.value_;
|
||||
htmlInput.oldValue_ = null;
|
||||
if (userAgent.GECKO) {
|
||||
// In FF, ensure the browser reflows before resizing to avoid issue #2777.
|
||||
setTimeout(this.resizeEditor_.bind(this), 0);
|
||||
} else {
|
||||
this.resizeEditor_();
|
||||
}
|
||||
|
||||
this.bindInputEvents_(htmlInput);
|
||||
|
||||
return htmlInput;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the maxLines config for this field.
|
||||
* @param {number} maxLines Defines the maximum number of lines allowed,
|
||||
* before scrolling functionality is enabled.
|
||||
*/
|
||||
FieldMultilineInput.prototype.setMaxLines = function(maxLines) {
|
||||
if (typeof maxLines === 'number' && maxLines > 0 &&
|
||||
maxLines !== this.maxLines_) {
|
||||
this.maxLines_ = maxLines;
|
||||
this.forceRerender();
|
||||
/**
|
||||
* Construct a FieldMultilineInput from a JSON arg object,
|
||||
* dereferencing any string table references.
|
||||
* @param {!Object} options A JSON object with options (text, and spellcheck).
|
||||
* @return {!FieldMultilineInput} The new field instance.
|
||||
* @package
|
||||
* @nocollapse
|
||||
* @override
|
||||
*/
|
||||
static fromJson(options) {
|
||||
const text = parsing.replaceMessageReferences(options['text']);
|
||||
// `this` might be a subclass of FieldMultilineInput if that class doesn't
|
||||
// override the static fromJson method.
|
||||
return new this(text, undefined, options);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the maxLines config of this field.
|
||||
* @return {number} The maxLines config value.
|
||||
*/
|
||||
FieldMultilineInput.prototype.getMaxLines = function() {
|
||||
return this.maxLines_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle key down to the editor. Override the text input definition of this
|
||||
* so as to not close the editor when enter is typed in.
|
||||
* @param {!Event} e Keyboard event.
|
||||
* @protected
|
||||
*/
|
||||
FieldMultilineInput.prototype.onHtmlInputKeyDown_ = function(e) {
|
||||
if (e.keyCode !== KeyCodes.ENTER) {
|
||||
FieldMultilineInput.superClass_.onHtmlInputKeyDown_.call(this, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS for multiline field. See css.js for use.
|
||||
|
||||
332
core/icon.js
332
core/icon.js
@@ -29,189 +29,197 @@ const {Svg} = goog.require('Blockly.utils.Svg');
|
||||
|
||||
/**
|
||||
* Class for an icon.
|
||||
* @param {BlockSvg} block The block associated with this icon.
|
||||
* @constructor
|
||||
* @abstract
|
||||
* @alias Blockly.Icon
|
||||
*/
|
||||
const Icon = function(block) {
|
||||
class Icon {
|
||||
/**
|
||||
* The block this icon is attached to.
|
||||
* @type {BlockSvg}
|
||||
* @param {BlockSvg} block The block associated with this icon.
|
||||
* @alias Blockly.Icon
|
||||
*/
|
||||
constructor(block) {
|
||||
/**
|
||||
* The block this icon is attached to.
|
||||
* @type {BlockSvg}
|
||||
* @protected
|
||||
*/
|
||||
this.block_ = block;
|
||||
|
||||
/**
|
||||
* The icon SVG group.
|
||||
* @type {?SVGGElement}
|
||||
*/
|
||||
this.iconGroup_ = null;
|
||||
|
||||
/**
|
||||
* Whether this icon gets hidden when the block is collapsed.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.collapseHidden = true;
|
||||
|
||||
/**
|
||||
* Height and width of icons.
|
||||
* @const
|
||||
*/
|
||||
this.SIZE = 17;
|
||||
|
||||
/**
|
||||
* Bubble UI (if visible).
|
||||
* @type {?Bubble}
|
||||
* @protected
|
||||
*/
|
||||
this.bubble_ = null;
|
||||
|
||||
/**
|
||||
* Absolute coordinate of icon's center.
|
||||
* @type {?Coordinate}
|
||||
* @protected
|
||||
*/
|
||||
this.iconXY_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the icon on the block.
|
||||
*/
|
||||
createIcon() {
|
||||
if (this.iconGroup_) {
|
||||
// Icon already exists.
|
||||
return;
|
||||
}
|
||||
/* Here's the markup that will be generated:
|
||||
<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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
948
core/mutator.js
948
core/mutator.js
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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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_;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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_;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -47,6 +47,11 @@ class Drawer extends BaseDrawer {
|
||||
*/
|
||||
constructor(block, info) {
|
||||
super(block, info);
|
||||
|
||||
/**
|
||||
* @type {!RenderInfo}
|
||||
*/
|
||||
this.info_;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
1590
core/scrollbar.js
1590
core/scrollbar.js
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
254
core/warning.js
254
core/warning.js
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'});
|
||||
|
||||
Reference in New Issue
Block a user