scratch-blocks/core/workspace_comment_render_svg.js
2018-06-06 18:45:32 -04:00

495 lines
14 KiB
JavaScript

/**
* @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);
};