/** * @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'); /** * Size of the resize icon. * @type {number} * @const * @private */ Blockly.WorkspaceCommentSvg.RESIZE_SIZE = 8; /** * Radius of the border around the comment. * @type {number} * @const * @private */ Blockly.WorkspaceCommentSvg.BORDER_RADIUS = 3; /** * Offset from the foreignobject edge to the textarea edge. * @type {number} * @const * @private */ Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET = 2; /** * Offset from the top to make room for a top bar. * @type {number} * @const * @private */ Blockly.WorkspaceCommentSvg.TOP_OFFSET = 10; /** * 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.createEditor_(); this.svgGroup_.appendChild(this.foreignObject_); this.svgHandleTarget_ = Blockly.utils.createSvgElement('rect', { 'class': 'blocklyCommentHandleTarget', 'x': 0, 'y': 0 }); this.svgGroup_.appendChild(this.svgHandleTarget_); this.svgRectTarget_ = Blockly.utils.createSvgElement('rect', { 'class': 'blocklyCommentTarget', 'x': 0, 'y': 0, 'rx': Blockly.WorkspaceCommentSvg.BORDER_RADIUS, 'ry': Blockly.WorkspaceCommentSvg.BORDER_RADIUS }); this.svgGroup_.appendChild(this.svgRectTarget_); // Add the resize icon this.addResizeDom_(); if (this.isDeletable()) { // Add the delete icon this.addDeleteDom_(); } 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_); } if (this.isDeletable()) { Blockly.bindEventWithChecks_( this.deleteGroup_, 'mousedown', this, this.deleteMouseDown_); Blockly.bindEventWithChecks_( this.deleteGroup_, 'mouseout', this, this.deleteMouseOut_); Blockly.bindEventWithChecks_( this.deleteGroup_, 'mouseup', this, this.deleteMouseUp_); } }; /** * Create the text area for the comment. * @return {!Element} The top-level node of the editor. * @private */ Blockly.WorkspaceCommentSvg.prototype.createEditor_ = function() { /* Create the editor. Here's the markup that will be generated: <foreignObject class="blocklyCommentForeignObject" x="0" y="10" width="164" height="164"> <body xmlns="http://www.w3.org/1999/xhtml" class="blocklyMinimalBody"> <textarea xmlns="http://www.w3.org/1999/xhtml" class="blocklyCommentTextarea" style="height: 164px; width: 164px;"></textarea> </body> </foreignObject> */ this.foreignObject_ = Blockly.utils.createSvgElement( 'foreignObject', { 'x': 0, 'y': Blockly.WorkspaceCommentSvg.TOP_OFFSET, 'class': 'blocklyCommentForeignObject' }, null); var body = document.createElementNS(Blockly.HTML_NS, 'body'); body.setAttribute('xmlns', Blockly.HTML_NS); body.className = 'blocklyMinimalBody'; var textarea = document.createElementNS(Blockly.HTML_NS, 'textarea'); textarea.className = 'blocklyCommentTextarea'; textarea.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); body.appendChild(textarea); this.textarea_ = textarea; this.foreignObject_.appendChild(body); // Don't zoom with mousewheel. Blockly.bindEventWithChecks_(textarea, 'wheel', this, function(e) { e.stopPropagation(); }); Blockly.bindEventWithChecks_(textarea, 'change', this, function( /* eslint-disable no-unused-vars */ e /* eslint-enable no-unused-vars */) { this.setText(textarea.value); }); 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 ? 'blocklyResizeSW' : 'blocklyResizeSE' }, this.svgGroup_); var resizeSize = Blockly.WorkspaceCommentSvg.RESIZE_SIZE; Blockly.utils.createSvgElement( 'polygon', {'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())}, 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_); }; /** * Add the delete icon to the DOM * @private */ Blockly.WorkspaceCommentSvg.prototype.addDeleteDom_ = function() { this.deleteGroup_ = Blockly.utils.createSvgElement( 'g', { 'class': 'blocklyCommentDeleteIcon' }, this.svgGroup_); this.deleteIconBorder_ = Blockly.utils.createSvgElement('circle', { 'class': 'blocklyDeleteIconShape', 'r': '7', 'cx': '7.5', 'cy': '7.5' }, this.deleteGroup_); // x icon. Blockly.utils.createSvgElement( 'line', { 'x1': '5', 'y1': '10', 'x2': '10', 'y2': '5', 'stroke': '#fff', 'stroke-width': '2' }, this.deleteGroup_); Blockly.utils.createSvgElement( 'line', { 'x1': '5', 'y1': '5', 'x2': '10', 'y2': '10', 'stroke': '#fff', 'stroke-width': '2' }, this.deleteGroup_); }; /** * 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_(); 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(); }; /** * Handle a mouse-down on comment's delete icon. * @param {!Event} e Mouse down event. * @private */ Blockly.WorkspaceCommentSvg.prototype.deleteMouseDown_ = function(e) { // highlight the delete icon Blockly.utils.addClass( /** @type {!Element} */ (this.deleteIconBorder_), 'blocklyDeleteIconHighlighted'); // This event has been handled. No need to bubble up to the document. e.stopPropagation(); }; /** * Handle a mouse-out on comment's delete icon. * @param {!Event} e Mouse out event. * @private */ Blockly.WorkspaceCommentSvg.prototype.deleteMouseOut_ = function(/*e*/) { // restore highlight on the delete icon Blockly.utils.removeClass( /** @type {!Element} */ (this.deleteIconBorder_), 'blocklyDeleteIconHighlighted'); }; /** * Handle a mouse-up on comment's delete icon. * @param {!Event} e Mouse up event. * @private */ Blockly.WorkspaceCommentSvg.prototype.deleteMouseUp_ = function(e) { // Delete this comment this.dispose(true, true); // This event has been handled. No need to bubble up to the document. e.stopPropagation(); }; /** * 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_})); }; /** * 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 size = this.getHeightWidth(); var topOffset = Blockly.WorkspaceCommentSvg.TOP_OFFSET; var textOffset = Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET * 2; this.foreignObject_.setAttribute('width', size.width); this.foreignObject_.setAttribute('height', size.height - topOffset); if (this.RTL) { this.foreignObject_.setAttribute('x', -size.width); } this.textarea_.style.width = (size.width - textOffset) + 'px'; this.textarea_.style.height = (size.height - 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_; // Minimum size of a comment. width = Math.max(width, 45); height = Math.max(height, 20 + Blockly.WorkspaceCommentSvg.TOP_OFFSET); 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); this.svgHandleTarget_.setAttribute('width', width); this.svgHandleTarget_.setAttribute('height', Blockly.WorkspaceCommentSvg.TOP_OFFSET); if (this.RTL) { this.svgRect_.setAttribute('transform', 'scale(-1 1)'); this.svgRectTarget_.setAttribute('transform', 'scale(-1 1)'); } var resizeSize = Blockly.WorkspaceCommentSvg.RESIZE_SIZE; if (this.resizeGroup_) { if (this.RTL) { // Mirror the resize group. this.resizeGroup_.setAttribute('transform', 'translate(' + (-width + resizeSize) + ',' + (height - resizeSize) + ') scale(-1 1)'); this.deleteGroup_.setAttribute('transform', 'translate(' + (-width + resizeSize) + ',' + (-resizeSize) + ') scale(-1 1)'); } else { this.resizeGroup_.setAttribute('transform', 'translate(' + (width - resizeSize) + ',' + (height - resizeSize) + ')'); this.deleteGroup_.setAttribute('transform', 'translate(' + (width - resizeSize) + ',' + (-resizeSize) + ')'); } } // Allow the contents to resize. this.resizeComment_(); }; /** * Dispose of any rendered comment components. * @private */ Blockly.WorkspaceCommentSvg.prototype.disposeInternal_ = function() { this.textarea_ = null; this.foreignObject_ = 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; // Defer CSS changes. setTimeout(function() { comment.textarea_.focus(); comment.addFocus(); Blockly.utils.addClass( comment.svgRectTarget_, 'blocklyCommentTargetFocused'); Blockly.utils.addClass( comment.svgHandleTarget_, 'blocklyCommentHandleTargetFocused'); }, 0); }; /** * Remove focus from the text area. * @package */ Blockly.WorkspaceCommentSvg.prototype.blurFocus = function() { var comment = this; this.focused_ = false; // Defer CSS changes. // TODO (github.com/google/blockly/issues/1848): Fix warnings when the comment // has already been deleted. setTimeout(function() { comment.textarea_.blur(); comment.removeFocus(); Blockly.utils.removeClass( comment.svgRectTarget_, 'blocklyCommentTargetFocused'); Blockly.utils.removeClass( comment.svgHandleTarget_, 'blocklyCommentHandleTargetFocused'); }, 0); };