diff --git a/core/block.js b/core/block.js
index e7c38e27e..ab1b25089 100644
--- a/core/block.js
+++ b/core/block.js
@@ -40,7 +40,6 @@ goog.require('Blockly.utils.Coordinate');
goog.require('Blockly.utils.object');
goog.require('Blockly.fieldRegistry');
goog.require('Blockly.utils.string');
-goog.require('Blockly.Warning');
goog.require('Blockly.Workspace');
@@ -128,9 +127,24 @@ Blockly.Block = function(workspace, prototypeName, opt_id) {
*/
this.collapsed_ = false;
- /** @type {string|Blockly.Comment} */
+ /**
+ * A string representing the comment attached to this block.
+ * @type {string|Blockly.Comment}
+ * @deprecated August 2019. Use getCommentText instead.
+ */
this.comment = null;
+ /**
+ * A model of the comment attached to this block.
+ * @type {!Blockly.Block.CommentModel}
+ * @package
+ */
+ this.commentModel = {
+ text: null,
+ pinned: false,
+ size: new Blockly.utils.Size(160, 80)
+ };
+
/**
* The block's position in workspace units. (0, 0) is at the workspace's
* origin; scale does not change this value.
@@ -205,6 +219,15 @@ Blockly.Block = function(workspace, prototypeName, opt_id) {
}
};
+/**
+ * @typedef {{
+ * text:?string,
+ * pinned:boolean,
+ * size:Blockly.utils.Size
+ * }}
+ */
+Blockly.Block.CommentModel;
+
/**
* Optional text data that round-trips between blocks and XML.
* Has no effect. May be used by 3rd parties for meta information.
@@ -1783,11 +1806,11 @@ Blockly.Block.prototype.getInputTargetBlock = function(name) {
};
/**
- * Returns the comment on this block (or '' if none).
+ * Returns the comment on this block (or null if there is no comment).
* @return {string} Block's comment.
*/
Blockly.Block.prototype.getCommentText = function() {
- return this.comment || '';
+ return this.commentModel.text;
};
/**
@@ -1795,11 +1818,13 @@ Blockly.Block.prototype.getCommentText = function() {
* @param {?string} text The text, or null to delete.
*/
Blockly.Block.prototype.setCommentText = function(text) {
- if (this.comment != text) {
- Blockly.Events.fire(new Blockly.Events.BlockChange(
- this, 'comment', null, this.comment, text || ''));
- this.comment = text;
+ if (this.commentModel.text == text) {
+ return;
}
+ Blockly.Events.fire(new Blockly.Events.BlockChange(
+ this, 'comment', null, this.commentModel.text, text));
+ this.commentModel.text = text;
+ this.comment = text; // For backwards compatibility.
};
/**
diff --git a/core/block_svg.js b/core/block_svg.js
index 5e705302a..11232da35 100644
--- a/core/block_svg.js
+++ b/core/block_svg.js
@@ -42,6 +42,7 @@ goog.require('Blockly.utils.Coordinate');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.Rect');
+goog.require('Blockly.Warning');
/**
@@ -258,9 +259,17 @@ Blockly.BlockSvg.prototype.mutator = null;
/**
* Block's comment icon (if any).
* @type {Blockly.Comment}
+ * @deprecated August 2019. Use getCommentIcon instead.
*/
Blockly.BlockSvg.prototype.comment = null;
+/**
+ * Block's comment icon (if any).
+ * @type {Blockly.Comment}
+ * @private
+ */
+Blockly.BlockSvg.prototype.commentIcon_ = null;
+
/**
* Block's warning icon (if any).
* @type {Blockly.Warning}
@@ -276,8 +285,8 @@ Blockly.BlockSvg.prototype.getIcons = function() {
if (this.mutator) {
icons.push(this.mutator);
}
- if (this.comment) {
- icons.push(this.comment);
+ if (this.commentIcon_) {
+ icons.push(this.commentIcon_);
}
if (this.warning) {
icons.push(this.warning);
@@ -951,28 +960,11 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) {
this.warningTextDb_ = null;
}
- // If the block is rendered we need to record the event before disposing of
- // the icons to prevent losing information.
- // TODO (#1969): Remove event generation/firing once comments are fixed.
- this.unplug(healStack);
- var deleteEvent;
- if (Blockly.Events.isEnabled()) {
- deleteEvent = new Blockly.Events.BlockDelete(this);
- }
- Blockly.Events.disable();
- try {
- var icons = this.getIcons();
- for (var i = 0; i < icons.length; i++) {
- icons[i].dispose();
- }
- // TODO (#1969): Move out of disable block once comments are fixed.
- Blockly.BlockSvg.superClass_.dispose.call(this, healStack);
- } finally {
- Blockly.Events.enable();
- }
- if (Blockly.Events.isEnabled() && deleteEvent) {
- Blockly.Events.fire(deleteEvent);
+ var icons = this.getIcons();
+ for (var i = 0; i < icons.length; i++) {
+ icons[i].dispose();
}
+ Blockly.BlockSvg.superClass_.dispose.call(this, healStack);
Blockly.utils.dom.removeNode(this.svgGroup_);
blockWorkspace.resizeContents();
@@ -1072,16 +1064,12 @@ Blockly.BlockSvg.prototype.updateDisabled = function() {
};
/**
- * Returns the comment on this block (or '' if none).
- * @return {string} Block's comment.
+ * Get the comment icon attached to this block, or null if the block has no
+ * comment.
+ * @return {Blockly.Comment} The comment icon attached to this block, or null.
*/
-Blockly.BlockSvg.prototype.getCommentText = function() {
- if (this.comment) {
- var comment = this.comment.getText();
- // Trim off trailing whitespace.
- return comment.replace(/\s+$/, '').replace(/ +\n/g, '\n');
- }
- return '';
+Blockly.BlockSvg.prototype.getCommentIcon = function() {
+ return this.commentIcon_;
};
/**
@@ -1089,23 +1077,30 @@ Blockly.BlockSvg.prototype.getCommentText = function() {
* @param {?string} text The text, or null to delete.
*/
Blockly.BlockSvg.prototype.setCommentText = function(text) {
- var changedState = false;
- if (typeof text == 'string') {
- if (!this.comment) {
- if (!Blockly.Comment) {
- throw Error('Missing require for Blockly.Comment');
- }
- this.comment = new Blockly.Comment(this);
- changedState = true;
- }
- this.comment.setText(/** @type {string} */ (text));
- } else {
- if (this.comment) {
- this.comment.dispose();
- changedState = true;
- }
+ if (!Blockly.Comment) {
+ throw Error('Missing require for Blockly.Comment');
}
- if (changedState && this.rendered) {
+ if (this.commentModel.text == text) {
+ return;
+ }
+ Blockly.BlockSvg.superClass_.setCommentText.call(this, text);
+
+ var shouldHaveComment = text != null;
+ if (!!this.commentIcon_ == shouldHaveComment) {
+ // If the comment's state of existence is correct, but the text is new
+ // that means we're just updating a comment.
+ this.commentIcon_.updateText();
+ return;
+ }
+ if (shouldHaveComment) {
+ this.commentIcon_ = new Blockly.Comment(this);
+ this.comment = this.commentIcon_; // For backwards compatibility.
+ } else {
+ this.commentIcon_.dispose();
+ this.commentIcon_ = null;
+ this.comment = null; // For backwards compatibility.
+ }
+ if (this.rendered) {
this.render();
// Adding or removing a comment icon will cause the block to change shape.
this.bumpNeighbours_();
diff --git a/core/bubble.js b/core/bubble.js
index 516c4b208..9e205a055 100644
--- a/core/bubble.js
+++ b/core/bubble.js
@@ -625,10 +625,10 @@ Blockly.Bubble.prototype.moveTo = function(x, y) {
/**
* Get the dimensions of this bubble.
- * @return {!Object} Object with width and height properties.
+ * @return {!Blockly.utils.Size} The height and width of the bubble.
*/
Blockly.Bubble.prototype.getBubbleSize = function() {
- return {width: this.width_, height: this.height_};
+ return new Blockly.utils.Size(this.width_, this.height_);
};
/**
@@ -657,13 +657,12 @@ Blockly.Bubble.prototype.setBubbleSize = function(width, height) {
(height - doubleBorderWidth) + ')');
}
}
- if (this.rendered_) {
- if (this.autoLayout_) {
- this.layoutBubble_();
- }
- this.positionBubble_();
- this.renderArrow_();
+ if (this.autoLayout_) {
+ this.layoutBubble_();
}
+ this.positionBubble_();
+ this.renderArrow_();
+
// Allow the contents to resize.
if (this.resizeCallback_) {
this.resizeCallback_();
diff --git a/core/comment.js b/core/comment.js
index a86bfac6f..df3e9e5e2 100644
--- a/core/comment.js
+++ b/core/comment.js
@@ -44,28 +44,29 @@ goog.require('Blockly.utils.userAgent');
*/
Blockly.Comment = function(block) {
Blockly.Comment.superClass_.constructor.call(this, block);
+
+ /**
+ * The model for this comment.
+ * @type {!Blockly.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 || '';
+
+ /**
+ * 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_ = '';
+
this.createIcon();
};
Blockly.utils.object.inherits(Blockly.Comment, Blockly.Icon);
-/**
- * Comment text (if bubble is not visible).
- * @private
- */
-Blockly.Comment.prototype.text_ = '';
-
-/**
- * Width of bubble.
- * @private
- */
-Blockly.Comment.prototype.width_ = 160;
-
-/**
- * Height of bubble.
- * @private
- */
-Blockly.Comment.prototype.height_ = 80;
-
/**
* Draw the comment icon.
* @param {!Element} group The icon group.
@@ -104,7 +105,8 @@ Blockly.Comment.prototype.drawIcon_ = function(group) {
* @private
*/
Blockly.Comment.prototype.createEditor_ = function() {
- /* Create the editor. Here's the markup that will be generated:
+ /* Create the editor. Here's the markup that will be generated in
+ * editable mode:
- */
+ * For non-editable mode see Warning.textToDom_.
+ */
+
this.foreignObject_ = Blockly.utils.dom.createSvgElement('foreignObject',
{'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH},
null);
+
var body = document.createElementNS(Blockly.utils.dom.HTML_NS, 'body');
body.setAttribute('xmlns', Blockly.utils.dom.HTML_NS);
body.className = 'blocklyMinimalBody';
- var textarea = document.createElementNS(Blockly.utils.dom.HTML_NS, 'textarea');
+
+ this.textarea_ = document.createElementNS(
+ Blockly.utils.dom.HTML_NS, 'textarea');
+ var 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.textarea_ = textarea;
this.foreignObject_.appendChild(body);
- Blockly.bindEventWithChecks_(textarea, 'mouseup', this, this.textareaFocus_,
+
+ // 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.
+ Blockly.bindEventWithChecks_(textarea, 'mouseup', this, this.startEdit_,
true, true);
// Don't zoom with mousewheel.
Blockly.bindEventWithChecks_(textarea, 'wheel', this, function(e) {
e.stopPropagation();
});
Blockly.bindEventWithChecks_(textarea, 'change', this, function(_e) {
- if (this.text_ != textarea.value) {
+ if (this.cachedText_ != this.model_.text) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
- this.block_, 'comment', null, this.text_, textarea.value));
- this.text_ = textarea.value;
+ this.block_, 'comment', null, this.cachedText_, this.model_.text));
}
});
+ Blockly.bindEventWithChecks_(textarea, 'input', this, function(_e) {
+ this.model_.text = textarea.value;
+ });
+
setTimeout(textarea.focus.bind(textarea), 0);
+
return this.foreignObject_;
};
@@ -147,13 +165,12 @@ Blockly.Comment.prototype.createEditor_ = function() {
* @override
*/
Blockly.Comment.prototype.updateEditable = function() {
+ Blockly.Comment.superClass_.updateEditable.call(this);
if (this.isVisible()) {
- // Toggling visibility will force a rerendering.
- this.setVisible(false);
- this.setVisible(true);
+ // Recreate the bubble with the correct UI.
+ this.disposeBubble_();
+ this.createBubble_();
}
- // Allow the icon to update.
- Blockly.Icon.prototype.updateEditable.call(this);
};
/**
@@ -161,15 +178,28 @@ Blockly.Comment.prototype.updateEditable = function() {
* Resize the text area accordingly.
* @private
*/
-Blockly.Comment.prototype.resizeBubble_ = function() {
- if (this.isVisible()) {
- var size = this.bubble_.getBubbleSize();
- var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
- this.foreignObject_.setAttribute('width', size.width - doubleBorderWidth);
- this.foreignObject_.setAttribute('height', size.height - doubleBorderWidth);
- this.textarea_.style.width = (size.width - doubleBorderWidth - 4) + 'px';
- this.textarea_.style.height = (size.height - doubleBorderWidth - 4) + 'px';
+Blockly.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
+ */
+Blockly.Comment.prototype.resizeTextarea_ = function() {
+ var size = this.model_.size;
+ var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
+ var widthMinusBorder = size.width - doubleBorderWidth;
+ var 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';
};
/**
@@ -178,71 +208,99 @@ Blockly.Comment.prototype.resizeBubble_ = function() {
*/
Blockly.Comment.prototype.setVisible = function(visible) {
if (visible == this.isVisible()) {
- // No change.
return;
}
Blockly.Events.fire(
new Blockly.Events.Ui(this.block_, 'commentOpen', !visible, visible));
- if ((!this.block_.isEditable() && !this.textarea_) ||
- Blockly.utils.userAgent.IE) {
+ this.model_.pinned = visible;
+ if (visible) {
+ this.createBubble_();
+ } else {
+ this.disposeBubble_();
+ }
+};
+
+/**
+ * Show the bubble. Handles deciding if it should be editable or not.
+ * @private
+ */
+Blockly.Comment.prototype.createBubble_ = function() {
+ if (!this.block_.isEditable() || Blockly.utils.userAgent.IE) {
// Steal the code from warnings to make an uneditable text bubble.
// 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.
- Blockly.Warning.prototype.setVisible.call(this, visible);
- return;
- }
- // Save the bubble stats before the visibility switch.
- var text = this.getText();
- var size = this.getBubbleSize();
- if (visible) {
- // Create the bubble.
- this.bubble_ = new Blockly.Bubble(
- /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
- this.createEditor_(), this.block_.svgPath_,
- this.iconXY_, this.width_, this.height_);
- // Expose this comment's block's ID on its top-level SVG group.
- this.bubble_.setSvgId(this.block_.id);
- this.bubble_.registerResizeEvent(this.resizeBubble_.bind(this));
- this.updateColour();
+ this.createNonEditableBubble_();
} else {
- // Dispose of the bubble.
- this.bubble_.dispose();
- this.bubble_ = null;
- this.textarea_ = null;
- this.foreignObject_ = null;
+ this.createEditableBubble_();
}
- // Restore the bubble stats after the visibility switch.
- this.setText(text);
- this.setBubbleSize(size.width, size.height);
};
/**
- * Bring the comment to the top of the stack when clicked on.
+ * Show an editable bubble.
+ * @private
+ */
+Blockly.Comment.prototype.createEditableBubble_ = function() {
+ this.bubble_ = new Blockly.Bubble(
+ /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
+ this.createEditor_(), this.block_.svgPath_,
+ 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.updateColour();
+};
+
+/**
+ * Show a non-editable bubble.
+ * @private
+ */
+Blockly.Comment.prototype.createNonEditableBubble_ = function() {
+ // TODO (#2917): It would be great if the comment could support line breaks.
+ Blockly.Warning.prototype.createBubble.call(this);
+};
+
+/**
+ * Dispose of the bubble.
+ * @private
+ */
+Blockly.Comment.prototype.disposeBubble_ = function() {
+ if (this.paragraphElement_) {
+ // We're using the warning UI so we have to let it dispose.
+ Blockly.Warning.prototype.disposeBubble.call(this);
+ return;
+ }
+
+ this.bubble_.dispose();
+ this.bubble_ = null;
+ this.textarea_ = null;
+ this.foreignObject_ = 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
*/
-Blockly.Comment.prototype.textareaFocus_ = function(_e) {
- // 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.
+Blockly.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();
}
+
+ this.cachedText_ = this.model_.text;
};
/**
* Get the dimensions of this comment's bubble.
- * @return {!Object} Object with width and height properties.
+ * @return {Blockly.utils.Size} Object with width and height properties.
*/
Blockly.Comment.prototype.getBubbleSize = function() {
- if (this.isVisible()) {
- return this.bubble_.getBubbleSize();
- } else {
- return {width: this.width_, height: this.height_};
- }
+ return this.model_.size;
};
/**
@@ -251,44 +309,60 @@ Blockly.Comment.prototype.getBubbleSize = function() {
* @param {number} height Height of the bubble.
*/
Blockly.Comment.prototype.setBubbleSize = function(width, height) {
- if (this.textarea_) {
+ if (this.bubble_) {
this.bubble_.setBubbleSize(width, height);
} else {
- this.width_ = width;
- this.height_ = height;
+ this.model_.size.width = width;
+ this.model_.size.height = height;
}
};
/**
* Returns this comment's text.
* @return {string} Comment text.
+ * @deprecated August 2019 Use block.getCommentText() instead.
*/
Blockly.Comment.prototype.getText = function() {
- return this.textarea_ ? this.textarea_.value : this.text_;
+ return this.model_.text || '';
};
/**
* Set this comment's text.
+ *
+ * If you want to receive a comment change event, then this should not be called
+ * directly. Instead call block.setCommentText();
* @param {string} text Comment text.
+ * @deprecated August 2019 Use block.setCommentText() instead.
*/
Blockly.Comment.prototype.setText = function(text) {
- if (this.text_ != text) {
- Blockly.Events.fire(new Blockly.Events.BlockChange(
- this.block_, 'comment', null, this.text_, text));
- this.text_ = text;
+ if (this.model_.text == text) {
+ return;
}
+ this.model_.text = text;
+ this.updateText();
+};
+
+/**
+ * Update the comment's view to match the model.
+ * @package
+ */
+Blockly.Comment.prototype.updateText = function() {
if (this.textarea_) {
- this.textarea_.value = text;
+ 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);
*/
Blockly.Comment.prototype.dispose = function() {
- if (Blockly.Events.isEnabled()) {
- this.setText(''); // Fire event to delete comment.
- }
this.block_.comment = null;
Blockly.Icon.prototype.dispose.call(this);
};
diff --git a/core/icon.js b/core/icon.js
index 8b71bbefa..be54e9747 100644
--- a/core/icon.js
+++ b/core/icon.js
@@ -34,10 +34,15 @@ goog.require('Blockly.utils.Size');
/**
* Class for an icon.
- * @param {Blockly.Block} block The block associated with this icon.
+ * @param {Blockly.BlockSvg} block The block associated with this icon.
* @constructor
*/
Blockly.Icon = function(block) {
+ /**
+ * The block this icon is attached to.
+ * @type {Blockly.BlockSvg}
+ * @protected
+ */
this.block_ = block;
};
@@ -108,6 +113,7 @@ Blockly.Icon.prototype.dispose = function() {
* Add or remove the UI indicating if this icon may be clicked or not.
*/
Blockly.Icon.prototype.updateEditable = function() {
+ // No-op on the base class.
};
/**
diff --git a/core/mutator.js b/core/mutator.js
index 78fceaa3e..b641eee32 100644
--- a/core/mutator.js
+++ b/core/mutator.js
@@ -183,6 +183,7 @@ Blockly.Mutator.prototype.createEditor_ = function() {
* Add or remove the UI indicating if this icon may be clicked or not.
*/
Blockly.Mutator.prototype.updateEditable = function() {
+ Blockly.Mutator.superClass_.updateEditable.call(this);
if (!this.block_.isInFlyout) {
if (this.block_.isEditable()) {
if (this.iconGroup_) {
@@ -200,8 +201,6 @@ Blockly.Mutator.prototype.updateEditable = function() {
}
}
}
- // Default behaviour for an icon.
- Blockly.Icon.prototype.updateEditable.call(this);
};
/**
@@ -297,7 +296,6 @@ Blockly.Mutator.prototype.setVisible = function(visible) {
};
this.block_.workspace.addChangeListener(this.sourceListener_);
}
- this.resizeBubble_();
// When the mutator's workspace changes, update the source block.
this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));
this.updateColour();
diff --git a/core/warning.js b/core/warning.js
index fa21b68e0..7e436e0e8 100644
--- a/core/warning.js
+++ b/core/warning.js
@@ -116,40 +116,57 @@ Blockly.Warning.textToDom_ = function(text) {
*/
Blockly.Warning.prototype.setVisible = function(visible) {
if (visible == this.isVisible()) {
- // No change.
return;
}
Blockly.Events.fire(
new Blockly.Events.Ui(this.block_, 'warningOpen', !visible, visible));
if (visible) {
- // Create the bubble to display all warnings.
- var paragraph = Blockly.Warning.textToDom_(this.getText());
- this.bubble_ = new Blockly.Bubble(
- /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
- paragraph, this.block_.svgPath_, this.iconXY_, null, null);
- // Expose this warning's block's ID on its top-level SVG group.
- this.bubble_.setSvgId(this.block_.id);
- if (this.block_.RTL) {
- // Right-align the paragraph.
- // This cannot be done until the bubble is rendered on screen.
- var maxWidth = paragraph.getBBox().width;
- for (var i = 0, textElement; textElement = paragraph.childNodes[i]; i++) {
- textElement.setAttribute('text-anchor', 'end');
- textElement.setAttribute('x', maxWidth + Blockly.Bubble.BORDER_WIDTH);
- }
- }
- this.updateColour();
- // Bump the warning into the right location.
- var size = this.bubble_.getBubbleSize();
- this.bubble_.setBubbleSize(size.width, size.height);
+ this.createBubble();
} else {
- // Dispose of the bubble.
- this.bubble_.dispose();
- this.bubble_ = null;
- this.body_ = null;
+ this.disposeBubble();
}
};
+/**
+ * Show the bubble.
+ * @package
+ */
+Blockly.Warning.prototype.createBubble = function() {
+ // TODO (#2943): This is package because comments steal this UI for
+ // non-editable comments, but really this should be private.
+ this.paragraphElement_ = Blockly.Warning.textToDom_(this.getText());
+ this.bubble_ = new Blockly.Bubble(
+ /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
+ this.paragraphElement_, this.block_.svgPath_, this.iconXY_, null, null);
+ // Expose this warning's block's ID on its top-level SVG group.
+ this.bubble_.setSvgId(this.block_.id);
+ if (this.block_.RTL) {
+ // Right-align the paragraph.
+ // This cannot be done until the bubble is rendered on screen.
+ var maxWidth = this.paragraphElement_.getBBox().width;
+ for (var i = 0, textElement;
+ textElement = this.paragraphElement_.childNodes[i]; i++) {
+
+ textElement.setAttribute('text-anchor', 'end');
+ textElement.setAttribute('x', maxWidth + Blockly.Bubble.BORDER_WIDTH);
+ }
+ }
+ this.updateColour();
+};
+
+/**
+ * Dispose of the bubble and references to it.
+ * @package
+ */
+Blockly.Warning.prototype.disposeBubble = function() {
+ // TODO (#2943): This is package because comments steal this UI for
+ // non-editable comments, but really this should be private.
+ this.bubble_.dispose();
+ this.bubble_ = null;
+ this.body_ = null;
+ this.paragraphElement_ = null;
+};
+
/**
* Bring the warning to the top of the stack when clicked on.
* @param {!Event} _e Mouse up event.
@@ -162,7 +179,8 @@ Blockly.Warning.prototype.bodyFocus_ = function(_e) {
/**
* Set this warning's text.
- * @param {string} text Warning text (or '' to delete).
+ * @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.
*/
diff --git a/core/xml.js b/core/xml.js
index decb44f1e..c33a63a5a 100644
--- a/core/xml.js
+++ b/core/xml.js
@@ -163,14 +163,15 @@ Blockly.Xml.blockToDom = function(block, opt_noId) {
var commentText = block.getCommentText();
if (commentText) {
+ var size = block.commentModel.size;
+ var pinned = block.commentModel.pinned;
+
var commentElement = Blockly.utils.xml.createElement('comment');
commentElement.appendChild(Blockly.utils.xml.createTextNode(commentText));
- if (typeof block.comment == 'object') {
- commentElement.setAttribute('pinned', block.comment.isVisible());
- var hw = block.comment.getBubbleSize();
- commentElement.setAttribute('h', hw.height);
- commentElement.setAttribute('w', hw.width);
- }
+ commentElement.setAttribute('pinned', pinned);
+ commentElement.setAttribute('h', size.height);
+ commentElement.setAttribute('w', size.width);
+
element.appendChild(commentElement);
}
@@ -655,22 +656,21 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) {
}
break;
case 'comment':
- block.setCommentText(xmlChild.textContent);
- var visible = xmlChild.getAttribute('pinned');
- if (visible && !block.isInFlyout) {
- // Give the renderer a millisecond to render and position the block
- // before positioning the comment bubble.
- setTimeout(function() {
- if (block.comment && block.comment.setVisible) {
- block.comment.setVisible(visible == 'true');
- }
- }, 1);
+ var text = xmlChild.textContent;
+ var pinned = xmlChild.getAttribute('pinned') == 'true';
+ var width = parseInt(xmlChild.getAttribute('w'), 10);
+ var height = parseInt(xmlChild.getAttribute('h'), 10);
+
+ block.setCommentText(text);
+ block.commentModel.pinned = pinned;
+ if (!isNaN(width) && !isNaN(height)) {
+ block.commentModel.size = new Blockly.utils.Size(width, height);
}
- var bubbleW = parseInt(xmlChild.getAttribute('w'), 10);
- var bubbleH = parseInt(xmlChild.getAttribute('h'), 10);
- if (!isNaN(bubbleW) && !isNaN(bubbleH) &&
- block.comment && block.comment.setVisible) {
- block.comment.setBubbleSize(bubbleW, bubbleH);
+
+ if (pinned && block.getCommentIcon && !block.isInFlyout) {
+ setTimeout(function() {
+ block.getCommentIcon().setVisible(true);
+ }, 1);
}
break;
case 'data':
diff --git a/generators/dart.js b/generators/dart.js
index 0f9541ac1..e8e31b442 100644
--- a/generators/dart.js
+++ b/generators/dart.js
@@ -213,8 +213,9 @@ Blockly.Dart.scrub_ = function(block, code, opt_thisOnly) {
if (!block.outputConnection || !block.outputConnection.targetConnection) {
// Collect comment for this block.
var comment = block.getCommentText();
- comment = Blockly.utils.string.wrap(comment, Blockly.Dart.COMMENT_WRAP - 3);
if (comment) {
+ comment = Blockly.utils.string.wrap(comment,
+ Blockly.Dart.COMMENT_WRAP - 3);
if (block.getProcedureDef) {
// Use documentation comment for function comments.
commentCode += Blockly.Dart.prefixLines(comment + '\n', '/// ');
diff --git a/generators/javascript.js b/generators/javascript.js
index 1c40ae68b..14aa82e00 100644
--- a/generators/javascript.js
+++ b/generators/javascript.js
@@ -254,9 +254,9 @@ Blockly.JavaScript.scrub_ = function(block, code, opt_thisOnly) {
if (!block.outputConnection || !block.outputConnection.targetConnection) {
// Collect comment for this block.
var comment = block.getCommentText();
- comment = Blockly.utils.string.wrap(comment,
- Blockly.JavaScript.COMMENT_WRAP - 3);
if (comment) {
+ comment = Blockly.utils.string.wrap(comment,
+ Blockly.JavaScript.COMMENT_WRAP - 3);
if (block.getProcedureDef) {
// Use a comment block for function comments.
commentCode += '/**\n' +
diff --git a/generators/lua.js b/generators/lua.js
index e89289812..bc968cd75 100644
--- a/generators/lua.js
+++ b/generators/lua.js
@@ -187,8 +187,9 @@ Blockly.Lua.scrub_ = function(block, code, opt_thisOnly) {
if (!block.outputConnection || !block.outputConnection.targetConnection) {
// Collect comment for this block.
var comment = block.getCommentText();
- comment = Blockly.utils.string.wrap(comment, Blockly.Lua.COMMENT_WRAP - 3);
if (comment) {
+ comment = Blockly.utils.string.wrap(comment,
+ Blockly.Lua.COMMENT_WRAP - 3);
commentCode += Blockly.Lua.prefixLines(comment, '-- ') + '\n';
}
// Collect comments for all value arguments.
diff --git a/generators/php.js b/generators/php.js
index 06d219e84..9d5ed9ab5 100644
--- a/generators/php.js
+++ b/generators/php.js
@@ -240,8 +240,9 @@ Blockly.PHP.scrub_ = function(block, code, opt_thisOnly) {
if (!block.outputConnection || !block.outputConnection.targetConnection) {
// Collect comment for this block.
var comment = block.getCommentText();
- comment = Blockly.utils.string.wrap(comment, Blockly.PHP.COMMENT_WRAP - 3);
if (comment) {
+ comment = Blockly.utils.string.wrap(comment,
+ Blockly.PHP.COMMENT_WRAP - 3);
commentCode += Blockly.PHP.prefixLines(comment, '// ') + '\n';
}
// Collect comments for all value arguments.
diff --git a/generators/python.js b/generators/python.js
index e2fbac3a9..498d8eed4 100644
--- a/generators/python.js
+++ b/generators/python.js
@@ -268,9 +268,9 @@ Blockly.Python.scrub_ = function(block, code, opt_thisOnly) {
if (!block.outputConnection || !block.outputConnection.targetConnection) {
// Collect comment for this block.
var comment = block.getCommentText();
- comment = Blockly.utils.string.wrap(comment,
- Blockly.Python.COMMENT_WRAP - 3);
if (comment) {
+ comment = Blockly.utils.string.wrap(comment,
+ Blockly.Python.COMMENT_WRAP - 3);
if (block.getProcedureDef) {
// Use a comment block for function comments.
commentCode += '"""' + comment + '\n"""\n';
diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js
index b0d87ed4f..500854889 100644
--- a/tests/mocha/block_test.js
+++ b/tests/mocha/block_test.js
@@ -410,11 +410,137 @@ suite('Blocks', function() {
.connect(blockB.previousConnection);
this.blockA.removeInput('STATEMENT');
- console.log(blockB.disposed, blockB);
chai.assert.isTrue(blockB.disposed);
chai.assert.equal(this.blockA.getChildren().length, 0);
});
});
});
});
+ suite('Comments', function() {
+ setup(function() {
+ Blockly.defineBlocksWithJsonArray([
+ {
+ "type": "empty_block",
+ "message0": "",
+ "args0": []
+ },
+ ]);
+ this.eventSpy = sinon.spy(Blockly.Events, 'fire');
+ });
+ teardown(function() {
+ delete Blockly.Blocks['empty_block'];
+ this.eventSpy.restore();
+ });
+ suite('Set/Get Text', function() {
+ function assertCommentEvent(eventSpy, oldValue, newValue) {
+ var calls = eventSpy.getCalls();
+ var event = calls[calls.length - 1].args[0];
+ chai.assert.equal(event.type, Blockly.Events.BLOCK_CHANGE);
+ chai.assert.equal(event.element, 'comment');
+ chai.assert.equal(event.oldValue, oldValue);
+ chai.assert.equal(event.newValue, newValue);
+ }
+ function assertNoCommentEvent(eventSpy) {
+ var calls = eventSpy.getCalls();
+ var event = calls[calls.length - 1].args[0];
+ chai.assert.notEqual(event.type, Blockly.Events.BLOCK_CHANGE);
+ }
+ suite('Headless', function() {
+ setup(function() {
+ this.block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ ''
+ ), this.workspace);
+ });
+ test('Text', function() {
+ this.block.setCommentText('test text');
+ chai.assert.equal(this.block.getCommentText(), 'test text');
+ assertCommentEvent(this.eventSpy, null, 'test text');
+ });
+ test('Text Empty', function() {
+ this.block.setCommentText('');
+ chai.assert.equal(this.block.getCommentText(), '');
+ assertCommentEvent(this.eventSpy, null, '');
+ });
+ test('Text Null', function() {
+ this.block.setCommentText(null);
+ chai.assert.equal(this.block.getCommentText(), null);
+ assertNoCommentEvent(this.eventSpy);
+ });
+ test('Text -> Null', function() {
+ this.block.setCommentText('first text');
+
+ this.block.setCommentText(null);
+ chai.assert.equal(this.block.getCommentText(), null);
+ assertCommentEvent(this.eventSpy, 'first text', null);
+ });
+ });
+ suite('Rendered', function() {
+ setup(function() {
+ // Let the parent teardown take care of this.
+ this.workspace = Blockly.inject('blocklyDiv', {
+ comments: true,
+ scrollbars: true
+ });
+ this.block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ ''
+ ), this.workspace);
+ });
+ test('Text', function() {
+ this.block.setCommentText('test text');
+ chai.assert.equal(this.block.getCommentText(), 'test text');
+ assertCommentEvent(this.eventSpy, null, 'test text');
+ });
+ test('Text Empty', function() {
+ this.block.setCommentText('');
+ chai.assert.equal(this.block.getCommentText(), '');
+ assertCommentEvent(this.eventSpy, null, '');
+ });
+ test('Text Null', function() {
+ this.block.setCommentText(null);
+ chai.assert.equal(this.block.getCommentText(), null);
+ assertNoCommentEvent(this.eventSpy);
+ });
+ test('Text -> Null', function() {
+ this.block.setCommentText('first text');
+
+ this.block.setCommentText(null);
+ chai.assert.equal(this.block.getCommentText(), null);
+ assertCommentEvent(this.eventSpy, 'first text', null);
+ });
+ test('Set While Visible - Editable', function() {
+ this.block.setCommentText('test1');
+ var icon = this.block.getCommentIcon();
+ icon.setVisible(true);
+
+ this.block.setCommentText('test2');
+ chai.assert.equal(this.block.getCommentText(), 'test2');
+ assertCommentEvent(this.eventSpy, 'test1', 'test2');
+ chai.assert.equal(icon.textarea_.value, 'test2');
+ });
+ test('Set While Visible - NonEditable', function() {
+ this.block.setCommentText('test1');
+ var editableStub = sinon.stub(this.block, 'isEditable').returns(false);
+ var icon = this.block.getCommentIcon();
+ icon.setVisible(true);
+
+ this.block.setCommentText('test2');
+ chai.assert.equal(this.block.getCommentText(), 'test2');
+ assertCommentEvent(this.eventSpy, 'test1', 'test2');
+ chai.assert.equal(icon.paragraphElement_.firstChild.textContent,
+ 'test2');
+
+ editableStub.restore();
+ });
+ test('Get Text While Editing', function() {
+ this.block.setCommentText('test1');
+ var icon = this.block.getCommentIcon();
+ icon.setVisible(true);
+ icon.textarea_.value = 'test2';
+ icon.textarea_.dispatchEvent(new Event('input'));
+
+ chai.assert.equal(this.block.getCommentText(), 'test2');
+ });
+ });
+ });
+ });
});
diff --git a/tests/mocha/comment_test.js b/tests/mocha/comment_test.js
new file mode 100644
index 000000000..4651405c1
--- /dev/null
+++ b/tests/mocha/comment_test.js
@@ -0,0 +1,144 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2019 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+suite('Comments', function() {
+ setup(function() {
+ Blockly.defineBlocksWithJsonArray([
+ {
+ "type": "empty_block",
+ "message0": "",
+ "args0": []
+ },
+ ]);
+
+ this.workspace = Blockly.inject('blocklyDiv', {
+ comments: true,
+ scrollbars: true
+ });
+ this.block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ ''
+ ), this.workspace);
+ this.comment = new Blockly.Comment(this.block);
+ this.comment.computeIconLocation();
+ });
+ teardown(function() {
+ delete Blockly.Blocks['empty_block'];
+ this.workspace.dispose();
+ });
+ suite('Visibility and Editability', function() {
+ setup(function() {
+ this.comment.setText('test text');
+ this.eventSpy = sinon.stub(Blockly.Events, 'fire');
+ });
+ teardown(function() {
+ this.eventSpy.restore();
+ });
+ function assertEvent(eventSpy, type, element, oldValue, newValue) {
+ var calls = eventSpy.getCalls();
+ var event = calls[calls.length - 1].args[0];
+ chai.assert.equal(event.type, type);
+ chai.assert.equal(event.element, element);
+ chai.assert.equal(event.oldValue, oldValue);
+ chai.assert.equal(event.newValue, newValue);
+ }
+ function assertEditable(comment) {
+ chai.assert.isNotOk(comment.paragraphElement_);
+ chai.assert.isOk(comment.textarea_);
+ chai.assert.equal(comment.textarea_.value, 'test text');
+ }
+ function assertNotEditable(comment) {
+ chai.assert.isNotOk(comment.textarea_);
+ chai.assert.isOk(comment.paragraphElement_);
+ chai.assert.equal(comment.paragraphElement_.firstChild.textContent,
+ 'test text');
+ }
+ test('Editable', function() {
+ this.comment.setVisible(true);
+ chai.assert.isTrue(this.comment.isVisible());
+ assertEditable(this.comment);
+ assertEvent(this.eventSpy, Blockly.Events.UI, 'commentOpen', false, true);
+ });
+ test('Not Editable', function() {
+ var editableStub = sinon.stub(this.block, 'isEditable').returns(false);
+
+ this.comment.setVisible(true);
+ chai.assert.isTrue(this.comment.isVisible());
+ assertNotEditable(this.comment);
+ assertEvent(this.eventSpy, Blockly.Events.UI, 'commentOpen', false, true);
+
+ editableStub.restore();
+ });
+ test('Editable -> Not Editable', function() {
+ this.comment.setVisible(true);
+ var editableStub = sinon.stub(this.block, 'isEditable').returns(false);
+
+ this.comment.updateEditable();
+ chai.assert.isTrue(this.comment.isVisible());
+ assertNotEditable(this.comment);
+ assertEvent(this.eventSpy, Blockly.Events.UI, 'commentOpen', false, true);
+
+ editableStub.restore();
+ });
+ test('Not Editable -> Editable', function() {
+ var editableStub = sinon.stub(this.block, 'isEditable').returns(false);
+ this.comment.setVisible(true);
+ editableStub.returns(true);
+
+ this.comment.updateEditable();
+ chai.assert.isTrue(this.comment.isVisible());
+ assertEditable(this.comment);
+ assertEvent(this.eventSpy, Blockly.Events.UI, 'commentOpen', false, true);
+
+ editableStub.restore();
+ });
+ });
+ suite('Set/Get Bubble Size', function() {
+ function assertBubbleSize(comment, height, width) {
+ var size = comment.getBubbleSize();
+ chai.assert.equal(size.height, height);
+ chai.assert.equal(size.width, width);
+ }
+ function assertBubbleSizeDefault(comment) {
+ assertBubbleSize(comment, 80, 160);
+ }
+ test('Set Size While Visible', function() {
+ this.comment.setVisible(true);
+ var bubbleSizeSpy = sinon.spy(this.comment.bubble_, 'setBubbleSize');
+
+ assertBubbleSizeDefault(this.comment);
+ this.comment.setBubbleSize(100, 100);
+ assertBubbleSize(this.comment, 100, 100);
+ chai.assert(bubbleSizeSpy.calledOnce);
+
+ this.comment.setVisible(false);
+ assertBubbleSize(this.comment, 100, 100);
+
+ bubbleSizeSpy.restore();
+ });
+ test('Set Size While Invisible', function() {
+ assertBubbleSizeDefault(this.comment);
+ this.comment.setBubbleSize(100, 100);
+ assertBubbleSize(this.comment, 100, 100);
+
+ this.comment.setVisible(true);
+ assertBubbleSize(this.comment, 100, 100);
+ });
+ });
+});
diff --git a/tests/mocha/index.html b/tests/mocha/index.html
index d254233ad..e6a9ca37f 100644
--- a/tests/mocha/index.html
+++ b/tests/mocha/index.html
@@ -26,6 +26,7 @@
+
diff --git a/tests/mocha/xml_test.js b/tests/mocha/xml_test.js
index dacf89250..66ab5ae92 100644
--- a/tests/mocha/xml_test.js
+++ b/tests/mocha/xml_test.js
@@ -19,12 +19,6 @@
*/
suite('XML', function() {
- setup(function() {
- this.workspace = new Blockly.Workspace();
- });
- teardown(function() {
- this.workspace.dispose();
- });
var assertSimpleField = function(fieldDom, name, text) {
assertEquals(text, fieldDom.textContent);
assertEquals(name, fieldDom.getAttribute('name'));
@@ -38,7 +32,25 @@ suite('XML', function() {
assertEquals(id, fieldDom.getAttribute('id'));
assertEquals(text, fieldDom.textContent);
};
+ setup(function() {
+ Blockly.defineBlocksWithJsonArray([
+ {
+ "type": "empty_block",
+ "message0": "",
+ "args0": []
+ },
+ ]);
+ });
+ teardown(function() {
+ delete Blockly.Blocks['empty_block'];
+ });
suite('Serialization', function() {
+ setup(function() {
+ this.workspace = new Blockly.Workspace();
+ });
+ teardown(function() {
+ this.workspace.dispose();
+ });
suite('Fields', function() {
test('Angle', function() {
Blockly.defineBlocksWithJsonArray([{
@@ -201,7 +213,6 @@ suite('XML', function() {
var block = new Blockly.Block(this.workspace,
'field_label_serializable_test_block');
var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
- console.log(resultFieldDom);
assertSimpleField(resultFieldDom, 'LABEL', 'default');
delete Blockly.Blocks['field_label_serializable_test_block'];
});
@@ -289,8 +300,88 @@ suite('XML', function() {
});
});
});
+ suite('Comments', function() {
+ suite('Headless', function() {
+ setup(function() {
+ this.block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ ''
+ ), this.workspace);
+ });
+ test('Text', function() {
+ this.block.setCommentText('test text');
+ var xml = Blockly.Xml.blockToDom(this.block);
+ var commentXml = xml.firstChild;
+ chai.assert.equal(commentXml.tagName, 'comment');
+ chai.assert.equal(commentXml.innerHTML, 'test text');
+ });
+ test('No Text', function() {
+ var xml = Blockly.Xml.blockToDom(this.block);
+ chai.assert.isNull(xml.firstChild);
+ });
+ test('Empty Text', function() {
+ this.block.setCommentText('');
+ var xml = Blockly.Xml.blockToDom(this.block);
+ chai.assert.isNull(xml.firstChild);
+ });
+ });
+ suite('Rendered', function() {
+ setup(function() {
+ // Let the parent teardown dispose of it.
+ this.workspace = Blockly.inject('blocklyDiv', {comments: true});
+ this.block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ ''
+ ), this.workspace);
+ });
+ test('Text', function() {
+ this.block.setCommentText('test text');
+ var xml = Blockly.Xml.blockToDom(this.block);
+ var commentXml = xml.firstChild;
+ chai.assert.equal(commentXml.tagName, 'comment');
+ chai.assert.equal(commentXml.innerHTML, 'test text');
+ });
+ test('No Text', function() {
+ var xml = Blockly.Xml.blockToDom(this.block);
+ chai.assert.isNull(xml.firstChild);
+ });
+ test('Empty Text', function() {
+ this.block.setCommentText('');
+ var xml = Blockly.Xml.blockToDom(this.block);
+ chai.assert.isNull(xml.firstChild);
+ });
+ test('Size', function() {
+ this.block.setCommentText('test text');
+ this.block.getCommentIcon().setBubbleSize(100, 200);
+ var xml = Blockly.Xml.blockToDom(this.block);
+ var commentXml = xml.firstChild;
+ chai.assert.equal(commentXml.tagName, 'comment');
+ chai.assert.equal(commentXml.getAttribute('w'), 100);
+ chai.assert.equal(commentXml.getAttribute('h'), 200);
+ });
+ test('Pinned True', function() {
+ this.block.setCommentText('test text');
+ this.block.getCommentIcon().setVisible(true);
+ var xml = Blockly.Xml.blockToDom(this.block);
+ var commentXml = xml.firstChild;
+ chai.assert.equal(commentXml.tagName, 'comment');
+ chai.assert.equal(commentXml.getAttribute('pinned'), 'true');
+ });
+ test('Pinned False', function() {
+ this.block.setCommentText('test text');
+ var xml = Blockly.Xml.blockToDom(this.block);
+ var commentXml = xml.firstChild;
+ chai.assert.equal(commentXml.tagName, 'comment');
+ chai.assert.equal(commentXml.getAttribute('pinned'), 'false');
+ });
+ });
+ });
});
suite('Deserialization', function() {
+ setup(function() {
+ this.workspace = new Blockly.Workspace();
+ });
+ teardown(function() {
+ this.workspace.dispose();
+ });
suite('Dynamic Category Blocks', function() {
test('Untyped Variables', function() {
Blockly.defineBlocksWithJsonArray([{
@@ -424,5 +515,196 @@ suite('XML', function() {
}
});
});
+ suite('Comments', function() {
+ suite('Headless', function() {
+ test('Text', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ chai.assert.equal(block.getCommentText(), 'test text');
+ });
+ test('No Text', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' ' +
+ ''
+ ), this.workspace);
+ chai.assert.equal(block.getCommentText(), '');
+ });
+ test('Size', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ chai.assert.deepEqual(block.commentModel.size,
+ {width: 100, height: 200});
+ });
+ test('Pinned True', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ chai.assert.isTrue(block.commentModel.pinned);
+ });
+ test('Pinned False', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ chai.assert.isFalse(block.commentModel.pinned);
+ });
+ test('Pinned Undefined', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ chai.assert.isFalse(block.commentModel.pinned);
+ });
+ });
+ suite('Rendered', function() {
+ setup(function() {
+ // Let the parent teardown dispose of it.
+ this.workspace = Blockly.inject('blocklyDiv', {comments: true});
+ });
+ test('Text', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ chai.assert.equal(block.getCommentText(), 'test text');
+ chai.assert.isNotNull(block.getCommentIcon());
+ });
+ test('No Text', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' ' +
+ ''
+ ), this.workspace);
+ chai.assert.equal(block.getCommentText(), '');
+ chai.assert.isNotNull(block.getCommentIcon());
+ });
+ test('Size', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ chai.assert.deepEqual(block.commentModel.size,
+ {width: 100, height: 200});
+ chai.assert.isNotNull(block.getCommentIcon());
+ chai.assert.deepEqual(block.getCommentIcon().getBubbleSize(),
+ {width: 100, height: 200});
+ });
+ suite('Pinned', function() {
+ setup(function() {
+ this.clock = sinon.useFakeTimers();
+ });
+ teardown(function() {
+ this.clock.restore();
+ });
+ test('Pinned True', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ this.clock.tick(1);
+ chai.assert.isTrue(block.commentModel.pinned);
+ chai.assert.isNotNull(block.getCommentIcon());
+ chai.assert.isTrue(block.getCommentIcon().isVisible());
+ });
+ test('Pinned False', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ this.clock.tick(1);
+ chai.assert.isFalse(block.commentModel.pinned);
+ chai.assert.isNotNull(block.getCommentIcon());
+ chai.assert.isFalse(block.getCommentIcon().isVisible());
+ });
+ test('Pinned Undefined', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' test text' +
+ ''
+ ), this.workspace);
+ this.clock.tick(1);
+ chai.assert.isFalse(block.commentModel.pinned);
+ chai.assert.isNotNull(block.getCommentIcon());
+ chai.assert.isFalse(block.getCommentIcon().isVisible());
+ });
+ });
+ });
+ });
+ });
+ suite('Round Tripping', function() {
+ setup(function() {
+ var options = {
+ comments: true
+ };
+ this.renderedWorkspace = Blockly.inject('blocklyDiv', options);
+ this.headlessWorkspace = new Blockly.Workspace(options);
+ });
+ teardown(function() {
+ this.renderedWorkspace.dispose();
+ this.headlessWorkspace.dispose();
+ });
+ suite('Rendered -> XML -> Headless -> XML', function() {
+ setup(function() {
+ this.assertRoundTrip = function() {
+ var renderedXml = Blockly.Xml.workspaceToDom(this.renderedWorkspace);
+ Blockly.Xml.domToWorkspace(renderedXml, this.headlessWorkspace);
+ var headlessXml = Blockly.Xml.workspaceToDom(this.headlessWorkspace);
+
+ var renderedText = Blockly.Xml.domToText(renderedXml);
+ var headlessText = Blockly.Xml.domToText(headlessXml);
+
+ chai.assert.equal(headlessText, renderedText);
+ };
+ });
+ test('Comment', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ ''
+ ), this.renderedWorkspace);
+ block.setCommentText('test text');
+ block.getCommentIcon().setBubbleSize(100, 100);
+ block.getCommentIcon().setVisible(true);
+
+ this.assertRoundTrip();
+ });
+ });
+ suite('Headless -> XML -> Rendered -> XML', function() {
+ setup(function() {
+ this.assertRoundTrip = function() {
+ var headlessXml = Blockly.Xml.workspaceToDom(this.headlessWorkspace);
+ Blockly.Xml.domToWorkspace(headlessXml, this.renderedWorkspace);
+ var renderedXml = Blockly.Xml.workspaceToDom(this.renderedWorkspace);
+
+ var renderedText = Blockly.Xml.domToText(renderedXml);
+ var headlessText = Blockly.Xml.domToText(headlessXml);
+
+ chai.assert.equal(renderedText, headlessText);
+ };
+ });
+ test('Comment', function() {
+ var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ ''
+ ), this.headlessWorkspace);
+ block.setCommentText('test text');
+ block.commentModel.size = new Blockly.utils.Size(100, 100);
+ block.commentModel.pinned = true;
+
+ this.assertRoundTrip();
+ });
+ });
});
});