UNPKG

cloud-blocks

Version:

Cloud Blocks is a library for building scratch computing interfaces with Luxrobo MODI.

896 lines (831 loc) 24.7 kB
/** * @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); };