cloud-blocks
Version:
Cloud Blocks is a library for building scratch computing interfaces with Luxrobo MODI.
839 lines (779 loc) • 23.1 kB
JavaScript
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Object representing a UI bubble.
* @author kchadha@scratch.mit.edu (Karishma Chadha)
*/
'use strict';
goog.provide('Blockly.ScratchBubble');
goog.require('Blockly.Touch');
goog.require('Blockly.Workspace');
goog.require('goog.dom');
goog.require('goog.math');
goog.require('goog.math.Coordinate');
goog.require('goog.userAgent');
/**
* Class for Scratch comment UI bubble.
* @param {!Blockly.ScratchBlockComment} comment The comment this bubble belongs
* to.
* @param {!Blockly.WorkspaceSvg} workspace The workspace on which to draw the
* bubble.
* @param {!Element} content SVG content for the bubble.
* @param {!goog.math.Coordinate} anchorXY Absolute position of bubble's anchor
* point.
* @param {?number} bubbleWidth Width of bubble, or null if not resizable.
* @param {?number} bubbleHeight Height of bubble, or null if not resizable.
* @param {?number} bubbleX X position of bubble
* @param {?number} bubbleY Y position of bubble
* @param {?boolean} minimized Whether or not this comment bubble is minimized
* (only the top bar displays), defaults to false if not provided.
* @extends {Blockly.Bubble}
* @constructor
*/
Blockly.ScratchBubble = function (
comment,
workspace,
content,
anchorXY,
bubbleWidth,
bubbleHeight,
bubbleX,
bubbleY,
minimized
) {
// Needed for Events
/**
* The comment this bubble belongs to.
* @type {Blockly.ScratchBlockComment}
* @package
*/
this.comment = comment;
this.workspace_ = workspace;
this.content_ = content;
this.x = bubbleX;
this.y = bubbleY;
this.isMinimized_ = minimized || false;
var canvas = workspace.getBubbleCanvas();
canvas.appendChild(
this.createDom_(content, !!(bubbleWidth && bubbleHeight), this.isMinimized_)
);
this.setAnchorLocation(anchorXY);
if (!bubbleWidth || !bubbleHeight) {
var bBox = /** @type {SVGLocatable} */ (this.content_).getBBox();
bubbleWidth = bBox.width + 2 * Blockly.ScratchBubble.BORDER_WIDTH;
bubbleHeight = bBox.height + 2 * Blockly.ScratchBubble.BORDER_WIDTH;
}
this.setBubbleSize(bubbleWidth, bubbleHeight);
// Render the bubble.
this.positionBubble_();
this.renderArrow_();
this.rendered_ = true;
if (!workspace.options.readOnly) {
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
);
Blockly.bindEventWithChecks_(
this.commentTopBar_,
'mousedown',
this,
this.bubbleMouseDown_
);
Blockly.bindEventWithChecks_(
this.bubbleBack_,
'mousedown',
this,
this.bubbleMouseDown_
);
if (this.resizeGroup_) {
Blockly.bindEventWithChecks_(
this.resizeGroup_,
'mousedown',
this,
this.resizeMouseDown_
);
Blockly.bindEventWithChecks_(
this.resizeGroup_,
'mouseup',
this,
this.resizeMouseUp_
);
}
}
this.setAutoLayout(false);
this.moveTo(this.x, this.y);
};
goog.inherits(Blockly.ScratchBubble, Blockly.Bubble);
/**
* Width of the border around the bubble.
* @package
*/
Blockly.ScratchBubble.BORDER_WIDTH = 1;
/**
* Thickness of the line connecting the bubble
* to the block.
* @private
*/
Blockly.ScratchBubble.LINE_THICKNESS = 1;
/**
* The height of the comment top bar.
* @package
*/
Blockly.ScratchBubble.TOP_BAR_HEIGHT = 32;
/**
* The size of the minimize arrow icon in the comment top bar.
* @private
*/
Blockly.ScratchBubble.MINIMIZE_ICON_SIZE = 32;
/**
* The size of the delete icon in the comment top bar.
* @private
*/
Blockly.ScratchBubble.DELETE_ICON_SIZE = 32;
/**
* The inset for the top bar icons.
* @private
*/
Blockly.ScratchBubble.TOP_BAR_ICON_INSET = 0;
/**
* The inset for the top bar icons.
* @private
*/
Blockly.ScratchBubble.RESIZE_SIZE = 16;
/**
* The bottom corner padding of the resize handle touch target.
* Extends slightly outside the comment box.
* @private
*/
Blockly.ScratchBubble.RESIZE_CORNER_PAD = 4;
/**
* The top/side padding around resize handle touch target.
* Extends about one extra "diagonal" above resize handle.
* @private
*/
Blockly.ScratchBubble.RESIZE_OUTER_PAD = 8;
/**
* Create the bubble's DOM.
* @param {!Element} content SVG content for the bubble.
* @param {boolean} hasResize Add diagonal resize gripper if true.
* @param {boolean} minimized Whether the bubble is minimized
* @return {!Element} The bubble's SVG group.
* @private
*/
Blockly.ScratchBubble.prototype.createDom_ = function (
content,
hasResize,
minimized
) {
this.bubbleGroup_ = Blockly.utils.createSvgElement('g', {}, null);
this.bubbleArrow_ = Blockly.utils.createSvgElement(
'line',
{ 'stroke-linecap': 'round' },
this.bubbleGroup_
);
this.bubbleBack_ = Blockly.utils.createSvgElement(
'rect',
{
class: 'blocklyDraggable scratchCommentRect',
x: 0,
y: 0,
rx: 4 * Blockly.ScratchBubble.BORDER_WIDTH,
ry: 4 * Blockly.ScratchBubble.BORDER_WIDTH
},
this.bubbleGroup_
);
this.labelText_ = content.labelText;
this.createCommentTopBar_();
// Comment Text Editor
this.commentEditor_ = content.commentEditor;
this.bubbleGroup_.appendChild(this.commentEditor_);
// Comment Resize Handle
if (hasResize) {
this.createResizeHandle_();
} else {
this.resizeGroup_ = null;
}
// Show / hide relevant things based on minimized state
if (minimized) {
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');
}
return this.bubbleGroup_;
};
/**
* Create the comment top bar and its contents.
* @private
*/
Blockly.ScratchBubble.prototype.createCommentTopBar_ = function () {
this.commentTopBar_ = Blockly.utils.createSvgElement(
'rect',
{
class: 'blocklyDraggable scratchCommentTopBar',
rx: Blockly.ScratchBubble.BORDER_WIDTH,
ry: Blockly.ScratchBubble.BORDER_WIDTH,
height: Blockly.ScratchBubble.TOP_BAR_HEIGHT
},
this.bubbleGroup_
);
this.createTopBarIcons_();
this.createTopBarLabel_();
};
/**
* Create the minimize toggle and delete icons that in the comment top bar.
* @private
*/
Blockly.ScratchBubble.prototype.createTopBarIcons_ = function () {
var topBarMiddleY =
Blockly.ScratchBubble.TOP_BAR_HEIGHT / 2 +
Blockly.ScratchBubble.BORDER_WIDTH;
// Minimize Toggle Icon in Comment Top Bar
var xInset = Blockly.ScratchBubble.TOP_BAR_ICON_INSET;
this.minimizeArrow_ = Blockly.utils.createSvgElement(
'image',
{
x: xInset,
y: topBarMiddleY - Blockly.ScratchBubble.MINIMIZE_ICON_SIZE / 2,
width: Blockly.ScratchBubble.MINIMIZE_ICON_SIZE,
height: Blockly.ScratchBubble.MINIMIZE_ICON_SIZE
},
this.bubbleGroup_
);
// Delete Icon in Comment Top Bar
this.deleteIcon_ = Blockly.utils.createSvgElement(
'image',
{
x: xInset,
y: topBarMiddleY - Blockly.ScratchBubble.DELETE_ICON_SIZE / 2,
width: Blockly.ScratchBubble.DELETE_ICON_SIZE,
height: Blockly.ScratchBubble.DELETE_ICON_SIZE
},
this.bubbleGroup_
);
this.deleteIcon_.setAttributeNS(
'http://www.w3.org/1999/xlink',
'xlink:href',
Blockly.mainWorkspace.options.pathToMedia + 'delete-x.svg'
);
};
/**
* Create the comment top bar label. This is the truncated comment text
* that shows when comment is minimized.
* @private
*/
Blockly.ScratchBubble.prototype.createTopBarLabel_ = function () {
this.topBarLabel_ = Blockly.utils.createSvgElement(
'text',
{
class: 'scratchCommentText',
x: this.width_ / 2,
y:
Blockly.ScratchBubble.TOP_BAR_HEIGHT / 2 +
Blockly.ScratchBubble.BORDER_WIDTH,
'text-anchor': 'middle',
'dominant-baseline': 'middle'
},
this.bubbleGroup_
);
var labelTextNode = document.createTextNode(this.labelText_);
this.topBarLabel_.appendChild(labelTextNode);
};
/**
* Create the comment resize handle.
* @private
*/
Blockly.ScratchBubble.prototype.createResizeHandle_ = function () {
this.resizeGroup_ = Blockly.utils.createSvgElement(
'g',
{
class: this.workspace_.RTL
? 'scratchCommentResizeSW'
: 'scratchCommentResizeSE'
},
this.bubbleGroup_
);
var resizeSize = Blockly.ScratchBubble.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_
);
};
/**
* Show the context menu for this bubble.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.ScratchBubble.prototype.showContextMenu_ = function (e) {
if (this.workspace_.options.readOnly) {
return;
}
if (this.contextMenuCallback_) {
this.contextMenuCallback_(e);
}
};
/**
* Handle a mouse-down on bubble's minimize icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.minimizeArrowMouseDown_ = function (e) {
// Set a property indicating that this comment's minimize arrow got a mouse
// down event. This property will get reset if the mouse leaves the icon or
// when a mouse up occurs on this icon after this mouse down.
this.shouldToggleMinimize_ = true;
e.stopPropagation();
};
/**
* Handle a mouse-out on bubble's minimize icon.
* @param {!Event} _e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.minimizeArrowMouseOut_ = function (_e) {
// If the mouse has left the minimize arrow icon, the
// shouldToggleMinimize property should get reset to false.
this.shouldToggleMinimize_ = false;
};
/**
* Handle a mouse-up on bubble's minimize icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.minimizeArrowMouseUp_ = function (e) {
// First check if this icon had a mouse down event
// on it and that the mouse never left the icon
if (this.shouldToggleMinimize_) {
this.shouldToggleMinimize_ = false;
if (this.minimizeToggleCallback_) {
this.minimizeToggleCallback_.call(this);
}
}
e.stopPropagation();
};
/**
* Handle a mouse-down on bubble's delete icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.deleteMouseDown_ = function (e) {
this.shouldDelete_ = true;
e.stopPropagation();
};
/**
* Handle a mouse-out on bubble's delete icon.
* @param {!Event} _e Mouse out event.
* @private
*/
Blockly.ScratchBubble.prototype.deleteMouseOut_ = function (_e) {
// If the mouse has left the delete icon, the shouldDelete_ property
// should get reset to false.
this.shouldDelete_ = false;
};
/**
* Handle a mouse-up on bubble's delete icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.deleteMouseUp_ = function (e) {
// First check that this is actually the same icon that had a mouse down event
// on it and that the mouse never left the icon
if (this.shouldDelete_) {
this.shouldDelete_ = false;
if (this.deleteCallback_) {
this.deleteCallback_.call(this);
}
}
e.stopPropagation();
};
/**
* Handle a mouse-down on bubble's resize corner.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.ScratchBubble.prototype.resizeMouseDown_ = function (e) {
this.resizeStartSize_ = { width: this.width_, height: this.height_ };
this.workspace_.setResizesEnabled(false);
Blockly.ScratchBubble.superClass_.resizeMouseDown_.call(this, e);
};
/**
* Handle a mouse-up on bubble's resize corner.
* @param {!Event} _e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.resizeMouseUp_ = function (_e) {
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.comment,
{ width: oldHW.width, height: oldHW.height },
{ width: this.width_, height: this.height_ }
)
);
this.workspace_.setResizesEnabled(true);
};
/**
* Set the minimized state of the bubble.
* @param {boolean} minimize Whether the bubble should be minimized
* @param {?string} labelText Optional label text for the comment top bar
* when it is minimized.
* @package
*/
Blockly.ScratchBubble.prototype.setMinimized = function (minimize, labelText) {
if (minimize == this.isMinimized_) {
return;
}
if (minimize) {
this.isMinimized_ = true;
// 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
this.topBarLabel_.textContent = labelText;
}
Blockly.utils.removeAttribute(this.topBarLabel_, 'display');
} else {
this.isMinimized_ = false;
// 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');
}
}
};
/**
* Register a function as a callback event for when the bubble is minimized.
* @param {!Function} callback The function to call on resize.
* @package
*/
Blockly.ScratchBubble.prototype.registerMinimizeToggleEvent = function (
callback
) {
this.minimizeToggleCallback_ = callback;
};
/**
* Register a function as a callback event for when the bubble is resized.
* @param {!Function} callback The function to call on resize.
* @package
*/
Blockly.ScratchBubble.prototype.registerDeleteEvent = function (callback) {
this.deleteCallback_ = callback;
};
/**
* Register a function as a callback to show the context menu for this comment.
* @param {!Function} callback The function to call on resize.
* @package
*/
Blockly.ScratchBubble.prototype.registerContextMenuCallback = function (
callback
) {
this.contextMenuCallback_ = callback;
};
/**
* Notification that the anchor has moved.
* Update the arrow and bubble accordingly.
* @param {!goog.math.Coordinate} xy Absolute location.
* @package
*/
Blockly.ScratchBubble.prototype.setAnchorLocation = function (xy) {
this.anchorXY_ = xy;
if (this.rendered_) {
this.positionBubble_();
}
};
/**
* 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
*/
Blockly.ScratchBubble.prototype.moveTo = function (x, y) {
Blockly.ScratchBubble.superClass_.moveTo.call(this, x, y);
this.updatePosition_(x, y);
};
/**
* Size this bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
* @package
*/
Blockly.ScratchBubble.prototype.setBubbleSize = function (width, height) {
var doubleBorderWidth = 2 * Blockly.ScratchBubble.BORDER_WIDTH;
// Minimum size of a bubble.
width = Math.max(width, doubleBorderWidth + 50);
height = Math.max(height, Blockly.ScratchBubble.TOP_BAR_HEIGHT);
this.width_ = width;
this.height_ = height;
this.bubbleBack_.setAttribute('width', width);
this.bubbleBack_.setAttribute('height', height);
this.commentTopBar_.setAttribute('width', width);
this.commentTopBar_.setAttribute(
'height',
Blockly.ScratchBubble.TOP_BAR_HEIGHT
);
if (this.workspace_.RTL) {
this.minimizeArrow_.setAttribute(
'x',
width -
Blockly.ScratchBubble.MINIMIZE_ICON_SIZE -
Blockly.ScratchBubble.TOP_BAR_ICON_INSET
);
} else {
this.deleteIcon_.setAttribute(
'x',
width -
Blockly.ScratchBubble.DELETE_ICON_SIZE -
Blockly.ScratchBubble.TOP_BAR_ICON_INSET
);
}
if (this.resizeGroup_) {
var resizeSize = Blockly.ScratchBubble.RESIZE_SIZE;
if (this.workspace_.RTL) {
// Mirror the resize group.
this.resizeGroup_.setAttribute(
'transform',
'translate(' +
(resizeSize + doubleBorderWidth) +
',' +
(this.height_ - doubleBorderWidth - resizeSize) +
') scale(-1, 1)'
);
} else {
this.resizeGroup_.setAttribute(
'transform',
'translate(' +
(this.width_ - doubleBorderWidth - resizeSize) +
',' +
(this.height_ - doubleBorderWidth - resizeSize) +
')'
);
}
}
if (this.isMinimized_) {
this.topBarLabel_.setAttribute('x', this.width_ / 2);
this.topBarLabel_.setAttribute('y', this.height_ / 2);
}
if (this.rendered_) {
this.positionBubble_();
this.renderArrow_();
}
// Allow the contents to resize.
if (this.resizeCallback_) {
this.resizeCallback_();
}
};
/**
* Draw the line between the bubble and the origin.
* @private
*/
Blockly.ScratchBubble.prototype.renderArrow_ = function () {
// Find the relative coordinates of the top bar center of the bubble.
var relBubbleX = this.width_ / 2;
var relBubbleY = Blockly.ScratchBubble.TOP_BAR_HEIGHT / 2;
// Find the relative coordinates of the center of the anchor.
var relAnchorX = -this.relativeLeft_;
var relAnchorY = -this.relativeTop_;
if (relBubbleX != relAnchorX || relBubbleY != relAnchorY) {
// Compute the angle of the arrow's line.
var rise = relAnchorY - relBubbleY;
var run = relAnchorX - relBubbleX;
if (this.workspace_.RTL) {
run *= -1;
run -= this.width_;
}
var baseX1 = relBubbleX;
var baseY1 = relBubbleY;
this.bubbleArrow_.setAttribute('x1', baseX1);
this.bubbleArrow_.setAttribute('y1', baseY1);
this.bubbleArrow_.setAttribute('x2', baseX1 + run);
this.bubbleArrow_.setAttribute('y2', baseY1 + rise);
this.bubbleArrow_.setAttribute(
'stroke-width',
Blockly.ScratchBubble.LINE_THICKNESS
);
}
};
/**
* Change the colour of a bubble.
* @param {string} hexColour Hex code of colour.
* @package
*/
Blockly.ScratchBubble.prototype.setColour = function (hexColour) {
this.bubbleBack_.setAttribute('stroke', hexColour);
this.bubbleArrow_.setAttribute('stroke', hexColour);
};
/**
* Move this bubble during a drag, taking into account whether or not there is
* a drag surface.
* @param {?Blockly.BlockDragSurfaceSvg} dragSurface The surface that carries
* rendered items during a drag, or null if no drag surface is in use.
* @param {!goog.math.Coordinate} newLoc The location to translate to, in
* workspace coordinates.
* @package
*/
Blockly.ScratchBubble.prototype.moveDuringDrag = function (
dragSurface,
newLoc
) {
if (dragSurface) {
dragSurface.translateSurface(newLoc.x, newLoc.y);
this.updatePosition_(newLoc.x, newLoc.y);
} else {
this.moveTo(newLoc.x, newLoc.y);
}
};
/**
* Update the relative left and top of the bubble after a move.
* @param {number} x The x position of the bubble
* @param {number} y The y position of the bubble
* @private
*/
Blockly.ScratchBubble.prototype.updatePosition_ = function (x, y) {
// Relative left is the distance *and* direction to get from the comment
// anchor position on the block to the starting edge of the comment (e.g.
// the left edge of the comment in LTR and the right edge of the comment in RTL)
if (this.workspace_.RTL) {
// we want relativeLeft_ to actually be the distance from the anchor point to the *right* edge of the comment in RTL
this.relativeLeft_ = this.anchorXY_.x - x;
} else {
this.relativeLeft_ = x - this.anchorXY_.x;
}
this.relativeTop_ = y - this.anchorXY_.y;
this.renderArrow_();
};
/**
* Dispose of this bubble.
* @package
*/
Blockly.ScratchBubble.prototype.dispose = function () {
Blockly.ScratchBubble.superClass_.dispose.call(this);
this.topBarLabel_ = null;
this.commentTopBar_ = null;
this.minimizeArrow_ = null;
this.deleteIcon_ = null;
};