From 9e5df6216a28a21ea8c8f423afe1bc50195ab5d7 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Fri, 20 Sep 2019 13:16:07 -0700 Subject: [PATCH] Fixed comment ownership. (#2923) * Moved comment icons to use a model-based system. The block holds the model of the comment, and the comment icon holds a reference to it. * Reorganized the setVisible function. * Changed how xml.js serializes and deserializes comments. --- core/block.js | 41 ++++- core/block_svg.js | 91 ++++++----- core/bubble.js | 15 +- core/comment.js | 252 +++++++++++++++++++----------- core/icon.js | 8 +- core/mutator.js | 4 +- core/warning.js | 70 +++++---- core/xml.js | 42 ++--- generators/dart.js | 3 +- generators/javascript.js | 4 +- generators/lua.js | 3 +- generators/php.js | 3 +- generators/python.js | 4 +- tests/mocha/block_test.js | 128 +++++++++++++++- tests/mocha/comment_test.js | 144 ++++++++++++++++++ tests/mocha/index.html | 1 + tests/mocha/xml_test.js | 296 +++++++++++++++++++++++++++++++++++- 17 files changed, 890 insertions(+), 219 deletions(-) create mode 100644 tests/mocha/comment_test.js 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(); + }); + }); }); });