blockly
Version:
Blockly is a library for building visual programming editors.
684 lines (604 loc) • 21.1 kB
JavaScript
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Methods for graphically rendering a marker as SVG.
*/
'use strict';
/**
* Methods for graphically rendering a marker as SVG.
* @class
*/
goog.module('Blockly.blockRendering.MarkerSvg');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const svgPaths = goog.require('Blockly.utils.svgPaths');
const {ASTNode} = goog.require('Blockly.ASTNode');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
/* eslint-disable-next-line no-unused-vars */
const {Connection} = goog.requireType('Blockly.Connection');
/* eslint-disable-next-line no-unused-vars */
const {ConstantProvider} = goog.requireType('Blockly.blockRendering.ConstantProvider');
/* eslint-disable-next-line no-unused-vars */
const {Field} = goog.requireType('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {IASTNodeLocationSvg} = goog.requireType('Blockly.IASTNodeLocationSvg');
/* eslint-disable-next-line no-unused-vars */
const {Marker} = goog.requireType('Blockly.Marker');
/* eslint-disable-next-line no-unused-vars */
const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
const {Svg} = goog.require('Blockly.utils.Svg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.MarkerMove');
/**
* The name of the CSS class for a cursor.
* @const {string}
*/
const CURSOR_CLASS = 'blocklyCursor';
/**
* The name of the CSS class for a marker.
* @const {string}
*/
const MARKER_CLASS = 'blocklyMarker';
/**
* What we multiply the height by to get the height of the marker.
* Only used for the block and block connections.
* @const {number}
*/
const HEIGHT_MULTIPLIER = 3 / 4;
/**
* Class for a marker.
* @param {!WorkspaceSvg} workspace The workspace the marker belongs to.
* @param {!ConstantProvider} constants The constants for
* the renderer.
* @param {!Marker} marker The marker to draw.
* @constructor
* @alias Blockly.blockRendering.MarkerSvg
*/
const MarkerSvg = function(workspace, constants, marker) {
/**
* The workspace the marker belongs to.
* @type {!WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* The marker to draw.
* @type {!Marker}
* @private
*/
this.marker_ = marker;
/**
* The workspace, field, or block that the marker SVG element should be
* attached to.
* @type {IASTNodeLocationSvg}
* @private
*/
this.parent_ = null;
/**
* The constants necessary to draw the marker.
* @type {ConstantProvider}
* @protected
*/
this.constants_ = constants;
/**
* The current SVG element for the marker.
* @type {Element}
*/
this.currentMarkerSvg = null;
const defaultColour = this.isCursor() ? this.constants_.CURSOR_COLOUR :
this.constants_.MARKER_COLOUR;
/**
* The colour of the marker.
* @type {string}
*/
this.colour_ = marker.colour || defaultColour;
};
/**
* Return the root node of the SVG or null if none exists.
* @return {SVGElement} The root SVG node.
*/
MarkerSvg.prototype.getSvgRoot = function() {
return this.svgGroup_;
};
/**
* Get the marker.
* @return {!Marker} The marker to draw for.
*/
MarkerSvg.prototype.getMarker = function() {
return this.marker_;
};
/**
* True if the marker should be drawn as a cursor, false otherwise.
* A cursor is drawn as a flashing line. A marker is drawn as a solid line.
* @return {boolean} True if the marker is a cursor, false otherwise.
*/
MarkerSvg.prototype.isCursor = function() {
return this.marker_.type === 'cursor';
};
/**
* Create the DOM element for the marker.
* @return {!SVGElement} The marker controls SVG group.
* @package
*/
MarkerSvg.prototype.createDom = function() {
const className = this.isCursor() ? CURSOR_CLASS : MARKER_CLASS;
this.svgGroup_ = dom.createSvgElement(Svg.G, {'class': className}, null);
this.createDomInternal_();
return this.svgGroup_;
};
/**
* Attaches the SVG root of the marker to the SVG group of the parent.
* @param {!IASTNodeLocationSvg} newParent The workspace, field, or
* block that the marker SVG element should be attached to.
* @protected
*/
MarkerSvg.prototype.setParent_ = function(newParent) {
if (!this.isCursor()) {
if (this.parent_) {
this.parent_.setMarkerSvg(null);
}
newParent.setMarkerSvg(this.getSvgRoot());
} else {
if (this.parent_) {
this.parent_.setCursorSvg(null);
}
newParent.setCursorSvg(this.getSvgRoot());
}
this.parent_ = newParent;
};
/**
* Update the marker.
* @param {ASTNode} oldNode The previous node the marker was on or null.
* @param {ASTNode} curNode The node that we want to draw the marker for.
*/
MarkerSvg.prototype.draw = function(oldNode, curNode) {
if (!curNode) {
this.hide();
return;
}
this.constants_ = this.workspace_.getRenderer().getConstants();
const defaultColour = this.isCursor() ? this.constants_.CURSOR_COLOUR :
this.constants_.MARKER_COLOUR;
this.colour_ = this.marker_.colour || defaultColour;
this.applyColour_(curNode);
this.showAtLocation_(curNode);
this.fireMarkerEvent_(oldNode, curNode);
// Ensures the marker will be visible immediately after the move.
const animate = this.currentMarkerSvg.childNodes[0];
if (animate !== undefined) {
animate.beginElement && animate.beginElement();
}
};
/**
* Update the marker's visible state based on the type of curNode..
* @param {!ASTNode} curNode The node that we want to draw the marker for.
* @protected
*/
MarkerSvg.prototype.showAtLocation_ = function(curNode) {
const curNodeAsConnection =
/** @type {!Connection} */ (curNode.getLocation());
const connectionType = curNodeAsConnection.type;
if (curNode.getType() === ASTNode.types.BLOCK) {
this.showWithBlock_(curNode);
} else if (curNode.getType() === ASTNode.types.OUTPUT) {
this.showWithOutput_(curNode);
} else if (connectionType === ConnectionType.INPUT_VALUE) {
this.showWithInput_(curNode);
} else if (connectionType === ConnectionType.NEXT_STATEMENT) {
this.showWithNext_(curNode);
} else if (curNode.getType() === ASTNode.types.PREVIOUS) {
this.showWithPrevious_(curNode);
} else if (curNode.getType() === ASTNode.types.FIELD) {
this.showWithField_(curNode);
} else if (curNode.getType() === ASTNode.types.WORKSPACE) {
this.showWithCoordinates_(curNode);
} else if (curNode.getType() === ASTNode.types.STACK) {
this.showWithStack_(curNode);
}
};
/**************************
* Display
**************************/
/**
* Show the marker as a combination of the previous connection and block,
* the output connection and block, or just the block.
* @param {!ASTNode} curNode The node to draw the marker for.
* @private
*/
MarkerSvg.prototype.showWithBlockPrevOutput_ = function(curNode) {
const block = /** @type {!BlockSvg} */ (curNode.getSourceBlock());
const width = block.width;
const height = block.height;
const markerHeight = height * HEIGHT_MULTIPLIER;
const markerOffset = this.constants_.CURSOR_BLOCK_PADDING;
if (block.previousConnection) {
const connectionShape = this.constants_.shapeFor(block.previousConnection);
this.positionPrevious_(width, markerOffset, markerHeight, connectionShape);
} else if (block.outputConnection) {
const connectionShape = this.constants_.shapeFor(block.outputConnection);
this.positionOutput_(width, height, connectionShape);
} else {
this.positionBlock_(width, markerOffset, markerHeight);
}
this.setParent_(block);
this.showCurrent_();
};
/**
* Position and display the marker for a block.
* @param {!ASTNode} curNode The node to draw the marker for.
* @protected
*/
MarkerSvg.prototype.showWithBlock_ = function(curNode) {
this.showWithBlockPrevOutput_(curNode);
};
/**
* Position and display the marker for a previous connection.
* @param {!ASTNode} curNode The node to draw the marker for.
* @protected
*/
MarkerSvg.prototype.showWithPrevious_ = function(curNode) {
this.showWithBlockPrevOutput_(curNode);
};
/**
* Position and display the marker for an output connection.
* @param {!ASTNode} curNode The node to draw the marker for.
* @protected
*/
MarkerSvg.prototype.showWithOutput_ = function(curNode) {
this.showWithBlockPrevOutput_(curNode);
};
/**
* Position and display the marker for a workspace coordinate.
* This is a horizontal line.
* @param {!ASTNode} curNode The node to draw the marker for.
* @protected
*/
MarkerSvg.prototype.showWithCoordinates_ = function(curNode) {
const wsCoordinate = curNode.getWsCoordinate();
let x = wsCoordinate.x;
const y = wsCoordinate.y;
if (this.workspace_.RTL) {
x -= this.constants_.CURSOR_WS_WIDTH;
}
this.positionLine_(x, y, this.constants_.CURSOR_WS_WIDTH);
this.setParent_(this.workspace_);
this.showCurrent_();
};
/**
* Position and display the marker for a field.
* This is a box around the field.
* @param {!ASTNode} curNode The node to draw the marker for.
* @protected
*/
MarkerSvg.prototype.showWithField_ = function(curNode) {
const field = /** @type {Field} */ (curNode.getLocation());
const width = field.getSize().width;
const height = field.getSize().height;
this.positionRect_(0, 0, width, height);
this.setParent_(field);
this.showCurrent_();
};
/**
* Position and display the marker for an input.
* This is a puzzle piece.
* @param {!ASTNode} curNode The node to draw the marker for.
* @protected
*/
MarkerSvg.prototype.showWithInput_ = function(curNode) {
const connection = /** @type {RenderedConnection} */
(curNode.getLocation());
const sourceBlock = /** @type {!BlockSvg} */ (connection.getSourceBlock());
this.positionInput_(connection);
this.setParent_(sourceBlock);
this.showCurrent_();
};
/**
* Position and display the marker for a next connection.
* This is a horizontal line.
* @param {!ASTNode} curNode The node to draw the marker for.
* @protected
*/
MarkerSvg.prototype.showWithNext_ = function(curNode) {
const connection =
/** @type {!RenderedConnection} */ (curNode.getLocation());
const targetBlock =
/** @type {BlockSvg} */ (connection.getSourceBlock());
let x = 0;
const y = connection.getOffsetInBlock().y;
const width = targetBlock.getHeightWidth().width;
if (this.workspace_.RTL) {
x = -width;
}
this.positionLine_(x, y, width);
this.setParent_(targetBlock);
this.showCurrent_();
};
/**
* Position and display the marker for a stack.
* This is a box with extra padding around the entire stack of blocks.
* @param {!ASTNode} curNode The node to draw the marker for.
* @protected
*/
MarkerSvg.prototype.showWithStack_ = function(curNode) {
const block = /** @type {BlockSvg} */ (curNode.getLocation());
// Gets the height and width of entire stack.
const heightWidth = block.getHeightWidth();
// Add padding so that being on a stack looks different than being on a block.
const width = heightWidth.width + this.constants_.CURSOR_STACK_PADDING;
const height = heightWidth.height + this.constants_.CURSOR_STACK_PADDING;
// Shift the rectangle slightly to upper left so padding is equal on all
// sides.
const xPadding = -this.constants_.CURSOR_STACK_PADDING / 2;
const yPadding = -this.constants_.CURSOR_STACK_PADDING / 2;
let x = xPadding;
const y = yPadding;
if (this.workspace_.RTL) {
x = -(width + xPadding);
}
this.positionRect_(x, y, width, height);
this.setParent_(block);
this.showCurrent_();
};
/**
* Show the current marker.
* @protected
*/
MarkerSvg.prototype.showCurrent_ = function() {
this.hide();
this.currentMarkerSvg.style.display = '';
};
/**************************
* Position
**************************/
/**
* Position the marker for a block.
* Displays an outline of the top half of a rectangle around a block.
* @param {number} width The width of the block.
* @param {number} markerOffset The extra padding for around the block.
* @param {number} markerHeight The height of the marker.
* @protected
*/
MarkerSvg.prototype.positionBlock_ = function(
width, markerOffset, markerHeight) {
const markerPath = svgPaths.moveBy(-markerOffset, markerHeight) +
svgPaths.lineOnAxis('V', -markerOffset) +
svgPaths.lineOnAxis('H', width + markerOffset * 2) +
svgPaths.lineOnAxis('V', markerHeight);
this.markerBlock_.setAttribute('d', markerPath);
if (this.workspace_.RTL) {
this.flipRtl_(this.markerBlock_);
}
this.currentMarkerSvg = this.markerBlock_;
};
/**
* Position the marker for an input connection.
* Displays a filled in puzzle piece.
* @param {!RenderedConnection} connection The connection to position
* marker around.
* @protected
*/
MarkerSvg.prototype.positionInput_ = function(connection) {
const x = connection.getOffsetInBlock().x;
const y = connection.getOffsetInBlock().y;
const path =
svgPaths.moveTo(0, 0) + this.constants_.shapeFor(connection).pathDown;
this.markerInput_.setAttribute('d', path);
this.markerInput_.setAttribute(
'transform',
'translate(' + x + ',' + y + ')' +
(this.workspace_.RTL ? ' scale(-1 1)' : ''));
this.currentMarkerSvg = this.markerInput_;
};
/**
* Move and show the marker at the specified coordinate in workspace units.
* Displays a horizontal line.
* @param {number} x The new x, in workspace units.
* @param {number} y The new y, in workspace units.
* @param {number} width The new width, in workspace units.
* @protected
*/
MarkerSvg.prototype.positionLine_ = function(x, y, width) {
this.markerSvgLine_.setAttribute('x', x);
this.markerSvgLine_.setAttribute('y', y);
this.markerSvgLine_.setAttribute('width', width);
this.currentMarkerSvg = this.markerSvgLine_;
};
/**
* Position the marker for an output connection.
* Displays a puzzle outline and the top and bottom path.
* @param {number} width The width of the block.
* @param {number} height The height of the block.
* @param {!Object} connectionShape The shape object for the connection.
* @protected
*/
MarkerSvg.prototype.positionOutput_ = function(width, height, connectionShape) {
const markerPath = svgPaths.moveBy(width, 0) +
svgPaths.lineOnAxis('h', -(width - connectionShape.width)) +
svgPaths.lineOnAxis('v', this.constants_.TAB_OFFSET_FROM_TOP) +
connectionShape.pathDown + svgPaths.lineOnAxis('V', height) +
svgPaths.lineOnAxis('H', width);
this.markerBlock_.setAttribute('d', markerPath);
if (this.workspace_.RTL) {
this.flipRtl_(this.markerBlock_);
}
this.currentMarkerSvg = this.markerBlock_;
};
/**
* Position the marker for a previous connection.
* Displays a half rectangle with a notch in the top to represent the previous
* connection.
* @param {number} width The width of the block.
* @param {number} markerOffset The offset of the marker from around the block.
* @param {number} markerHeight The height of the marker.
* @param {!Object} connectionShape The shape object for the connection.
* @protected
*/
MarkerSvg.prototype.positionPrevious_ = function(
width, markerOffset, markerHeight, connectionShape) {
const markerPath = svgPaths.moveBy(-markerOffset, markerHeight) +
svgPaths.lineOnAxis('V', -markerOffset) +
svgPaths.lineOnAxis('H', this.constants_.NOTCH_OFFSET_LEFT) +
connectionShape.pathLeft +
svgPaths.lineOnAxis('H', width + markerOffset * 2) +
svgPaths.lineOnAxis('V', markerHeight);
this.markerBlock_.setAttribute('d', markerPath);
if (this.workspace_.RTL) {
this.flipRtl_(this.markerBlock_);
}
this.currentMarkerSvg = this.markerBlock_;
};
/**
* Move and show the marker at the specified coordinate in workspace units.
* Displays a filled in rectangle.
* @param {number} x The new x, in workspace units.
* @param {number} y The new y, in workspace units.
* @param {number} width The new width, in workspace units.
* @param {number} height The new height, in workspace units.
* @protected
*/
MarkerSvg.prototype.positionRect_ = function(x, y, width, height) {
this.markerSvgRect_.setAttribute('x', x);
this.markerSvgRect_.setAttribute('y', y);
this.markerSvgRect_.setAttribute('width', width);
this.markerSvgRect_.setAttribute('height', height);
this.currentMarkerSvg = this.markerSvgRect_;
};
/**
* Flip the SVG paths in RTL.
* @param {!SVGElement} markerSvg The marker that we want to flip.
* @private
*/
MarkerSvg.prototype.flipRtl_ = function(markerSvg) {
markerSvg.setAttribute('transform', 'scale(-1 1)');
};
/**
* Hide the marker.
*/
MarkerSvg.prototype.hide = function() {
this.markerSvgLine_.style.display = 'none';
this.markerSvgRect_.style.display = 'none';
this.markerInput_.style.display = 'none';
this.markerBlock_.style.display = 'none';
};
/**
* Fire event for the marker or marker.
* @param {ASTNode} oldNode The old node the marker used to be on.
* @param {!ASTNode} curNode The new node the marker is currently on.
* @private
*/
MarkerSvg.prototype.fireMarkerEvent_ = function(oldNode, curNode) {
const curBlock = curNode.getSourceBlock();
const event = new (eventUtils.get(eventUtils.MARKER_MOVE))(
curBlock, this.isCursor(), oldNode, curNode);
eventUtils.fire(event);
};
/**
* Get the properties to make a marker blink.
* @return {!Object} The object holding attributes to make the marker blink.
* @protected
*/
MarkerSvg.prototype.getBlinkProperties_ = function() {
return {
'attributeType': 'XML',
'attributeName': 'fill',
'dur': '1s',
'values': this.colour_ + ';transparent;transparent;',
'repeatCount': 'indefinite',
};
};
/**
* Create the marker SVG.
* @return {Element} The SVG node created.
* @protected
*/
MarkerSvg.prototype.createDomInternal_ = function() {
/* This markup will be generated and added to the .svgGroup_:
<g>
<rect width="100" height="5">
<animate attributeType="XML" attributeName="fill" dur="1s"
values="transparent;transparent;#fff;transparent"
repeatCount="indefinite" />
</rect>
</g>
*/
this.markerSvg_ = dom.createSvgElement(
Svg.G, {
'width': this.constants_.CURSOR_WS_WIDTH,
'height': this.constants_.WS_CURSOR_HEIGHT,
},
this.svgGroup_);
// A horizontal line used to represent a workspace coordinate or next
// connection.
this.markerSvgLine_ = dom.createSvgElement(
Svg.RECT, {
'width': this.constants_.CURSOR_WS_WIDTH,
'height': this.constants_.WS_CURSOR_HEIGHT,
'style': 'display: none',
},
this.markerSvg_);
// A filled in rectangle used to represent a stack.
this.markerSvgRect_ = dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyVerticalMarker',
'rx': 10,
'ry': 10,
'style': 'display: none',
},
this.markerSvg_);
// A filled in puzzle piece used to represent an input value.
this.markerInput_ = dom.createSvgElement(
Svg.PATH, {'transform': '', 'style': 'display: none'}, this.markerSvg_);
// A path used to represent a previous connection and a block, an output
// connection and a block, or a block.
this.markerBlock_ = dom.createSvgElement(
Svg.PATH, {
'transform': '',
'style': 'display: none',
'fill': 'none',
'stroke-width': this.constants_.CURSOR_STROKE_WIDTH,
},
this.markerSvg_);
// Markers and stack markers don't blink.
if (this.isCursor()) {
const blinkProperties = this.getBlinkProperties_();
dom.createSvgElement(Svg.ANIMATE, blinkProperties, this.markerSvgLine_);
dom.createSvgElement(Svg.ANIMATE, blinkProperties, this.markerInput_);
blinkProperties['attributeName'] = 'stroke';
dom.createSvgElement(Svg.ANIMATE, blinkProperties, this.markerBlock_);
}
return this.markerSvg_;
};
/**
* Apply the marker's colour.
* @param {!ASTNode} _curNode The node that we want to draw the marker
* for.
* @protected
*/
MarkerSvg.prototype.applyColour_ = function(_curNode) {
this.markerSvgLine_.setAttribute('fill', this.colour_);
this.markerSvgRect_.setAttribute('stroke', this.colour_);
this.markerInput_.setAttribute('fill', this.colour_);
this.markerBlock_.setAttribute('stroke', this.colour_);
if (this.isCursor()) {
const values = this.colour_ + ';transparent;transparent;';
this.markerSvgLine_.firstChild.setAttribute('values', values);
this.markerInput_.firstChild.setAttribute('values', values);
this.markerBlock_.firstChild.setAttribute('values', values);
}
};
/**
* Dispose of this marker.
*/
MarkerSvg.prototype.dispose = function() {
if (this.svgGroup_) {
dom.removeNode(this.svgGroup_);
}
};
exports.MarkerSvg = MarkerSvg;