Files
blockly/core/bubble.js
Neil Fraser 90b3f75d82 Remove @author tags (#5601)
Our files are up to a decade old, and have churned so much, that the initial author of the file no longer has much meaning.

Furthermore, this will encourage developers to post to the developer group, rather than emailing Googlers (usually me) directly.
2021-10-15 09:50:46 -07:00

942 lines
29 KiB
JavaScript

/**
* @license
* Copyright 2012 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Object representing a UI bubble.
*/
'use strict';
/**
* Object representing a UI bubble.
* @class
*/
goog.module('Blockly.Bubble');
const Coordinate = goog.require('Blockly.utils.Coordinate');
/* eslint-disable-next-line no-unused-vars */
const IBubble = goog.require('Blockly.IBubble');
/* eslint-disable-next-line no-unused-vars */
const MetricsManager = goog.requireType('Blockly.MetricsManager');
const Scrollbar = goog.require('Blockly.Scrollbar');
const Size = goog.require('Blockly.utils.Size');
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 browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const math = goog.require('Blockly.utils.math');
const userAgent = goog.require('Blockly.utils.userAgent');
/* eslint-disable-next-line no-unused-vars */
const {BlockDragSurfaceSvg} = goog.requireType('Blockly.BlockDragSurfaceSvg');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/** @suppress {extraRequire} */
goog.require('Blockly.Workspace');
/**
* Class for UI bubble.
* @param {!WorkspaceSvg} workspace The workspace on which to draw the
* bubble.
* @param {!Element} content SVG content for the bubble.
* @param {!Element} shape SVG element to avoid eclipsing.
* @param {!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.
* @implements {IBubble}
* @constructor
* @alias Blockly.Bubble
*/
const Bubble = function(
workspace, content, shape, anchorXY, bubbleWidth, bubbleHeight) {
this.workspace_ = workspace;
this.content_ = content;
this.shape_ = shape;
/**
* Method to call on resize of bubble.
* @type {?function()}
* @private
*/
this.resizeCallback_ = null;
/**
* Method to call on move of bubble.
* @type {?function()}
* @private
*/
this.moveCallback_ = null;
/**
* Mouse down on bubbleBack_ event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseDownBubbleWrapper_ = null;
/**
* Mouse down on resizeGroup_ event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseDownResizeWrapper_ = null;
/**
* Describes whether this bubble has been disposed of (nodes and event
* listeners removed from the page) or not.
* @type {boolean}
* @package
*/
this.disposed = false;
let angle = Bubble.ARROW_ANGLE;
if (this.workspace_.RTL) {
angle = -angle;
}
this.arrow_radians_ = math.toRadians(angle);
const canvas = workspace.getBubbleCanvas();
canvas.appendChild(this.createDom_(content, !!(bubbleWidth && bubbleHeight)));
this.setAnchorLocation(anchorXY);
if (!bubbleWidth || !bubbleHeight) {
const bBox = /** @type {SVGLocatable} */ (this.content_).getBBox();
bubbleWidth = bBox.width + 2 * Bubble.BORDER_WIDTH;
bubbleHeight = bBox.height + 2 * Bubble.BORDER_WIDTH;
}
this.setBubbleSize(bubbleWidth, bubbleHeight);
// Render the bubble.
this.positionBubble_();
this.renderArrow_();
this.rendered_ = true;
};
/**
* Width of the border around the bubble.
*/
Bubble.BORDER_WIDTH = 6;
/**
* Determines the thickness of the base of the arrow in relation to the size
* of the bubble. Higher numbers result in thinner arrows.
*/
Bubble.ARROW_THICKNESS = 5;
/**
* The number of degrees that the arrow bends counter-clockwise.
*/
Bubble.ARROW_ANGLE = 20;
/**
* The sharpness of the arrow's bend. Higher numbers result in smoother arrows.
*/
Bubble.ARROW_BEND = 4;
/**
* Distance between arrow point and anchor point.
*/
Bubble.ANCHOR_RADIUS = 8;
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
Bubble.onMouseUpWrapper_ = null;
/**
* Mouse move event data.
* @type {?browserEvents.Data}
* @private
*/
Bubble.onMouseMoveWrapper_ = null;
/**
* Stop binding to the global mouseup and mousemove events.
* @private
*/
Bubble.unbindDragEvents_ = function() {
if (Bubble.onMouseUpWrapper_) {
browserEvents.unbind(Bubble.onMouseUpWrapper_);
Bubble.onMouseUpWrapper_ = null;
}
if (Bubble.onMouseMoveWrapper_) {
browserEvents.unbind(Bubble.onMouseMoveWrapper_);
Bubble.onMouseMoveWrapper_ = null;
}
};
/**
* Handle a mouse-up event while dragging a bubble's border or resize handle.
* @param {!Event} _e Mouse up event.
* @private
*/
Bubble.bubbleMouseUp_ = function(_e) {
Touch.clearTouchIdentifier();
Bubble.unbindDragEvents_();
};
/**
* Flag to stop incremental rendering during construction.
* @private
*/
Bubble.prototype.rendered_ = false;
/**
* Absolute coordinate of anchor point, in workspace coordinates.
* @type {Coordinate}
* @private
*/
Bubble.prototype.anchorXY_ = null;
/**
* Relative X coordinate of bubble with respect to the anchor's centre,
* in workspace units.
* In RTL mode the initial value is negated.
* @private
*/
Bubble.prototype.relativeLeft_ = 0;
/**
* Relative Y coordinate of bubble with respect to the anchor's centre, in
* workspace units.
* @private
*/
Bubble.prototype.relativeTop_ = 0;
/**
* Width of bubble, in workspace units.
* @private
*/
Bubble.prototype.width_ = 0;
/**
* Height of bubble, in workspace units.
* @private
*/
Bubble.prototype.height_ = 0;
/**
* Automatically position and reposition the bubble.
* @private
*/
Bubble.prototype.autoLayout_ = true;
/**
* Create the bubble's DOM.
* @param {!Element} content SVG content for the bubble.
* @param {boolean} hasResize Add diagonal resize gripper if true.
* @return {!SVGElement} The bubble's SVG group.
* @private
*/
Bubble.prototype.createDom_ = function(content, hasResize) {
/* Create the bubble. Here's the markup that will be generated:
<g>
<g filter="url(#blocklyEmbossFilter837493)">
<path d="... Z" />
<rect class="blocklyDraggable" rx="8" ry="8" width="180" height="180"/>
</g>
<g transform="translate(165, 165)" class="blocklyResizeSE">
<polygon points="0,15 15,15 15,0"/>
<line class="blocklyResizeLine" x1="5" y1="14" x2="14" y2="5"/>
<line class="blocklyResizeLine" x1="10" y1="14" x2="14" y2="10"/>
</g>
[...content goes here...]
</g>
*/
this.bubbleGroup_ = dom.createSvgElement(Svg.G, {}, null);
let filter = {
'filter': 'url(#' +
this.workspace_.getRenderer().getConstants().embossFilterId + ')'
};
if (userAgent.JAVA_FX) {
// Multiple reports that JavaFX can't handle filters.
// https://github.com/google/blockly/issues/99
filter = {};
}
const bubbleEmboss = dom.createSvgElement(Svg.G, filter, this.bubbleGroup_);
this.bubbleArrow_ = dom.createSvgElement(Svg.PATH, {}, bubbleEmboss);
this.bubbleBack_ = dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyDraggable',
'x': 0,
'y': 0,
'rx': Bubble.BORDER_WIDTH,
'ry': Bubble.BORDER_WIDTH
},
bubbleEmboss);
if (hasResize) {
this.resizeGroup_ = dom.createSvgElement(
Svg.G,
{'class': this.workspace_.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE'},
this.bubbleGroup_);
const resizeSize = 2 * Bubble.BORDER_WIDTH;
dom.createSvgElement(
Svg.POLYGON,
{'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())},
this.resizeGroup_);
dom.createSvgElement(
Svg.LINE, {
'class': 'blocklyResizeLine',
'x1': resizeSize / 3,
'y1': resizeSize - 1,
'x2': resizeSize - 1,
'y2': resizeSize / 3
},
this.resizeGroup_);
dom.createSvgElement(
Svg.LINE, {
'class': 'blocklyResizeLine',
'x1': resizeSize * 2 / 3,
'y1': resizeSize - 1,
'x2': resizeSize - 1,
'y2': resizeSize * 2 / 3
},
this.resizeGroup_);
} else {
this.resizeGroup_ = null;
}
if (!this.workspace_.options.readOnly) {
this.onMouseDownBubbleWrapper_ = browserEvents.conditionalBind(
this.bubbleBack_, 'mousedown', this, this.bubbleMouseDown_);
if (this.resizeGroup_) {
this.onMouseDownResizeWrapper_ = browserEvents.conditionalBind(
this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_);
}
}
this.bubbleGroup_.appendChild(content);
return this.bubbleGroup_;
};
/**
* Return the root node of the bubble's SVG group.
* @return {!SVGElement} The root SVG node of the bubble's group.
*/
Bubble.prototype.getSvgRoot = function() {
return this.bubbleGroup_;
};
/**
* Expose the block's ID on the bubble's top-level SVG group.
* @param {string} id ID of block.
*/
Bubble.prototype.setSvgId = function(id) {
if (this.bubbleGroup_.dataset) {
this.bubbleGroup_.dataset['blockId'] = id;
}
};
/**
* Handle a mouse-down on bubble's border.
* @param {!Event} e Mouse down event.
* @private
*/
Bubble.prototype.bubbleMouseDown_ = function(e) {
const gesture = this.workspace_.getGesture(e);
if (gesture) {
gesture.handleBubbleStart(e, this);
}
};
/**
* Show the context menu for this bubble.
* @param {!Event} _e Mouse event.
* @package
*/
Bubble.prototype.showContextMenu = function(_e) {
// NOP on bubbles, but used by the bubble dragger to pass events to
// workspace comments.
};
/**
* Get whether this bubble is deletable or not.
* @return {boolean} True if deletable.
* @package
*/
Bubble.prototype.isDeletable = function() {
return false;
};
/**
* Update the style of this bubble when it is dragged over a delete area.
* @param {boolean} _enable True if the bubble is about to be deleted, false
* otherwise.
*/
Bubble.prototype.setDeleteStyle = function(_enable) {
// NOP if bubble is not deletable.
};
/**
* Handle a mouse-down on bubble's resize corner.
* @param {!Event} e Mouse down event.
* @private
*/
Bubble.prototype.resizeMouseDown_ = function(e) {
this.promote();
Bubble.unbindDragEvents_();
if (browserEvents.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_));
Bubble.onMouseUpWrapper_ = browserEvents.conditionalBind(
document, 'mouseup', this, Bubble.bubbleMouseUp_);
Bubble.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();
};
/**
* Resize this bubble to follow the mouse.
* @param {!Event} e Mouse move event.
* @private
*/
Bubble.prototype.resizeMouseMove_ = function(e) {
this.autoLayout_ = false;
const newXY = this.workspace_.moveDrag(e);
this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y);
if (this.workspace_.RTL) {
// RTL requires the bubble to move its left edge.
this.positionBubble_();
}
};
/**
* Register a function as a callback event for when the bubble is resized.
* @param {!Function} callback The function to call on resize.
*/
Bubble.prototype.registerResizeEvent = function(callback) {
this.resizeCallback_ = callback;
};
/**
* Register a function as a callback event for when the bubble is moved.
* @param {!Function} callback The function to call on move.
*/
Bubble.prototype.registerMoveEvent = function(callback) {
this.moveCallback_ = callback;
};
/**
* Move this bubble to the top of the stack.
* @return {boolean} Whether or not the bubble has been moved.
* @package
*/
Bubble.prototype.promote = function() {
const svgGroup = this.bubbleGroup_.parentNode;
if (svgGroup.lastChild !== this.bubbleGroup_) {
svgGroup.appendChild(this.bubbleGroup_);
return true;
}
return false;
};
/**
* Notification that the anchor has moved.
* Update the arrow and bubble accordingly.
* @param {!Coordinate} xy Absolute location.
*/
Bubble.prototype.setAnchorLocation = function(xy) {
this.anchorXY_ = xy;
if (this.rendered_) {
this.positionBubble_();
}
};
/**
* Position the bubble so that it does not fall off-screen.
* @private
*/
Bubble.prototype.layoutBubble_ = function() {
// Get the metrics in workspace units.
const viewMetrics = this.workspace_.getMetricsManager().getViewMetrics(true);
const optimalLeft = this.getOptimalRelativeLeft_(viewMetrics);
const optimalTop = this.getOptimalRelativeTop_(viewMetrics);
const bbox = this.shape_.getBBox();
const topPosition = {
x: optimalLeft,
y: -this.height_ -
this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT
};
const startPosition = {x: -this.width_ - 30, y: optimalTop};
const endPosition = {x: bbox.width, y: optimalTop};
const bottomPosition = {x: optimalLeft, y: bbox.height};
const closerPosition =
bbox.width < bbox.height ? endPosition : bottomPosition;
const fartherPosition =
bbox.width < bbox.height ? bottomPosition : endPosition;
const topPositionOverlap = this.getOverlap_(topPosition, viewMetrics);
const startPositionOverlap = this.getOverlap_(startPosition, viewMetrics);
const closerPositionOverlap = this.getOverlap_(closerPosition, viewMetrics);
const fartherPositionOverlap = this.getOverlap_(fartherPosition, viewMetrics);
// Set the position to whichever position shows the most of the bubble,
// with tiebreaks going in the order: top > start > close > far.
const mostOverlap = Math.max(
topPositionOverlap, startPositionOverlap, closerPositionOverlap,
fartherPositionOverlap);
if (topPositionOverlap === mostOverlap) {
this.relativeLeft_ = topPosition.x;
this.relativeTop_ = topPosition.y;
return;
}
if (startPositionOverlap === mostOverlap) {
this.relativeLeft_ = startPosition.x;
this.relativeTop_ = startPosition.y;
return;
}
if (closerPositionOverlap === mostOverlap) {
this.relativeLeft_ = closerPosition.x;
this.relativeTop_ = closerPosition.y;
return;
}
// TODO: I believe relativeLeft_ should actually be called relativeStart_
// and then the math should be fixed to reflect this. (hopefully it'll
// make it look simpler)
this.relativeLeft_ = fartherPosition.x;
this.relativeTop_ = fartherPosition.y;
};
/**
* Calculate the what percentage of the bubble overlaps with the visible
* workspace (what percentage of the bubble is visible).
* @param {!{x: number, y: number}} relativeMin The position of the top-left
* corner of the bubble relative to the anchor point.
* @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
* of the workspace the bubble will appear in.
* @return {number} The percentage of the bubble that is visible.
* @private
*/
Bubble.prototype.getOverlap_ = function(relativeMin, viewMetrics) {
// The position of the top-left corner of the bubble in workspace units.
const bubbleMin = {
x: this.workspace_.RTL ? (this.anchorXY_.x - relativeMin.x - this.width_) :
(relativeMin.x + this.anchorXY_.x),
y: relativeMin.y + this.anchorXY_.y
};
// The position of the bottom-right corner of the bubble in workspace units.
const bubbleMax = {
x: bubbleMin.x + this.width_,
y: bubbleMin.y + this.height_
};
// We could adjust these values to account for the scrollbars, but the
// bubbles should have been adjusted to not collide with them anyway, so
// giving the workspace a slightly larger "bounding box" shouldn't affect the
// calculation.
// The position of the top-left corner of the workspace.
const workspaceMin = {x: viewMetrics.left, y: viewMetrics.top};
// The position of the bottom-right corner of the workspace.
const workspaceMax = {
x: viewMetrics.left + viewMetrics.width,
y: viewMetrics.top + viewMetrics.height
};
const overlapWidth = Math.min(bubbleMax.x, workspaceMax.x) -
Math.max(bubbleMin.x, workspaceMin.x);
const overlapHeight = Math.min(bubbleMax.y, workspaceMax.y) -
Math.max(bubbleMin.y, workspaceMin.y);
return Math.max(
0,
Math.min(
1, (overlapWidth * overlapHeight) / (this.width_ * this.height_)));
};
/**
* Calculate what the optimal horizontal position of the top-left corner of the
* bubble is (relative to the anchor point) so that the most area of the
* bubble is shown.
* @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
* of the workspace the bubble will appear in.
* @return {number} The optimal horizontal position of the top-left corner
* of the bubble.
* @private
*/
Bubble.prototype.getOptimalRelativeLeft_ = function(viewMetrics) {
let relativeLeft = -this.width_ / 4;
// No amount of sliding left or right will give us a better overlap.
if (this.width_ > viewMetrics.width) {
return relativeLeft;
}
if (this.workspace_.RTL) {
// Bubble coordinates are flipped in RTL.
const bubbleRight = this.anchorXY_.x - relativeLeft;
const bubbleLeft = bubbleRight - this.width_;
const workspaceRight = viewMetrics.left + viewMetrics.width;
const workspaceLeft = viewMetrics.left +
// Thickness in workspace units.
(Scrollbar.scrollbarThickness / this.workspace_.scale);
if (bubbleLeft < workspaceLeft) {
// Slide the bubble right until it is onscreen.
relativeLeft = -(workspaceLeft - this.anchorXY_.x + this.width_);
} else if (bubbleRight > workspaceRight) {
// Slide the bubble left until it is onscreen.
relativeLeft = -(workspaceRight - this.anchorXY_.x);
}
} else {
const bubbleLeft = relativeLeft + this.anchorXY_.x;
const bubbleRight = bubbleLeft + this.width_;
const workspaceLeft = viewMetrics.left;
const workspaceRight = viewMetrics.left + viewMetrics.width -
// Thickness in workspace units.
(Scrollbar.scrollbarThickness / this.workspace_.scale);
if (bubbleLeft < workspaceLeft) {
// Slide the bubble right until it is onscreen.
relativeLeft = workspaceLeft - this.anchorXY_.x;
} else if (bubbleRight > workspaceRight) {
// Slide the bubble left until it is onscreen.
relativeLeft = workspaceRight - this.anchorXY_.x - this.width_;
}
}
return relativeLeft;
};
/**
* Calculate what the optimal vertical position of the top-left corner of
* the bubble is (relative to the anchor point) so that the most area of the
* bubble is shown.
* @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
* of the workspace the bubble will appear in.
* @return {number} The optimal vertical position of the top-left corner
* of the bubble.
* @private
*/
Bubble.prototype.getOptimalRelativeTop_ = function(viewMetrics) {
let relativeTop = -this.height_ / 4;
// No amount of sliding up or down will give us a better overlap.
if (this.height_ > viewMetrics.height) {
return relativeTop;
}
const bubbleTop = this.anchorXY_.y + relativeTop;
const bubbleBottom = bubbleTop + this.height_;
const workspaceTop = viewMetrics.top;
const workspaceBottom = viewMetrics.top + viewMetrics.height -
// Thickness in workspace units.
(Scrollbar.scrollbarThickness / this.workspace_.scale);
const anchorY = this.anchorXY_.y;
if (bubbleTop < workspaceTop) {
// Slide the bubble down until it is onscreen.
relativeTop = workspaceTop - anchorY;
} else if (bubbleBottom > workspaceBottom) {
// Slide the bubble up until it is onscreen.
relativeTop = workspaceBottom - anchorY - this.height_;
}
return relativeTop;
};
/**
* Move the bubble to a location relative to the anchor's centre.
* @private
*/
Bubble.prototype.positionBubble_ = function() {
let left = this.anchorXY_.x;
if (this.workspace_.RTL) {
left -= this.relativeLeft_ + this.width_;
} else {
left += this.relativeLeft_;
}
const top = this.relativeTop_ + this.anchorXY_.y;
this.moveTo(left, top);
};
/**
* 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
*/
Bubble.prototype.moveTo = function(x, y) {
this.bubbleGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')');
};
/**
* Triggers a move callback if one exists at the end of a drag.
* @param {boolean} adding True if adding, false if removing.
* @package
*/
Bubble.prototype.setDragging = function(adding) {
if (!adding && this.moveCallback_) {
this.moveCallback_();
}
};
/**
* Get the dimensions of this bubble.
* @return {!Size} The height and width of the bubble.
*/
Bubble.prototype.getBubbleSize = function() {
return new Size(this.width_, this.height_);
};
/**
* Size this bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
*/
Bubble.prototype.setBubbleSize = function(width, height) {
const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
// Minimum size of a bubble.
width = Math.max(width, doubleBorderWidth + 45);
height = Math.max(height, doubleBorderWidth + 20);
this.width_ = width;
this.height_ = height;
this.bubbleBack_.setAttribute('width', width);
this.bubbleBack_.setAttribute('height', height);
if (this.resizeGroup_) {
if (this.workspace_.RTL) {
// Mirror the resize group.
const resizeSize = 2 * Bubble.BORDER_WIDTH;
this.resizeGroup_.setAttribute(
'transform',
'translate(' + resizeSize + ',' + (height - doubleBorderWidth) +
') scale(-1 1)');
} else {
this.resizeGroup_.setAttribute(
'transform',
'translate(' + (width - doubleBorderWidth) + ',' +
(height - doubleBorderWidth) + ')');
}
}
if (this.autoLayout_) {
this.layoutBubble_();
}
this.positionBubble_();
this.renderArrow_();
// Allow the contents to resize.
if (this.resizeCallback_) {
this.resizeCallback_();
}
};
/**
* Draw the arrow between the bubble and the origin.
* @private
*/
Bubble.prototype.renderArrow_ = function() {
const steps = [];
// Find the relative coordinates of the center of the bubble.
const relBubbleX = this.width_ / 2;
const relBubbleY = this.height_ / 2;
// Find the relative coordinates of the center of the anchor.
let relAnchorX = -this.relativeLeft_;
let relAnchorY = -this.relativeTop_;
if (relBubbleX === relAnchorX && relBubbleY === relAnchorY) {
// Null case. Bubble is directly on top of the anchor.
// Short circuit this rather than wade through divide by zeros.
steps.push('M ' + relBubbleX + ',' + relBubbleY);
} else {
// Compute the angle of the arrow's line.
const rise = relAnchorY - relBubbleY;
let run = relAnchorX - relBubbleX;
if (this.workspace_.RTL) {
run *= -1;
}
const hypotenuse = Math.sqrt(rise * rise + run * run);
let angle = Math.acos(run / hypotenuse);
if (rise < 0) {
angle = 2 * Math.PI - angle;
}
// Compute a line perpendicular to the arrow.
let rightAngle = angle + Math.PI / 2;
if (rightAngle > Math.PI * 2) {
rightAngle -= Math.PI * 2;
}
const rightRise = Math.sin(rightAngle);
const rightRun = Math.cos(rightAngle);
// Calculate the thickness of the base of the arrow.
const bubbleSize = this.getBubbleSize();
let thickness =
(bubbleSize.width + bubbleSize.height) / Bubble.ARROW_THICKNESS;
thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 4;
// Back the tip of the arrow off of the anchor.
const backoffRatio = 1 - Bubble.ANCHOR_RADIUS / hypotenuse;
relAnchorX = relBubbleX + backoffRatio * run;
relAnchorY = relBubbleY + backoffRatio * rise;
// Coordinates for the base of the arrow.
const baseX1 = relBubbleX + thickness * rightRun;
const baseY1 = relBubbleY + thickness * rightRise;
const baseX2 = relBubbleX - thickness * rightRun;
const baseY2 = relBubbleY - thickness * rightRise;
// Distortion to curve the arrow.
let swirlAngle = angle + this.arrow_radians_;
if (swirlAngle > Math.PI * 2) {
swirlAngle -= Math.PI * 2;
}
const swirlRise = Math.sin(swirlAngle) * hypotenuse / Bubble.ARROW_BEND;
const swirlRun = Math.cos(swirlAngle) * hypotenuse / Bubble.ARROW_BEND;
steps.push('M' + baseX1 + ',' + baseY1);
steps.push(
'C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) + ' ' +
relAnchorX + ',' + relAnchorY + ' ' + relAnchorX + ',' + relAnchorY);
steps.push(
'C' + relAnchorX + ',' + relAnchorY + ' ' + (baseX2 + swirlRun) + ',' +
(baseY2 + swirlRise) + ' ' + baseX2 + ',' + baseY2);
}
steps.push('z');
this.bubbleArrow_.setAttribute('d', steps.join(' '));
};
/**
* Change the colour of a bubble.
* @param {string} hexColour Hex code of colour.
*/
Bubble.prototype.setColour = function(hexColour) {
this.bubbleBack_.setAttribute('fill', hexColour);
this.bubbleArrow_.setAttribute('fill', hexColour);
};
/**
* Dispose of this bubble.
*/
Bubble.prototype.dispose = function() {
if (this.onMouseDownBubbleWrapper_) {
browserEvents.unbind(this.onMouseDownBubbleWrapper_);
}
if (this.onMouseDownResizeWrapper_) {
browserEvents.unbind(this.onMouseDownResizeWrapper_);
}
Bubble.unbindDragEvents_();
dom.removeNode(this.bubbleGroup_);
this.disposed = true;
};
/**
* Move this bubble during a drag, taking into account whether or not there is
* a drag surface.
* @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
*/
Bubble.prototype.moveDuringDrag = function(dragSurface, newLoc) {
if (dragSurface) {
dragSurface.translateSurface(newLoc.x, newLoc.y);
} else {
this.moveTo(newLoc.x, newLoc.y);
}
if (this.workspace_.RTL) {
this.relativeLeft_ = this.anchorXY_.x - newLoc.x - this.width_;
} else {
this.relativeLeft_ = newLoc.x - this.anchorXY_.x;
}
this.relativeTop_ = newLoc.y - this.anchorXY_.y;
this.renderArrow_();
};
/**
* Return the coordinates of the top-left corner of this bubble's body relative
* to the drawing surface's origin (0,0), in workspace units.
* @return {!Coordinate} Object with .x and .y properties.
*/
Bubble.prototype.getRelativeToSurfaceXY = function() {
return new Coordinate(
this.workspace_.RTL ?
-this.relativeLeft_ + this.anchorXY_.x - this.width_ :
this.anchorXY_.x + this.relativeLeft_,
this.anchorXY_.y + this.relativeTop_);
};
/**
* 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
*/
Bubble.prototype.setAutoLayout = function(enable) {
this.autoLayout_ = enable;
};
/**
* Create the text for a non editable bubble.
* @param {string} text The text to display.
* @return {!SVGTextElement} The top-level node of the text.
* @package
*/
Bubble.textToDom = function(text) {
const paragraph = dom.createSvgElement(
Svg.TEXT, {
'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents',
'y': Bubble.BORDER_WIDTH
},
null);
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const tspanElement = dom.createSvgElement(
Svg.TSPAN, {'dy': '1em', 'x': Bubble.BORDER_WIDTH}, paragraph);
const textNode = document.createTextNode(lines[i]);
tspanElement.appendChild(textNode);
}
return paragraph;
};
/**
* Creates a bubble that can not be edited.
* @param {!SVGTextElement} paragraphElement The text element for the non
* editable bubble.
* @param {!BlockSvg} block The block that the bubble is attached to.
* @param {!Coordinate} iconXY The coordinate of the icon.
* @return {!Bubble} The non editable bubble.
* @package
*/
Bubble.createNonEditableBubble = function(paragraphElement, block, iconXY) {
const bubble = new Bubble(
/** @type {!WorkspaceSvg} */ (block.workspace), paragraphElement,
block.pathObject.svgPath,
/** @type {!Coordinate} */ (iconXY), null, null);
// Expose this bubble's block's ID on its top-level SVG group.
bubble.setSvgId(block.id);
if (block.RTL) {
// Right-align the paragraph.
// This cannot be done until the bubble is rendered on screen.
const maxWidth = paragraphElement.getBBox().width;
for (let i = 0, textElement; (textElement = paragraphElement.childNodes[i]);
i++) {
textElement.setAttribute('text-anchor', 'end');
textElement.setAttribute('x', maxWidth + Bubble.BORDER_WIDTH);
}
}
return bubble;
};
exports = Bubble;