/** * @license * Visual Blocks Editor * * Copyright 2017 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. */ /** * @fileoverview Methods for rendering a workspace comment as SVG * @author fenichel@google.com (Rachel Fenichel) */ 'use strict'; goog.provide('Blockly.WorkspaceCommentSvg.render'); goog.require('Blockly.WorkspaceCommentSvg'); /** * Radius of the border around the comment. * @type {number} * @const * @private */ Blockly.WorkspaceCommentSvg.BORDER_WIDTH = 1; /** * Size of the resize icon. * @type {number} * @const * @private */ Blockly.WorkspaceCommentSvg.RESIZE_SIZE = 16; /** * Offset from the foreignobject edge to the textarea edge. * @type {number} * @const * @private */ Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET = 12; /** * The height of the comment top bar. * @package */ Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT = 32; /** * The size of the minimize arrow icon in the comment top bar. * @private */ Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE = 32; /** * The size of the delete icon in the comment top bar. * @private */ Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE = 32; /** * The inset for the top bar icons. * @private */ Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET = 0; /** * The bottom corner padding of the resize handle touch target. * Extends slightly outside the comment box. * @private */ Blockly.WorkspaceCommentSvg.RESIZE_CORNER_PAD = 4; /** * The top/side padding around resize handle touch target. * Extends about one extra "diagonal" above resize handle. * @private */ Blockly.WorkspaceCommentSvg.RESIZE_OUTER_PAD = 8; /** * Width that a minimized comment should have. * @private */ Blockly.WorkspaceCommentSvg.MINIMIZE_WIDTH = 200; /** * Returns a bounding box describing the dimensions of this comment. * @return {!{height: number, width: number}} Object with height and width * properties in workspace units. * @package */ Blockly.WorkspaceCommentSvg.prototype.getHeightWidth = function() { return { width: this.getWidth(), height: this.getHeight() }; }; /** * Renders the workspace comment. * @package */ Blockly.WorkspaceCommentSvg.prototype.render = function() { if (this.rendered_) { return; } var size = this.getHeightWidth(); // Add text area this.commentEditor_ = this.createEditor_(); this.svgGroup_.appendChild(this.commentEditor_); this.createCommentTopBar_(); this.svgRectTarget_ = Blockly.utils.createSvgElement('rect', { 'class': 'blocklyDraggable scratchCommentTarget', 'x': 0, 'y': Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT, 'rx': 4 * Blockly.WorkspaceCommentSvg.BORDER_WIDTH, 'ry': 4 * Blockly.WorkspaceCommentSvg.BORDER_WIDTH }); this.svgGroup_.appendChild(this.svgRectTarget_); // Add the resize icon this.addResizeDom_(); // Show / hide relevant things based on minimized state if (this.isMinimized()) { this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-up.svg'); this.commentEditor_.setAttribute('display', 'none'); this.resizeGroup_.setAttribute('display', 'none'); } else { this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-down.svg'); this.topBarLabel_.setAttribute('display', 'none'); } this.setSize(size.width, size.height); // Set the content this.textarea_.value = this.content_; this.rendered_ = true; if (this.resizeGroup_) { Blockly.bindEventWithChecks_( this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_); Blockly.bindEventWithChecks_( this.resizeGroup_, 'mouseup', this, this.resizeMouseUp_); } Blockly.bindEventWithChecks_( this.minimizeArrow_, 'mousedown', this, this.minimizeArrowMouseDown_, true); Blockly.bindEventWithChecks_( this.minimizeArrow_, 'mouseout', this, this.minimizeArrowMouseOut_, true); Blockly.bindEventWithChecks_( this.minimizeArrow_, 'mouseup', this, this.minimizeArrowMouseUp_, true); Blockly.bindEventWithChecks_( this.deleteIcon_, 'mousedown', this, this.deleteMouseDown_, true); Blockly.bindEventWithChecks_( this.deleteIcon_, 'mouseout', this, this.deleteMouseOut_, true); Blockly.bindEventWithChecks_( this.deleteIcon_, 'mouseup', this, this.deleteMouseUp_, true); }; /** * Create the text area for the comment. * @return {!Element} The top-level node of the editor. * @private */ Blockly.WorkspaceCommentSvg.prototype.createEditor_ = function() { this.foreignObject_ = Blockly.utils.createSvgElement( 'foreignObject', { 'x': Blockly.WorkspaceCommentSvg.BORDER_WIDTH, 'y': Blockly.WorkspaceCommentSvg.BORDER_WIDTH + Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT, 'class': 'scratchCommentForeignObject' }, null); var body = document.createElementNS(Blockly.HTML_NS, 'body'); body.setAttribute('xmlns', Blockly.HTML_NS); body.className = 'blocklyMinimalBody scratchCommentBody'; var textarea = document.createElementNS(Blockly.HTML_NS, 'textarea'); textarea.className = 'scratchCommentTextarea scratchCommentText'; textarea.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); textarea.setAttribute('maxlength', Blockly.WorkspaceComment.COMMENT_TEXT_LIMIT); textarea.setAttribute('placeholder', Blockly.Msg.WORKSPACE_COMMENT_DEFAULT_TEXT); body.appendChild(textarea); this.textarea_ = textarea; this.textarea_.style.margin = (Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET) + 'px'; this.foreignObject_.appendChild(body); Blockly.bindEventWithChecks_(textarea, 'mousedown', this, function(e) { e.stopPropagation(); // Propagation causes preventDefault from workspace handler }, 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) { this.setText(textarea.value); } }); this.labelText_ = this.getLabelText(); return this.foreignObject_; }; /** * Add the resize icon to the DOM * @private */ Blockly.WorkspaceCommentSvg.prototype.addResizeDom_ = function() { this.resizeGroup_ = Blockly.utils.createSvgElement( 'g', { 'class': this.RTL ? 'scratchCommentResizeSW' : 'scratchCommentResizeSE' }, this.svgGroup_); var resizeSize = Blockly.WorkspaceCommentSvg.RESIZE_SIZE; var outerPad = Blockly.ScratchBubble.RESIZE_OUTER_PAD; var cornerPad = Blockly.ScratchBubble.RESIZE_CORNER_PAD; // Build an (invisible) triangle that will catch resizes. It is padded on the // top/left by outerPad, and padded down/right by cornerPad. Blockly.utils.createSvgElement('polygon', { 'points': [ -outerPad, resizeSize + cornerPad, resizeSize + cornerPad, resizeSize + cornerPad, resizeSize + cornerPad, -outerPad ].join(' ') }, this.resizeGroup_); Blockly.utils.createSvgElement( 'line', { 'class': 'blocklyResizeLine', 'x1': resizeSize / 3, 'y1': resizeSize - 1, 'x2': resizeSize - 1, 'y2': resizeSize / 3 }, this.resizeGroup_); Blockly.utils.createSvgElement( 'line', { 'class': 'blocklyResizeLine', 'x1': resizeSize * 2 / 3, 'y1': resizeSize - 1, 'x2': resizeSize - 1, 'y2': resizeSize * 2 / 3 }, this.resizeGroup_); }; /** * Create the comment top bar and its contents. * @private */ Blockly.WorkspaceCommentSvg.prototype.createCommentTopBar_ = function() { this.svgHandleTarget_ = Blockly.utils.createSvgElement('rect', { 'class': 'blocklyDraggable scratchCommentTopBar', 'rx': Blockly.WorkspaceCommentSvg.BORDER_WIDTH, 'ry': Blockly.WorkspaceCommentSvg.BORDER_WIDTH, 'height': Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT }, this.svgGroup_); this.createTopBarIcons_(); this.createTopBarLabel_(); }; /** * Create the comment top bar label. This is the truncated comment text * that shows when comment is minimized. * @private */ Blockly.WorkspaceCommentSvg.prototype.createTopBarLabel_ = function() { this.topBarLabel_ = Blockly.utils.createSvgElement('text', { 'class': 'scratchCommentText', 'x': this.width_ / 2, 'y': (Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT / 2) + Blockly.WorkspaceCommentSvg.BORDER_WIDTH, 'text-anchor': 'middle', 'dominant-baseline': 'middle' }, this.svgGroup_); var labelTextNode = document.createTextNode(this.labelText_); this.topBarLabel_.appendChild(labelTextNode); }; /** * Create the minimize toggle and delete icons that in the comment top bar. * @private */ Blockly.WorkspaceCommentSvg.prototype.createTopBarIcons_ = function() { var topBarMiddleY = (Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT / 2) + Blockly.WorkspaceCommentSvg.BORDER_WIDTH; // Minimize Toggle Icon in Comment Top Bar var xInset = Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET; this.minimizeArrow_ = Blockly.utils.createSvgElement('image', { 'x': xInset, 'y': topBarMiddleY - Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE / 2, 'width': Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE, 'height': Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE }, this.svgGroup_); // Delete Icon in Comment Top Bar this.deleteIcon_ = Blockly.utils.createSvgElement('image', { 'x': xInset, 'y': topBarMiddleY - Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE / 2, 'width': Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE, 'height': Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE }, this.svgGroup_); this.deleteIcon_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'delete-x.svg'); }; /** * Handle a mouse-down on bubble's minimize icon. * @param {!Event} e Mouse down event. * @private */ Blockly.WorkspaceCommentSvg.prototype.minimizeArrowMouseDown_ = function(e) { // Set a property to indicate that this minimize arrow icon had a mouse down // event. This property will get reset if the mouse leaves the icon, or when // a mouse up event occurs on this icon. this.shouldToggleMinimize_ = true; e.stopPropagation(); }; /** * Handle a mouse-out on bubble's minimize icon. * @param {!Event} _e Mouse out event. * @private */ Blockly.WorkspaceCommentSvg.prototype.minimizeArrowMouseOut_ = function(_e) { // If the mouse leaves the minimize arrow icon, make sure the // shouldToggleMinimize_ property gets reset. this.shouldToggleMinimize_ = false; }; /** * Handle a mouse-up on bubble's minimize icon. * @param {!Event} e Mouse up event. * @private */ Blockly.WorkspaceCommentSvg.prototype.minimizeArrowMouseUp_ = function(e) { // First check if this is the icon that had a mouse down event on it and that // the mouse never left the icon. if (this.shouldToggleMinimize_) { this.shouldToggleMinimize = false; this.toggleMinimize_(); } e.stopPropagation(); }; /** * Handle a mouse-down on bubble's minimize icon. * @param {!Event} e Mouse down event. * @private */ Blockly.WorkspaceCommentSvg.prototype.deleteMouseDown_ = function(e) { // Set a property to indicate that this delete icon had a mouse down event. // This property will get reset if the mouse leaves the icon, or when // a mouse up event occurs on this icon. this.shouldDelete_ = true; e.stopPropagation(); }; /** * Handle a mouse-out on bubble's minimize icon. * @param {!Event} _e Mouse out event. * @private */ Blockly.WorkspaceCommentSvg.prototype.deleteMouseOut_ = function(_e) { // If the mouse leaves the delete icon, reset the shouldDelete_ property. this.shouldDelete_ = false; }; /** * Handle a mouse-up on bubble's delete icon. * @param {!Event} e Mouse up event. * @private */ Blockly.WorkspaceCommentSvg.prototype.deleteMouseUp_ = function(e) { // First check that this same icon had a mouse down event on it and that the // mouse never left the icon. if (this.shouldDelete_) { this.dispose(); } e.stopPropagation(); }; /** * Handle a mouse-down on comment's resize corner. * @param {!Event} e Mouse down event. * @private */ Blockly.WorkspaceCommentSvg.prototype.resizeMouseDown_ = function(e) { this.resizeStartSize_ = {width: this.width_, height: this.height_}; this.unbindDragEvents_(); this.workspace.setResizesEnabled(false); if (Blockly.utils.isRightButton(e)) { // No right-click. e.stopPropagation(); return; } // Left-click (or middle click) this.workspace.startDrag(e, new goog.math.Coordinate( this.workspace.RTL ? -this.width_ : this.width_, this.height_)); this.onMouseUpWrapper_ = Blockly.bindEventWithChecks_( document, 'mouseup', this, this.resizeMouseUp_); this.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_( document, 'mousemove', this, this.resizeMouseMove_); Blockly.hideChaff(); // This event has been handled. No need to bubble up to the document. e.stopPropagation(); }; /** * Set the apperance of the workspace comment bubble to the minimized or full size * appearance. In the minimized state, the comment should only have the top bar * displayed, with the minimize icon swapped to the minimized state, and * truncated comment text is shown in the middle of the top bar. There should be * no resize handle when the workspace comment is in its minimized state. * @param {boolean} minimize Whether the bubble should be minimized * @param {?string} labelText Optional label text for the comment top bar * when it is minimized. * @private */ Blockly.WorkspaceCommentSvg.prototype.setRenderedMinimizeState_ = function(minimize, labelText) { if (minimize) { // Change minimize icon this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-up.svg'); // Hide text area this.commentEditor_.setAttribute('display', 'none'); // Hide resize handle if it exists if (this.resizeGroup_) { this.resizeGroup_.setAttribute('display', 'none'); } if (labelText && this.labelText_ != labelText) { // Update label and display // TODO is there a better way to do this? this.topBarLabel_.textContent = labelText; } Blockly.utils.removeAttribute(this.topBarLabel_, 'display'); } else { // Change minimize icon this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-down.svg'); // Hide label this.topBarLabel_.setAttribute('display', 'none'); // Show text area Blockly.utils.removeAttribute(this.commentEditor_, 'display'); // Display resize handle if it exists if (this.resizeGroup_) { Blockly.utils.removeAttribute(this.resizeGroup_, 'display'); } } }; /** * Stop binding to the global mouseup and mousemove events. * @private */ Blockly.WorkspaceCommentSvg.prototype.unbindDragEvents_ = function() { if (this.onMouseUpWrapper_) { Blockly.unbindEvent_(this.onMouseUpWrapper_); this.onMouseUpWrapper_ = null; } if (this.onMouseMoveWrapper_) { Blockly.unbindEvent_(this.onMouseMoveWrapper_); this.onMouseMoveWrapper_ = null; } }; /* * Handle a mouse-up event while dragging a comment's border or resize handle. * @param {!Event} e Mouse up event. * @private */ Blockly.WorkspaceCommentSvg.prototype.resizeMouseUp_ = function(/*e*/) { Blockly.Touch.clearTouchIdentifier(); this.unbindDragEvents_(); var oldHW = this.resizeStartSize_; this.resizeStartSize_ = null; if (this.width_ == oldHW.width && this.height_ == oldHW.height) { return; } // Fire a change event for the new width/height after // resize mouse up Blockly.Events.fire(new Blockly.Events.CommentChange( this, {width: oldHW.width , height: oldHW.height}, {width: this.width_, height: this.height_})); this.workspace.setResizesEnabled(true); }; /** * Resize this comment to follow the mouse. * @param {!Event} e Mouse move event. * @private */ Blockly.WorkspaceCommentSvg.prototype.resizeMouseMove_ = function(e) { this.autoLayout_ = false; var newXY = this.workspace.moveDrag(e); // The call to setSize below emits a CommentChange event, // but we don't want multiple CommentChange events to be // emitted while the user is still in the process of resizing // the comment, so disable events here. The event is emitted in // resizeMouseUp_. var disabled = false; if (Blockly.Events.isEnabled()) { Blockly.Events.disable(); disabled = true; } this.setSize(this.RTL ? -newXY.x : newXY.x, newXY.y); if (disabled) { Blockly.Events.enable(); } }; /** * Callback function triggered when the comment has resized. * Resize the text area accordingly. * @private */ Blockly.WorkspaceCommentSvg.prototype.resizeComment_ = function() { var doubleBorderWidth = 2 * Blockly.WorkspaceCommentSvg.BORDER_WIDTH; var topOffset = Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT; var textOffset = Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET * 2; this.foreignObject_.setAttribute('width', this.width_ - doubleBorderWidth); this.foreignObject_.setAttribute('height', this.height_ - doubleBorderWidth - topOffset); if (this.RTL) { this.foreignObject_.setAttribute('x', -this.width_); } this.textarea_.style.width = (this.width_ - textOffset) + 'px'; this.textarea_.style.height = (this.height_ - doubleBorderWidth - textOffset - topOffset) + 'px'; }; /** * Set size * @param {number} width width of the container * @param {number} height height of the container * @package */ Blockly.WorkspaceCommentSvg.prototype.setSize = function(width, height) { var oldWidth = this.width_; var oldHeight = this.height_; var doubleBorderWidth = 2 * Blockly.WorkspaceCommentSvg.BORDER_WIDTH; if (this.isMinimized_) { width = Blockly.WorkspaceCommentSvg.MINIMIZE_WIDTH; height = Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT; } else { // Minimum size of a 'full size' (not minimized) comment. width = Math.max(width, doubleBorderWidth + 50); height = Math.max(height, doubleBorderWidth + 20 + Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT); // Note we are only updating this.width_ or this.height_ here // and not in the case above, because when we're minimizing a comment, // we want to keep track of the width/height of the maximized comment this.width_ = width; this.height_ = height; Blockly.Events.fire(new Blockly.Events.CommentChange(this, {width: oldWidth, height: oldHeight}, {width: this.width_, height: this.height_})); } this.svgRect_.setAttribute('width', width); this.svgRect_.setAttribute('height', height); this.svgRectTarget_.setAttribute('width', width); this.svgRectTarget_.setAttribute('height', height - Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT); this.svgHandleTarget_.setAttribute('width', width); this.svgHandleTarget_.setAttribute('height', Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT); if (this.RTL) { this.minimizeArrow_.setAttribute('x', width - (Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE) - Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET); this.deleteIcon_.setAttribute('x', (-width + Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET)); this.svgRect_.setAttribute('transform', 'scale(-1 1)'); this.svgHandleTarget_.setAttribute('transform', 'scale(-1 1)'); this.svgHandleTarget_.setAttribute('transform', 'translate(' + -width + ', 1)'); this.minimizeArrow_.setAttribute('transform', 'translate(' + -width + ', 1)'); this.deleteIcon_.setAttribute('tranform', 'translate(' + -width + ', 1)'); this.svgRectTarget_.setAttribute('transform', 'translate(' + -width + ', 1)'); this.topBarLabel_.setAttribute('transform', 'translate(' + -width + ', 1)'); } else { this.deleteIcon_.setAttribute('x', width - Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE - Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET); } var resizeSize = Blockly.WorkspaceCommentSvg.RESIZE_SIZE; if (this.resizeGroup_) { if (this.RTL) { // Mirror the resize group. this.resizeGroup_.setAttribute('transform', 'translate(' + (-width + doubleBorderWidth + resizeSize) + ',' + (height - doubleBorderWidth - resizeSize) + ') scale(-1 1)'); } else { this.resizeGroup_.setAttribute('transform', 'translate(' + (width - doubleBorderWidth - resizeSize) + ',' + (height - doubleBorderWidth - resizeSize) + ')'); } } if (this.isMinimized_) { this.topBarLabel_.setAttribute('x', width / 2); this.topBarLabel_.setAttribute('y', height / 2); } // Allow the contents to resize. this.resizeComment_(); }; /** * Toggle the minimization state of this comment. * @private */ Blockly.WorkspaceComment.prototype.toggleMinimize_ = function() { this.setMinimized(!this.isMinimized_); }; /** * Set the minimized state for this comment. If the comment is rendered, * change the appearance of the comment accordingly. * @param {boolean} minimize Whether the comment should be minimized * @package */ Blockly.WorkspaceComment.prototype.setMinimized = function(minimize) { if (this.isMinimized_ == minimize) { return; } Blockly.Events.fire(new Blockly.Events.CommentChange(this, {minimized: this.isMinimized_}, {minimized: minimize})); this.isMinimized_ = minimize; if (minimize) { if (this.rendered_) { this.setRenderedMinimizeState_(true, this.getLabelText()); } this.setSize(Blockly.WorkspaceCommentSvg.MINIMIZE_WIDTH, Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT); } else { if (this.rendered_) { this.setRenderedMinimizeState_(false); } this.setText(this.content_); this.setSize(this.width_, this.height_); } }; /** * Dispose of any rendered comment components. * @private */ Blockly.WorkspaceCommentSvg.prototype.disposeInternal_ = function() { this.textarea_ = null; this.foreignObject_ = null; this.svgRect_ = null; this.svgRectTarget_ = null; this.svgHandleTarget_ = null; }; /** * Set the focus on the text area. * @package */ Blockly.WorkspaceCommentSvg.prototype.setFocus = function() { var comment = this; this.focused_ = true; comment.textarea_.focus(); // Defer CSS changes. setTimeout(function() { comment.addFocus(); Blockly.utils.addClass( comment.svgRectTarget_, 'scratchCommentTargetFocused'); Blockly.utils.addClass( comment.svgHandleTarget_, 'scratchCommentHandleTargetFocused'); }, 0); }; /** * Remove focus from the text area. * @package */ Blockly.WorkspaceCommentSvg.prototype.blurFocus = function() { var comment = this; this.focused_ = false; comment.textarea_.blur(); // Defer CSS changes. setTimeout(function() { if (comment.svgGroup_) { // Could have been deleted in the meantime comment.removeFocus(); Blockly.utils.removeClass( comment.svgRectTarget_, 'scratchCommentTargetFocused'); Blockly.utils.removeClass( comment.svgHandleTarget_, 'scratchCommentHandleTargetFocused'); } }, 0); };