/** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Object representing a code comment on a rendered workspace. * @author fenichel@google.com (Rachel Fenichel) */ 'use strict'; goog.module('Blockly.WorkspaceCommentSvg'); goog.module.declareLegacyNamespace(); /* eslint-disable-next-line no-unused-vars */ const BlockDragSurfaceSvg = goog.requireType('Blockly.BlockDragSurfaceSvg'); const Blockly = goog.require('Blockly'); const ContextMenu = goog.require('Blockly.ContextMenu'); const Coordinate = goog.require('Blockly.utils.Coordinate'); const Css = goog.require('Blockly.Css'); const Events = goog.require('Blockly.Events'); /* eslint-disable-next-line no-unused-vars */ const IBoundedElement = goog.requireType('Blockly.IBoundedElement'); /* eslint-disable-next-line no-unused-vars */ const IBubble = goog.requireType('Blockly.IBubble'); /* eslint-disable-next-line no-unused-vars */ const ICopyable = goog.requireType('Blockly.ICopyable'); const Rect = goog.require('Blockly.utils.Rect'); const Svg = goog.require('Blockly.utils.Svg'); const Touch = goog.require('Blockly.Touch'); /* eslint-disable-next-line no-unused-vars */ const WorkspaceSvg = goog.requireType('Blockly.WorkspaceSvg'); const WorkspaceComment = goog.require('Blockly.WorkspaceComment'); const browserEvents = goog.require('Blockly.browserEvents'); const dom = goog.require('Blockly.utils.dom'); const object = goog.require('Blockly.utils.object'); const utils = goog.require('Blockly.utils'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.CommentCreate'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.CommentDelete'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.CommentMove'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.Selected'); /** * Size of the resize icon. * @type {number} * @const */ const RESIZE_SIZE = 8; /** * Radius of the border around the comment. * @type {number} * @const */ const BORDER_RADIUS = 3; /** * Offset from the foreignobject edge to the textarea edge. * @type {number} * @const */ const TEXTAREA_OFFSET = 2; /** * Class for a workspace comment's SVG representation. * @param {!WorkspaceSvg} workspace The block's workspace. * @param {string} content The content of this workspace comment. * @param {number} height Height of the comment. * @param {number} width Width of the comment. * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise * create a new ID. * @extends {WorkspaceComment} * @implements {IBoundedElement} * @implements {IBubble} * @implements {ICopyable} * @constructor */ const WorkspaceCommentSvg = function( workspace, content, height, width, opt_id) { /** * @type {!WorkspaceSvg} */ this.workspace; /** * Mouse up event data. * @type {?browserEvents.Data} * @private */ this.onMouseUpWrapper_ = null; /** * Mouse move event data. * @type {?browserEvents.Data} * @private */ this.onMouseMoveWrapper_ = null; // Create core elements for the block. /** * @type {!SVGElement} * @private */ this.svgGroup_ = dom.createSvgElement(Svg.G, {'class': 'blocklyComment'}, null); this.svgGroup_.translate_ = ''; this.svgRect_ = dom.createSvgElement(Svg.RECT, { 'class': 'blocklyCommentRect', 'x': 0, 'y': 0, 'rx': BORDER_RADIUS, 'ry': BORDER_RADIUS }); this.svgGroup_.appendChild(this.svgRect_); /** * Whether the comment is rendered onscreen and is a part of the DOM. * @type {boolean} * @private */ this.rendered_ = false; /** * Whether to move the comment to the drag surface when it is dragged. * True if it should move, false if it should be translated directly. * @type {boolean} * @private */ this.useDragSurface_ = utils.is3dSupported() && !!workspace.getBlockDragSurface(); WorkspaceCommentSvg.superClass_.constructor.call( this, workspace, content, height, width, opt_id); this.render(); }; object.inherits(WorkspaceCommentSvg, WorkspaceComment); /** * The width and height to use to size a workspace comment when it is first * added, before it has been edited by the user. * @type {number} * @package */ WorkspaceCommentSvg.DEFAULT_SIZE = 100; /** * Offset from the top to make room for a top bar. * @type {number} * @const * @private */ WorkspaceCommentSvg.TOP_OFFSET = 10; /** * Dispose of this comment. * @package */ WorkspaceCommentSvg.prototype.dispose = function() { if (this.disposed_) { return; } // If this comment is being dragged, unlink the mouse events. if (Blockly.selected == this) { this.unselect(); this.workspace.cancelCurrentGesture(); } if (Events.isEnabled()) { Events.fire(new (Events.get(Events.COMMENT_DELETE))(this)); } dom.removeNode(this.svgGroup_); // Dispose of any rendered components this.disposeInternal_(); Events.disable(); WorkspaceCommentSvg.superClass_.dispose.call(this); Events.enable(); }; /** * Create and initialize the SVG representation of a workspace comment. * May be called more than once. * * @param {boolean=} opt_noSelect Text inside text area will be selected if * false * * @package */ WorkspaceCommentSvg.prototype.initSvg = function(opt_noSelect) { if (!this.workspace.rendered) { throw TypeError('Workspace is headless.'); } if (!this.workspace.options.readOnly && !this.eventsInit_) { browserEvents.conditionalBind( this.svgRectTarget_, 'mousedown', this, this.pathMouseDown_); browserEvents.conditionalBind( this.svgHandleTarget_, 'mousedown', this, this.pathMouseDown_); } this.eventsInit_ = true; this.updateMovable(); if (!this.getSvgRoot().parentNode) { this.workspace.getBubbleCanvas().appendChild(this.getSvgRoot()); } if (!opt_noSelect && this.textarea_) { this.textarea_.select(); } }; /** * Handle a mouse-down on an SVG comment. * @param {!Event} e Mouse down event or touch start event. * @private */ WorkspaceCommentSvg.prototype.pathMouseDown_ = function(e) { const gesture = this.workspace.getGesture(e); if (gesture) { gesture.handleBubbleStart(e, this); } }; /** * Show the context menu for this workspace comment. * @param {!Event} e Mouse event. * @package */ WorkspaceCommentSvg.prototype.showContextMenu = function(e) { if (this.workspace.options.readOnly) { return; } // Save the current workspace comment in a variable for use in closures. const comment = this; const menuOptions = []; if (this.isDeletable() && this.isMovable()) { menuOptions.push(ContextMenu.commentDuplicateOption(comment)); menuOptions.push(ContextMenu.commentDeleteOption(comment)); } ContextMenu.show(e, menuOptions, this.RTL); }; /** * Select this comment. Highlight it visually. * @package */ WorkspaceCommentSvg.prototype.select = function() { if (Blockly.selected == this) { return; } let oldId = null; if (Blockly.selected) { oldId = Blockly.selected.id; // Unselect any previously selected block. Events.disable(); try { Blockly.selected.unselect(); } finally { Events.enable(); } } const event = new (Events.get(Events.SELECTED))(oldId, this.id, this.workspace.id); Events.fire(event); Blockly.selected = this; this.addSelect(); }; /** * Unselect this comment. Remove its highlighting. * @package */ WorkspaceCommentSvg.prototype.unselect = function() { if (Blockly.selected != this) { return; } const event = new (Events.get(Events.SELECTED))(this.id, null, this.workspace.id); Events.fire(event); Blockly.selected = null; this.removeSelect(); this.blurFocus(); }; /** * Select this comment. Highlight it visually. * @package */ WorkspaceCommentSvg.prototype.addSelect = function() { dom.addClass( /** @type {!Element} */ (this.svgGroup_), 'blocklySelected'); this.setFocus(); }; /** * Unselect this comment. Remove its highlighting. * @package */ WorkspaceCommentSvg.prototype.removeSelect = function() { dom.removeClass( /** @type {!Element} */ (this.svgGroup_), 'blocklySelected'); this.blurFocus(); }; /** * Focus this comment. Highlight it visually. * @package */ WorkspaceCommentSvg.prototype.addFocus = function() { dom.addClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyFocused'); }; /** * Unfocus this comment. Remove its highlighting. * @package */ WorkspaceCommentSvg.prototype.removeFocus = function() { dom.removeClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyFocused'); }; /** * Return the coordinates of the top-left corner of this comment relative to * the drawing surface's origin (0,0), in workspace units. * If the comment is on the workspace, (0, 0) is the origin of the workspace * coordinate system. * This does not change with workspace scale. * @return {!Coordinate} Object with .x and .y properties in * workspace coordinates. * @package */ WorkspaceCommentSvg.prototype.getRelativeToSurfaceXY = function() { let x = 0; let y = 0; const dragSurfaceGroup = this.useDragSurface_ ? this.workspace.getBlockDragSurface().getGroup() : null; let element = this.getSvgRoot(); if (element) { do { // Loop through this comment and every parent. const xy = utils.getRelativeXY(/** @type {!Element} */ (element)); x += xy.x; y += xy.y; // If this element is the current element on the drag surface, include // the translation of the drag surface itself. if (this.useDragSurface_ && this.workspace.getBlockDragSurface().getCurrentBlock() == element) { const surfaceTranslation = this.workspace.getBlockDragSurface().getSurfaceTranslation(); x += surfaceTranslation.x; y += surfaceTranslation.y; } element = element.parentNode; } while (element && element != this.workspace.getBubbleCanvas() && element != dragSurfaceGroup); } this.xy_ = new Coordinate(x, y); return this.xy_; }; /** * Move a comment by a relative offset. * @param {number} dx Horizontal offset, in workspace units. * @param {number} dy Vertical offset, in workspace units. * @package */ WorkspaceCommentSvg.prototype.moveBy = function(dx, dy) { const event = new (Events.get(Events.COMMENT_MOVE))(this); // TODO: Do I need to look up the relative to surface XY position here? const xy = this.getRelativeToSurfaceXY(); this.translate(xy.x + dx, xy.y + dy); this.xy_ = new Coordinate(xy.x + dx, xy.y + dy); event.recordNew(); Events.fire(event); this.workspace.resizeContents(); }; /** * Transforms a comment by setting the translation on the transform attribute * of the block's SVG. * @param {number} x The x coordinate of the translation in workspace units. * @param {number} y The y coordinate of the translation in workspace units. * @package */ WorkspaceCommentSvg.prototype.translate = function(x, y) { this.xy_ = new Coordinate(x, y); this.getSvgRoot().setAttribute('transform', 'translate(' + x + ',' + y + ')'); }; /** * Move this comment to its workspace's drag surface, accounting for * positioning. Generally should be called at the same time as * setDragging(true). Does nothing if useDragSurface_ is false. * @package */ WorkspaceCommentSvg.prototype.moveToDragSurface = function() { if (!this.useDragSurface_) { return; } // The translation for drag surface blocks, // is equal to the current relative-to-surface position, // to keep the position in sync as it move on/off the surface. // This is in workspace coordinates. const xy = this.getRelativeToSurfaceXY(); this.clearTransformAttributes_(); this.workspace.getBlockDragSurface().translateSurface(xy.x, xy.y); // Execute the move on the top-level SVG component this.workspace.getBlockDragSurface().setBlocksAndShow(this.getSvgRoot()); }; /** * Move this comment during a drag, taking into account whether we are using a * drag surface to translate blocks. * @param {BlockDragSurfaceSvg} dragSurface The surface that carries * rendered items during a drag, or null if no drag surface is in use. * @param {!Coordinate} newLoc The location to translate to, in * workspace coordinates. * @package */ WorkspaceCommentSvg.prototype.moveDuringDrag = function(dragSurface, newLoc) { if (dragSurface) { dragSurface.translateSurface(newLoc.x, newLoc.y); } else { this.svgGroup_.translate_ = 'translate(' + newLoc.x + ',' + newLoc.y + ')'; this.svgGroup_.setAttribute( 'transform', this.svgGroup_.translate_ + this.svgGroup_.skew_); } }; /** * Move the bubble group to the specified location in workspace coordinates. * @param {number} x The x position to move to. * @param {number} y The y position to move to. * @package */ WorkspaceCommentSvg.prototype.moveTo = function(x, y) { this.translate(x, y); }; /** * Clear the comment of transform="..." attributes. * Used when the comment is switching from 3d to 2d transform or vice versa. * @private */ WorkspaceCommentSvg.prototype.clearTransformAttributes_ = function() { this.getSvgRoot().removeAttribute('transform'); }; /** * Returns the coordinates of a bounding box describing the dimensions of this * comment. * Coordinate system: workspace coordinates. * @return {!Rect} Object with coordinates of the bounding box. * @package */ WorkspaceCommentSvg.prototype.getBoundingRectangle = function() { const blockXY = this.getRelativeToSurfaceXY(); const commentBounds = this.getHeightWidth(); const top = blockXY.y; const bottom = blockXY.y + commentBounds.height; let left; let right; if (this.RTL) { left = blockXY.x - commentBounds.width; // Add the width of the tab/puzzle piece knob to the x coordinate // since X is the corner of the rectangle, not the whole puzzle piece. right = blockXY.x; } else { // Subtract the width of the tab/puzzle piece knob to the x coordinate // since X is the corner of the rectangle, not the whole puzzle piece. left = blockXY.x; right = blockXY.x + commentBounds.width; } return new Rect(top, bottom, left, right); }; /** * Add or remove the UI indicating if this comment is movable or not. * @package */ WorkspaceCommentSvg.prototype.updateMovable = function() { if (this.isMovable()) { dom.addClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable'); } else { dom.removeClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable'); } }; /** * Set whether this comment is movable or not. * @param {boolean} movable True if movable. * @package */ WorkspaceCommentSvg.prototype.setMovable = function(movable) { WorkspaceCommentSvg.superClass_.setMovable.call(this, movable); this.updateMovable(); }; /** * Set whether this comment is editable or not. * @param {boolean} editable True if editable. */ WorkspaceCommentSvg.prototype.setEditable = function(editable) { WorkspaceCommentSvg.superClass_.setEditable.call(this, editable); if (this.textarea_) { this.textarea_.readOnly = !editable; } }; /** * Recursively adds or removes the dragging class to this node and its children. * @param {boolean} adding True if adding, false if removing. * @package */ WorkspaceCommentSvg.prototype.setDragging = function(adding) { if (adding) { const group = this.getSvgRoot(); group.translate_ = ''; group.skew_ = ''; dom.addClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyDragging'); } else { dom.removeClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyDragging'); } }; /** * Return the root node of the SVG or null if none exists. * @return {!SVGElement} The root SVG node (probably a group). * @package */ WorkspaceCommentSvg.prototype.getSvgRoot = function() { return this.svgGroup_; }; /** * Returns this comment's text. * @return {string} Comment text. * @package */ WorkspaceCommentSvg.prototype.getContent = function() { return this.textarea_ ? this.textarea_.value : this.content_; }; /** * Set this comment's content. * @param {string} content Comment content. * @package */ WorkspaceCommentSvg.prototype.setContent = function(content) { WorkspaceCommentSvg.superClass_.setContent.call(this, content); if (this.textarea_) { this.textarea_.value = content; } }; /** * Update the cursor over this comment by adding or removing a class. * @param {boolean} enable True if the delete cursor should be shown, false * otherwise. * @package */ WorkspaceCommentSvg.prototype.setDeleteStyle = function(enable) { if (enable) { dom.addClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyDraggingDelete'); } else { dom.removeClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyDraggingDelete'); } }; /** * Set whether auto-layout of this bubble is enabled. The first time a bubble * is shown it positions itself to not cover any blocks. Once a user has * dragged it to reposition, it renders where the user put it. * @param {boolean} _enable True if auto-layout should be enabled, false * otherwise. * @package */ WorkspaceCommentSvg.prototype.setAutoLayout = function(_enable) { // NOP for compatibility with the bubble dragger. }; /** * Decode an XML comment tag and create a rendered comment on the workspace. * @param {!Element} xmlComment XML comment element. * @param {!WorkspaceSvg} workspace The workspace. * @param {number=} opt_wsWidth The width of the workspace, which is used to * position comments correctly in RTL. * @return {!WorkspaceCommentSvg} The created workspace comment. * @package */ WorkspaceCommentSvg.fromXml = function(xmlComment, workspace, opt_wsWidth) { Events.disable(); let comment; try { const info = WorkspaceComment.parseAttributes(xmlComment); comment = new WorkspaceCommentSvg( workspace, info.content, info.h, info.w, info.id); if (workspace.rendered) { comment.initSvg(true); comment.render(); } // Position the comment correctly, taking into account the width of a // rendered RTL workspace. if (!isNaN(info.x) && !isNaN(info.y)) { if (workspace.RTL) { const wsWidth = opt_wsWidth || workspace.getWidth(); comment.moveBy(wsWidth - info.x, info.y); } else { comment.moveBy(info.x, info.y); } } } finally { Events.enable(); } WorkspaceComment.fireCreateEvent( /** @type {!WorkspaceCommentSvg} */ (comment)); return (/** @type {!WorkspaceCommentSvg} */ (comment)); }; /** * Encode a comment subtree as XML with XY coordinates. * @param {boolean=} opt_noId True if the encoder should skip the comment ID. * @return {!Element} Tree of XML elements. * @package */ WorkspaceCommentSvg.prototype.toXmlWithXY = function(opt_noId) { let width; // Not used in LTR. if (this.workspace.RTL) { // Here be performance dragons: This calls getMetrics(). width = this.workspace.getWidth(); } const element = this.toXml(opt_noId); const xy = this.getRelativeToSurfaceXY(); element.setAttribute( 'x', Math.round(this.workspace.RTL ? width - xy.x : xy.x)); element.setAttribute('y', Math.round(xy.y)); element.setAttribute('h', this.getHeight()); element.setAttribute('w', this.getWidth()); return element; }; /** * Encode a comment for copying. * @return {!ICopyable.CopyData} Copy metadata. * @package */ WorkspaceCommentSvg.prototype.toCopyData = function() { return {xml: this.toXmlWithXY(), source: this.workspace, typeCounts: null}; }; /** * 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 */ WorkspaceCommentSvg.prototype.getHeightWidth = function() { return {width: this.getWidth(), height: this.getHeight()}; }; /** * Renders the workspace comment. * @package */ WorkspaceCommentSvg.prototype.render = function() { if (this.rendered_) { return; } const size = this.getHeightWidth(); // Add text area this.createEditor_(); this.svgGroup_.appendChild(this.foreignObject_); this.svgHandleTarget_ = dom.createSvgElement( Svg.RECT, {'class': 'blocklyCommentHandleTarget', 'x': 0, 'y': 0}); this.svgGroup_.appendChild(this.svgHandleTarget_); this.svgRectTarget_ = dom.createSvgElement(Svg.RECT, { 'class': 'blocklyCommentTarget', 'x': 0, 'y': 0, 'rx': BORDER_RADIUS, 'ry': 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_) { browserEvents.conditionalBind( this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_); } if (this.isDeletable()) { browserEvents.conditionalBind( this.deleteGroup_, 'mousedown', this, this.deleteMouseDown_); browserEvents.conditionalBind( this.deleteGroup_, 'mouseout', this, this.deleteMouseOut_); browserEvents.conditionalBind( this.deleteGroup_, 'mouseup', this, this.deleteMouseUp_); } }; /** * Create the text area for the comment. * @return {!Element} The top-level node of the editor. * @private */ WorkspaceCommentSvg.prototype.createEditor_ = function() { /* Create the editor. Here's the markup that will be generated: */ this.foreignObject_ = dom.createSvgElement( Svg.FOREIGNOBJECT, { 'x': 0, 'y': WorkspaceCommentSvg.TOP_OFFSET, 'class': 'blocklyCommentForeignObject' }, null); const body = document.createElementNS(dom.HTML_NS, 'body'); body.setAttribute('xmlns', dom.HTML_NS); body.className = 'blocklyMinimalBody'; const textarea = document.createElementNS(dom.HTML_NS, 'textarea'); textarea.className = 'blocklyCommentTextarea'; textarea.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); textarea.readOnly = !this.isEditable(); body.appendChild(textarea); this.textarea_ = textarea; this.foreignObject_.appendChild(body); // Don't zoom with mousewheel. browserEvents.conditionalBind(textarea, 'wheel', this, function(e) { e.stopPropagation(); }); browserEvents.conditionalBind( textarea, 'change', this, function( /* eslint-disable no-unused-vars */ e /* eslint-enable no-unused-vars */) { this.setContent(textarea.value); }); return this.foreignObject_; }; /** * Add the resize icon to the DOM * @private */ WorkspaceCommentSvg.prototype.addResizeDom_ = function() { this.resizeGroup_ = dom.createSvgElement( Svg.G, {'class': this.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE'}, this.svgGroup_); dom.createSvgElement( Svg.POLYGON, {'points': '0,x x,x x,0'.replace(/x/g, RESIZE_SIZE.toString())}, this.resizeGroup_); dom.createSvgElement( Svg.LINE, { 'class': 'blocklyResizeLine', 'x1': RESIZE_SIZE / 3, 'y1': RESIZE_SIZE - 1, 'x2': RESIZE_SIZE - 1, 'y2': RESIZE_SIZE / 3 }, this.resizeGroup_); dom.createSvgElement( Svg.LINE, { 'class': 'blocklyResizeLine', 'x1': RESIZE_SIZE * 2 / 3, 'y1': RESIZE_SIZE - 1, 'x2': RESIZE_SIZE - 1, 'y2': RESIZE_SIZE * 2 / 3 }, this.resizeGroup_); }; /** * Add the delete icon to the DOM * @private */ WorkspaceCommentSvg.prototype.addDeleteDom_ = function() { this.deleteGroup_ = dom.createSvgElement( Svg.G, {'class': 'blocklyCommentDeleteIcon'}, this.svgGroup_); this.deleteIconBorder_ = dom.createSvgElement( Svg.CIRCLE, {'class': 'blocklyDeleteIconShape', 'r': '7', 'cx': '7.5', 'cy': '7.5'}, this.deleteGroup_); // x icon. dom.createSvgElement( Svg.LINE, { 'x1': '5', 'y1': '10', 'x2': '10', 'y2': '5', 'stroke': '#fff', 'stroke-width': '2' }, this.deleteGroup_); dom.createSvgElement( Svg.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 */ WorkspaceCommentSvg.prototype.resizeMouseDown_ = function(e) { this.unbindDragEvents_(); if (utils.isRightButton(e)) { // No right-click. e.stopPropagation(); return; } // Left-click (or middle click) this.workspace.startDrag( e, new Coordinate( this.workspace.RTL ? -this.width_ : this.width_, this.height_)); this.onMouseUpWrapper_ = browserEvents.conditionalBind( document, 'mouseup', this, this.resizeMouseUp_); this.onMouseMoveWrapper_ = browserEvents.conditionalBind( document, 'mousemove', this, this.resizeMouseMove_); this.workspace.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 */ WorkspaceCommentSvg.prototype.deleteMouseDown_ = function(e) { // Highlight the delete icon. dom.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 */ WorkspaceCommentSvg.prototype.deleteMouseOut_ = function(_e) { // Restore highlight on the delete icon. dom.removeClass( /** @type {!Element} */ (this.deleteIconBorder_), 'blocklyDeleteIconHighlighted'); }; /** * Handle a mouse-up on comment's delete icon. * @param {!Event} e Mouse up event. * @private */ WorkspaceCommentSvg.prototype.deleteMouseUp_ = function(e) { // Delete this comment. this.dispose(); // 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 */ WorkspaceCommentSvg.prototype.unbindDragEvents_ = function() { if (this.onMouseUpWrapper_) { browserEvents.unbind(this.onMouseUpWrapper_); this.onMouseUpWrapper_ = null; } if (this.onMouseMoveWrapper_) { browserEvents.unbind(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 */ WorkspaceCommentSvg.prototype.resizeMouseUp_ = function(_e) { Touch.clearTouchIdentifier(); this.unbindDragEvents_(); }; /** * Resize this comment to follow the mouse. * @param {!Event} e Mouse move event. * @private */ WorkspaceCommentSvg.prototype.resizeMouseMove_ = function(e) { this.autoLayout_ = false; const newXY = this.workspace.moveDrag(e); this.setSize_(this.RTL ? -newXY.x : newXY.x, newXY.y); }; /** * Callback function triggered when the comment has resized. * Resize the text area accordingly. * @private */ WorkspaceCommentSvg.prototype.resizeComment_ = function() { const size = this.getHeightWidth(); const topOffset = WorkspaceCommentSvg.TOP_OFFSET; const textOffset = 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 * @private */ WorkspaceCommentSvg.prototype.setSize_ = function(width, height) { // Minimum size of a comment. width = Math.max(width, 45); height = Math.max(height, 20 + WorkspaceCommentSvg.TOP_OFFSET); this.width_ = width; this.height_ = 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', WorkspaceCommentSvg.TOP_OFFSET); if (this.RTL) { this.svgRect_.setAttribute('transform', 'scale(-1 1)'); this.svgRectTarget_.setAttribute('transform', 'scale(-1 1)'); } if (this.resizeGroup_) { if (this.RTL) { // Mirror the resize group. this.resizeGroup_.setAttribute( 'transform', 'translate(' + (-width + RESIZE_SIZE) + ',' + (height - RESIZE_SIZE) + ') scale(-1 1)'); this.deleteGroup_.setAttribute( 'transform', 'translate(' + (-width + RESIZE_SIZE) + ',' + (-RESIZE_SIZE) + ') scale(-1 1)'); } else { this.resizeGroup_.setAttribute( 'transform', 'translate(' + (width - RESIZE_SIZE) + ',' + (height - RESIZE_SIZE) + ')'); this.deleteGroup_.setAttribute( 'transform', 'translate(' + (width - RESIZE_SIZE) + ',' + (-RESIZE_SIZE) + ')'); } } // Allow the contents to resize. this.resizeComment_(); }; /** * Dispose of any rendered comment components. * @private */ WorkspaceCommentSvg.prototype.disposeInternal_ = function() { this.textarea_ = null; this.foreignObject_ = null; this.svgRectTarget_ = null; this.svgHandleTarget_ = null; this.disposed_ = true; }; /** * Set the focus on the text area. * @package */ WorkspaceCommentSvg.prototype.setFocus = function() { const comment = this; this.focused_ = true; // Defer CSS changes. setTimeout(function() { if (comment.disposed_) { return; } comment.textarea_.focus(); comment.addFocus(); dom.addClass(comment.svgRectTarget_, 'blocklyCommentTargetFocused'); dom.addClass(comment.svgHandleTarget_, 'blocklyCommentHandleTargetFocused'); }, 0); }; /** * Remove focus from the text area. * @package */ WorkspaceCommentSvg.prototype.blurFocus = function() { const comment = this; this.focused_ = false; // Defer CSS changes. setTimeout(function() { if (comment.disposed_) { return; } comment.textarea_.blur(); comment.removeFocus(); dom.removeClass(comment.svgRectTarget_, 'blocklyCommentTargetFocused'); dom.removeClass( comment.svgHandleTarget_, 'blocklyCommentHandleTargetFocused'); }, 0); }; /** * CSS for workspace comment. See css.js for use. */ Css.register([ // clang-format off /* eslint-disable indent */ '.blocklyCommentForeignObject {', 'position: relative;', 'z-index: 0;', '}', '.blocklyCommentRect {', 'fill: #E7DE8E;', 'stroke: #bcA903;', 'stroke-width: 1px;', '}', '.blocklyCommentTarget {', 'fill: transparent;', 'stroke: #bcA903;', '}', '.blocklyCommentTargetFocused {', 'fill: none;', '}', '.blocklyCommentHandleTarget {', 'fill: none;', '}', '.blocklyCommentHandleTargetFocused {', 'fill: transparent;', '}', '.blocklyFocused>.blocklyCommentRect {', 'fill: #B9B272;', 'stroke: #B9B272;', '}', '.blocklySelected>.blocklyCommentTarget {', 'stroke: #fc3;', 'stroke-width: 3px;', '}', '.blocklyCommentDeleteIcon {', 'cursor: pointer;', 'fill: #000;', 'display: none;', '}', '.blocklySelected > .blocklyCommentDeleteIcon {', 'display: block;', '}', '.blocklyDeleteIconShape {', 'fill: #000;', 'stroke: #000;', 'stroke-width: 1px;', '}', '.blocklyDeleteIconShape.blocklyDeleteIconHighlighted {', 'stroke: #fc3;', '}' /* eslint-enable indent */ // clang-format on ]); exports = WorkspaceCommentSvg;