refactor: convert some classes to es6 classes (#5873)

* refactor: convert flyout_button.js to ES6 class

* chore: move some properties into constructor and add annotations for flyout base

* refactor: convert flyout_horizontal.js to ES6 class

* refactor: convert flyout_vertical.js to ES6 class

* refactor: convert flyout_base.js to ES6 class

* refactor: convert flyout_metrics_manager.js to ES6 class

* refactor: convert insertion_marker_manager.js to ES6 class

* refactor: convert metrics_manager.js to ES6 class

* refactor: convert grid.js to ES6 class

* refactor: convert input.js to ES6 class

* refactor: convert touch_gesture.js to ES6 class

* refactor: convert gesture.js to ES6 class

* refactor: convert trashcan.js to ES6 class

* chore: rebuild and run format

* chore: fix review comments

* chore: respond to PR comments
This commit is contained in:
Rachel Fenichel
2022-01-25 15:26:20 -08:00
committed by GitHub
parent 3f4f505791
commit efdb6549d9
13 changed files with 5430 additions and 5309 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -29,286 +29,303 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/**
* Class for a button in the flyout.
* @param {!WorkspaceSvg} workspace The workspace in which to place this
* button.
* @param {!WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {!toolbox.ButtonOrLabelInfo} json
* The JSON specifying the label/button.
* @param {boolean} isLabel Whether this button should be styled as a label.
* @constructor
* @package
* @alias Blockly.FlyoutButton
* Class for a button or label in the flyout.
*/
const FlyoutButton = function(workspace, targetWorkspace, json, isLabel) {
// Labels behave the same as buttons, but are styled differently.
class FlyoutButton {
/**
* @param {!WorkspaceSvg} workspace The workspace in which to place this
* button.
* @param {!WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {!toolbox.ButtonOrLabelInfo} json
* The JSON specifying the label/button.
* @param {boolean} isLabel Whether this button should be styled as a label.
* @package
* @alias Blockly.FlyoutButton
*/
constructor(workspace, targetWorkspace, json, isLabel) {
/**
* @type {!WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* @type {!WorkspaceSvg}
* @private
*/
this.targetWorkspace_ = targetWorkspace;
/**
* @type {string}
* @private
*/
this.text_ = json['text'];
/**
* @type {!Coordinate}
* @private
*/
this.position_ = new Coordinate(0, 0);
/**
* Whether this button should be styled as a label.
* Labels behave the same as buttons, but are styled differently.
* @type {boolean}
* @private
*/
this.isLabel_ = isLabel;
/**
* The key to the function called when this button is clicked.
* @type {string}
* @private
*/
this.callbackKey_ = json['callbackKey'] ||
/* Check the lower case version too to satisfy IE */
json['callbackkey'];
/**
* If specified, a CSS class to add to this button.
* @type {?string}
* @private
*/
this.cssClass_ = json['web-class'] || null;
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseUpWrapper_ = null;
/**
* The JSON specifying the label / button.
* @type {!toolbox.ButtonOrLabelInfo}
*/
this.info = json;
/**
* The width of the button's rect.
* @type {number}
*/
this.width = 0;
/**
* The height of the button's rect.
* @type {number}
*/
this.height = 0;
/**
* The root SVG group for the button or label.
* @type {?SVGGElement}
* @private
*/
this.svgGroup_ = null;
/**
* The SVG element with the text of the label or button.
* @type {?SVGTextElement}
* @private
*/
this.svgText_ = null;
}
/**
* @type {!WorkspaceSvg}
* Create the button elements.
* @return {!SVGElement} The button's SVG group.
*/
createDom() {
let cssClass = this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton';
if (this.cssClass_) {
cssClass += ' ' + this.cssClass_;
}
this.svgGroup_ = dom.createSvgElement(
Svg.G, {'class': cssClass}, this.workspace_.getCanvas());
let shadow;
if (!this.isLabel_) {
// Shadow rectangle (light source does not mirror in RTL).
shadow = dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyFlyoutButtonShadow',
'rx': 4,
'ry': 4,
'x': 1,
'y': 1,
},
this.svgGroup_);
}
// Background rectangle.
const rect = dom.createSvgElement(
Svg.RECT, {
'class': this.isLabel_ ? 'blocklyFlyoutLabelBackground' :
'blocklyFlyoutButtonBackground',
'rx': 4,
'ry': 4,
},
this.svgGroup_);
const svgText = dom.createSvgElement(
Svg.TEXT, {
'class': this.isLabel_ ? 'blocklyFlyoutLabelText' : 'blocklyText',
'x': 0,
'y': 0,
'text-anchor': 'middle',
},
this.svgGroup_);
let text = parsing.replaceMessageReferences(this.text_);
if (this.workspace_.RTL) {
// Force text to be RTL by adding an RLM.
text += '\u200F';
}
svgText.textContent = text;
if (this.isLabel_) {
this.svgText_ = svgText;
this.workspace_.getThemeManager().subscribe(
this.svgText_, 'flyoutForegroundColour', 'fill');
}
const fontSize = style.getComputedStyle(svgText, 'fontSize');
const fontWeight = style.getComputedStyle(svgText, 'fontWeight');
const fontFamily = style.getComputedStyle(svgText, 'fontFamily');
this.width = dom.getFastTextWidthWithSizeString(
svgText, fontSize, fontWeight, fontFamily);
const fontMetrics =
dom.measureFontMetrics(text, fontSize, fontWeight, fontFamily);
this.height = fontMetrics.height;
if (!this.isLabel_) {
this.width += 2 * FlyoutButton.TEXT_MARGIN_X;
this.height += 2 * FlyoutButton.TEXT_MARGIN_Y;
shadow.setAttribute('width', this.width);
shadow.setAttribute('height', this.height);
}
rect.setAttribute('width', this.width);
rect.setAttribute('height', this.height);
svgText.setAttribute('x', this.width / 2);
svgText.setAttribute(
'y', this.height / 2 - fontMetrics.height / 2 + fontMetrics.baseline);
this.updateTransform_();
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
this.svgGroup_, 'mouseup', this, this.onMouseUp_);
return this.svgGroup_;
}
/**
* Correctly position the flyout button and make it visible.
*/
show() {
this.updateTransform_();
this.svgGroup_.setAttribute('display', 'block');
}
/**
* Update SVG attributes to match internal state.
* @private
*/
this.workspace_ = workspace;
updateTransform_() {
this.svgGroup_.setAttribute(
'transform',
'translate(' + this.position_.x + ',' + this.position_.y + ')');
}
/**
* @type {!WorkspaceSvg}
* Move the button to the given x, y coordinates.
* @param {number} x The new x coordinate.
* @param {number} y The new y coordinate.
*/
moveTo(x, y) {
this.position_.x = x;
this.position_.y = y;
this.updateTransform_();
}
/**
* @return {boolean} Whether or not the button is a label.
*/
isLabel() {
return this.isLabel_;
}
/**
* Location of the button.
* @return {!Coordinate} x, y coordinates.
* @package
*/
getPosition() {
return this.position_;
}
/**
* @return {string} Text of the button.
*/
getButtonText() {
return this.text_;
}
/**
* Get the button's target workspace.
* @return {!WorkspaceSvg} The target workspace of the flyout where this
* button resides.
*/
getTargetWorkspace() {
return this.targetWorkspace_;
}
/**
* Dispose of this button.
*/
dispose() {
if (this.onMouseUpWrapper_) {
browserEvents.unbind(this.onMouseUpWrapper_);
}
if (this.svgGroup_) {
dom.removeNode(this.svgGroup_);
}
if (this.svgText_) {
this.workspace_.getThemeManager().unsubscribe(this.svgText_);
}
}
/**
* Do something when the button is clicked.
* @param {!Event} e Mouse up event.
* @private
*/
this.targetWorkspace_ = targetWorkspace;
onMouseUp_(e) {
const gesture = this.targetWorkspace_.getGesture(e);
if (gesture) {
gesture.cancel();
}
/**
* @type {string}
* @private
*/
this.text_ = json['text'];
/**
* @type {!Coordinate}
* @private
*/
this.position_ = new Coordinate(0, 0);
/**
* Whether this button should be styled as a label.
* @type {boolean}
* @private
*/
this.isLabel_ = isLabel;
/**
* The key to the function called when this button is clicked.
* @type {string}
* @private
*/
this.callbackKey_ = json['callbackKey'] ||
/* Check the lower case version too to satisfy IE */
json['callbackkey'];
/**
* If specified, a CSS class to add to this button.
* @type {?string}
* @private
*/
this.cssClass_ = json['web-class'] || null;
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseUpWrapper_ = null;
/**
* The JSON specifying the label / button.
* @type {!toolbox.ButtonOrLabelInfo}
*/
this.info = json;
/**
* The width of the button's rect.
* @type {number}
*/
this.width = 0;
/**
* The height of the button's rect.
* @type {number}
*/
this.height = 0;
};
if (this.isLabel_ && this.callbackKey_) {
console.warn(
'Labels should not have callbacks. Label text: ' + this.text_);
} else if (
!this.isLabel_ &&
!(this.callbackKey_ &&
this.targetWorkspace_.getButtonCallback(this.callbackKey_))) {
console.warn('Buttons should have callbacks. Button text: ' + this.text_);
} else if (!this.isLabel_) {
this.targetWorkspace_.getButtonCallback(this.callbackKey_)(this);
}
}
}
/**
* The horizontal margin around the text in the button.
*/
FlyoutButton.MARGIN_X = 5;
FlyoutButton.TEXT_MARGIN_X = 5;
/**
* The vertical margin around the text in the button.
*/
FlyoutButton.MARGIN_Y = 2;
/**
* Create the button elements.
* @return {!SVGElement} The button's SVG group.
*/
FlyoutButton.prototype.createDom = function() {
let cssClass = this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton';
if (this.cssClass_) {
cssClass += ' ' + this.cssClass_;
}
this.svgGroup_ = dom.createSvgElement(
Svg.G, {'class': cssClass}, this.workspace_.getCanvas());
let shadow;
if (!this.isLabel_) {
// Shadow rectangle (light source does not mirror in RTL).
shadow = dom.createSvgElement(
Svg.RECT, {
'class': 'blocklyFlyoutButtonShadow',
'rx': 4,
'ry': 4,
'x': 1,
'y': 1,
},
this.svgGroup_);
}
// Background rectangle.
const rect = dom.createSvgElement(
Svg.RECT, {
'class': this.isLabel_ ? 'blocklyFlyoutLabelBackground' :
'blocklyFlyoutButtonBackground',
'rx': 4,
'ry': 4,
},
this.svgGroup_);
const svgText = dom.createSvgElement(
Svg.TEXT, {
'class': this.isLabel_ ? 'blocklyFlyoutLabelText' : 'blocklyText',
'x': 0,
'y': 0,
'text-anchor': 'middle',
},
this.svgGroup_);
let text = parsing.replaceMessageReferences(this.text_);
if (this.workspace_.RTL) {
// Force text to be RTL by adding an RLM.
text += '\u200F';
}
svgText.textContent = text;
if (this.isLabel_) {
this.svgText_ = svgText;
this.workspace_.getThemeManager().subscribe(
this.svgText_, 'flyoutForegroundColour', 'fill');
}
const fontSize = style.getComputedStyle(svgText, 'fontSize');
const fontWeight = style.getComputedStyle(svgText, 'fontWeight');
const fontFamily = style.getComputedStyle(svgText, 'fontFamily');
this.width = dom.getFastTextWidthWithSizeString(
svgText, fontSize, fontWeight, fontFamily);
const fontMetrics =
dom.measureFontMetrics(text, fontSize, fontWeight, fontFamily);
this.height = fontMetrics.height;
if (!this.isLabel_) {
this.width += 2 * FlyoutButton.MARGIN_X;
this.height += 2 * FlyoutButton.MARGIN_Y;
shadow.setAttribute('width', this.width);
shadow.setAttribute('height', this.height);
}
rect.setAttribute('width', this.width);
rect.setAttribute('height', this.height);
svgText.setAttribute('x', this.width / 2);
svgText.setAttribute(
'y', this.height / 2 - fontMetrics.height / 2 + fontMetrics.baseline);
this.updateTransform_();
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
this.svgGroup_, 'mouseup', this, this.onMouseUp_);
return this.svgGroup_;
};
/**
* Correctly position the flyout button and make it visible.
*/
FlyoutButton.prototype.show = function() {
this.updateTransform_();
this.svgGroup_.setAttribute('display', 'block');
};
/**
* Update SVG attributes to match internal state.
* @private
*/
FlyoutButton.prototype.updateTransform_ = function() {
this.svgGroup_.setAttribute(
'transform',
'translate(' + this.position_.x + ',' + this.position_.y + ')');
};
/**
* Move the button to the given x, y coordinates.
* @param {number} x The new x coordinate.
* @param {number} y The new y coordinate.
*/
FlyoutButton.prototype.moveTo = function(x, y) {
this.position_.x = x;
this.position_.y = y;
this.updateTransform_();
};
/**
* @return {boolean} Whether or not the button is a label.
*/
FlyoutButton.prototype.isLabel = function() {
return this.isLabel_;
};
/**
* Location of the button.
* @return {!Coordinate} x, y coordinates.
* @package
*/
FlyoutButton.prototype.getPosition = function() {
return this.position_;
};
/**
* @return {string} Text of the button.
*/
FlyoutButton.prototype.getButtonText = function() {
return this.text_;
};
/**
* Get the button's target workspace.
* @return {!WorkspaceSvg} The target workspace of the flyout where this
* button resides.
*/
FlyoutButton.prototype.getTargetWorkspace = function() {
return this.targetWorkspace_;
};
/**
* Dispose of this button.
*/
FlyoutButton.prototype.dispose = function() {
if (this.onMouseUpWrapper_) {
browserEvents.unbind(this.onMouseUpWrapper_);
}
if (this.svgGroup_) {
dom.removeNode(this.svgGroup_);
}
if (this.svgText_) {
this.workspace_.getThemeManager().unsubscribe(this.svgText_);
}
};
/**
* Do something when the button is clicked.
* @param {!Event} e Mouse up event.
* @private
*/
FlyoutButton.prototype.onMouseUp_ = function(e) {
const gesture = this.targetWorkspace_.getGesture(e);
if (gesture) {
gesture.cancel();
}
if (this.isLabel_ && this.callbackKey_) {
console.warn('Labels should not have callbacks. Label text: ' + this.text_);
} else if (
!this.isLabel_ &&
!(this.callbackKey_ &&
this.targetWorkspace_.getButtonCallback(this.callbackKey_))) {
console.warn('Buttons should have callbacks. Button text: ' + this.text_);
} else if (!this.isLabel_) {
this.targetWorkspace_.getButtonCallback(this.callbackKey_)(this);
}
};
FlyoutButton.TEXT_MARGIN_Y = 2;
/**
* CSS for buttons and labels. See css.js for use.

View File

@@ -17,7 +17,6 @@ goog.module('Blockly.HorizontalFlyout');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const browserEvents = goog.require('Blockly.browserEvents');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const toolbox = goog.require('Blockly.utils.toolbox');
/* eslint-disable-next-line no-unused-vars */
@@ -32,355 +31,358 @@ const {Scrollbar} = goog.require('Blockly.Scrollbar');
/**
* Class for a flyout.
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
* @extends {Flyout}
* @constructor
* @alias Blockly.HorizontalFlyout
*/
const HorizontalFlyout = function(workspaceOptions) {
HorizontalFlyout.superClass_.constructor.call(this, workspaceOptions);
this.horizontalLayout = true;
};
object.inherits(HorizontalFlyout, Flyout);
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling and a
* similar x property.
* @protected
*/
HorizontalFlyout.prototype.setMetrics_ = function(xyRatio) {
if (!this.isVisible()) {
return;
class HorizontalFlyout extends Flyout {
/**
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
* @alias Blockly.HorizontalFlyout
*/
constructor(workspaceOptions) {
super(workspaceOptions);
this.horizontalLayout = true;
}
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a
* float between 0 and 1 specifying the degree of scrolling and a similar
* x property.
* @protected
*/
setMetrics_(xyRatio) {
if (!this.isVisible()) {
return;
}
if (typeof xyRatio.x === 'number') {
this.workspace_.scrollX =
-(scrollMetrics.left +
(scrollMetrics.width - viewMetrics.width) * xyRatio.x);
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
if (typeof xyRatio.x === 'number') {
this.workspace_.scrollX =
-(scrollMetrics.left +
(scrollMetrics.width - viewMetrics.width) * xyRatio.x);
}
this.workspace_.translate(
this.workspace_.scrollX + absoluteMetrics.left,
this.workspace_.scrollY + absoluteMetrics.top);
}
this.workspace_.translate(
this.workspace_.scrollX + absoluteMetrics.left,
this.workspace_.scrollY + absoluteMetrics.top);
};
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
*/
HorizontalFlyout.prototype.getX = function() {
// X is always 0 since this is a horizontal flyout.
return 0;
};
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
*/
HorizontalFlyout.prototype.getY = function() {
if (!this.isVisible()) {
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
*/
getX() {
// X is always 0 since this is a horizontal flyout.
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
let y = 0;
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
if (atTop) {
y = toolboxMetrics.height;
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
*/
getY() {
if (!this.isVisible()) {
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
let y = 0;
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
if (atTop) {
y = toolboxMetrics.height;
} else {
y = viewMetrics.height - this.height_;
}
// Simple (flyout-only) toolbox.
} else {
y = viewMetrics.height - this.height_;
if (atTop) {
y = 0;
} else {
// The simple flyout does not cover the workspace.
y = viewMetrics.height;
}
}
// Simple (flyout-only) toolbox.
// Trashcan flyout is opposite the main flyout.
} else {
if (atTop) {
y = 0;
} else {
// The simple flyout does not cover the workspace.
y = viewMetrics.height;
// Because the anchor point of the flyout is on the top, but we want
// to align the bottom edge of the flyout with the bottom edge of the
// blocklyDiv, we calculate the full height of the div minus the height
// of the flyout.
y = viewMetrics.height + absoluteMetrics.top - this.height_;
}
}
// Trashcan flyout is opposite the main flyout.
} else {
return y;
}
/**
* Move the flyout to the edge of the workspace.
*/
position() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the width for workspace metrics.
this.width_ = targetWorkspaceViewMetrics.width;
const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS;
const edgeHeight = this.height_ - this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
const x = this.getX();
const y = this.getY();
this.positionAt_(this.width_, this.height_, x, y);
}
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
setBackgroundPath_(width, height) {
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// Start at top left.
const path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
if (atTop) {
y = 0;
// Top.
path.push('h', width + 2 * this.CORNER_RADIUS);
// Right.
path.push('v', height);
// Bottom.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('h', -width);
// Left.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('z');
} else {
// Because the anchor point of the flyout is on the top, but we want
// to align the bottom edge of the flyout with the bottom edge of the
// blocklyDiv, we calculate the full height of the div minus the height
// of the flyout.
y = viewMetrics.height + absoluteMetrics.top - this.height_;
// Top.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('h', width);
// Right.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('v', height);
// Bottom.
path.push('h', -width - 2 * this.CORNER_RADIUS);
// Left.
path.push('z');
}
this.svgBackground_.setAttribute('d', path.join(' '));
}
/**
* Scroll the flyout to the top.
*/
scrollToStart() {
this.workspace_.scrollbar.setX(this.RTL ? Infinity : 0);
}
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @protected
*/
wheel_(e) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
const delta = scrollDelta.x || scrollDelta.y;
if (delta) {
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.left - scrollMetrics.left) + delta;
this.workspace_.scrollbar.setX(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and
// DropDownDiv.
WidgetDiv.hide();
DropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
}
/**
* Lay out the blocks in the flyout.
* @param {!Array<!Object>} contents The blocks and buttons to lay out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
*/
layout_(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
const margin = this.MARGIN;
let cursorX = margin + this.tabWidth_;
const cursorY = margin;
if (this.RTL) {
contents = contents.reverse();
}
for (let i = 0, item; (item = contents[i]); i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such
// a block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
// Figure out where to place the block.
const tab = block.outputConnection ? this.tabWidth_ : 0;
let moveX;
if (this.RTL) {
moveX = cursorX + blockHW.width;
} else {
moveX = cursorX - tab;
}
block.moveBy(moveX, cursorY);
const rect = this.createRect_(block, moveX, cursorY, blockHW, i);
cursorX += (blockHW.width + gaps[i]);
this.addBlockListeners_(root, block, rect);
} else if (item.type === 'button') {
this.initFlyoutButton_(item.button, cursorX, cursorY);
cursorX += (item.button.width + gaps[i]);
}
}
}
return y;
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
*/
isDragTowardWorkspace(currentDragDeltaXY) {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
const dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
/**
* Move the flyout to the edge of the workspace.
*/
HorizontalFlyout.prototype.position = function() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the width for workspace metrics.
this.width_ = targetWorkspaceViewMetrics.width;
const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS;
const edgeHeight = this.height_ - this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
const x = this.getX();
const y = this.getY();
this.positionAt_(this.width_, this.height_, x, y);
};
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
HorizontalFlyout.prototype.setBackgroundPath_ = function(width, height) {
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// Start at top left.
const path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
if (atTop) {
// Top.
path.push('h', width + 2 * this.CORNER_RADIUS);
// Right.
path.push('v', height);
// Bottom.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('h', -width);
// Left.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('z');
} else {
// Top.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('h', width);
// Right.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('v', height);
// Bottom.
path.push('h', -width - 2 * this.CORNER_RADIUS);
// Left.
path.push('z');
}
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Scroll the flyout to the top.
*/
HorizontalFlyout.prototype.scrollToStart = function() {
this.workspace_.scrollbar.setX(this.RTL ? Infinity : 0);
};
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @protected
*/
HorizontalFlyout.prototype.wheel_ = function(e) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
const delta = scrollDelta.x || scrollDelta.y;
if (delta) {
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.left - scrollMetrics.left) + delta;
this.workspace_.scrollbar.setX(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and DropDownDiv.
WidgetDiv.hide();
DropDownDiv.hideWithoutAnimation();
const range = this.dragAngleRange_;
// Check for up or down dragging.
if ((dragDirection < 90 + range && dragDirection > 90 - range) ||
(dragDirection > -90 - range && dragDirection < -90 + range)) {
return true;
}
return false;
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
};
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
getClientRect() {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
return null;
}
/**
* Lay out the blocks in the flyout.
* @param {!Array<!Object>} contents The blocks and buttons to lay out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
*/
HorizontalFlyout.prototype.layout_ = function(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
const margin = this.MARGIN;
let cursorX = margin + this.tabWidth_;
const cursorY = margin;
if (this.RTL) {
contents = contents.reverse();
}
const flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown
// flyout area are still deleted. Must be larger than the largest screen
// size, but be smaller than half Number.MAX_SAFE_INTEGER (not available on
// IE).
const BIG_NUM = 1000000000;
const top = flyoutRect.top;
for (let i = 0, item; (item = contents[i]); i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such a
// block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
// Figure out where to place the block.
const tab = block.outputConnection ? this.tabWidth_ : 0;
let moveX;
if (this.RTL) {
moveX = cursorX + blockHW.width;
} else {
moveX = cursorX - tab;
}
block.moveBy(moveX, cursorY);
const rect = this.createRect_(block, moveX, cursorY, blockHW, i);
cursorX += (blockHW.width + gaps[i]);
this.addBlockListeners_(root, block, rect);
} else if (item.type === 'button') {
this.initFlyoutButton_(item.button, cursorX, cursorY);
cursorX += (item.button.width + gaps[i]);
if (this.toolboxPosition_ === toolbox.Position.TOP) {
const height = flyoutRect.height;
return new Rect(-BIG_NUM, top + height, -BIG_NUM, BIG_NUM);
} else { // Bottom.
return new Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM);
}
}
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
*/
HorizontalFlyout.prototype.isDragTowardWorkspace = function(
currentDragDeltaXY) {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
const dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
const range = this.dragAngleRange_;
// Check for up or down dragging.
if ((dragDirection < 90 + range && dragDirection > 90 - range) ||
(dragDirection > -90 - range && dragDirection < -90 + range)) {
return true;
}
return false;
};
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
HorizontalFlyout.prototype.getClientRect = function() {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
return null;
}
const flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout
// area are still deleted. Must be larger than the largest screen size,
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
const BIG_NUM = 1000000000;
const top = flyoutRect.top;
if (this.toolboxPosition_ === toolbox.Position.TOP) {
const height = flyoutRect.height;
return new Rect(-BIG_NUM, top + height, -BIG_NUM, BIG_NUM);
} else { // Bottom.
return new Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM);
}
};
/**
* Compute height of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks right-aligned.
* @protected
*/
HorizontalFlyout.prototype.reflowInternal_ = function() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutHeight = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height);
}
const buttons = this.buttons_;
for (let i = 0, button; (button = buttons[i]); i++) {
flyoutHeight = Math.max(flyoutHeight, button.height);
}
flyoutHeight += this.MARGIN * 1.5;
flyoutHeight *= this.workspace_.scale;
flyoutHeight += Scrollbar.scrollbarThickness;
if (this.height_ !== flyoutHeight) {
/**
* Compute height of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks right-aligned.
* @protected
*/
reflowInternal_() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutHeight = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
if (block.flyoutRect_) {
this.moveRectToBlock_(block.flyoutRect_, block);
flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height);
}
const buttons = this.buttons_;
for (let i = 0, button; (button = buttons[i]); i++) {
flyoutHeight = Math.max(flyoutHeight, button.height);
}
flyoutHeight += this.MARGIN * 1.5;
flyoutHeight *= this.workspace_.scale;
flyoutHeight += Scrollbar.scrollbarThickness;
if (this.height_ !== flyoutHeight) {
for (let i = 0, block; (block = blocks[i]); i++) {
if (block.flyoutRect_) {
this.moveRectToBlock_(block.flyoutRect_, block);
}
}
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.TOP &&
!this.targetWorkspace.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that (0,0)
// is in the correct position relative to the new absolute edge (ie
// toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX,
this.targetWorkspace.scrollY + flyoutHeight);
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.TOP &&
!this.targetWorkspace.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that
// (0,0) is in the correct position relative to the new absolute edge
// (ie toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX,
this.targetWorkspace.scrollY + flyoutHeight);
}
// Record the height for workspace metrics and .position.
this.height_ = flyoutHeight;
this.position();
this.targetWorkspace.recordDragTargets();
// Record the height for workspace metrics and .position.
this.height_ = flyoutHeight;
this.position();
this.targetWorkspace.recordDragTargets();
}
}
};
}
registry.register(
registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, registry.DEFAULT,

View File

@@ -15,7 +15,6 @@
*/
goog.module('Blockly.FlyoutMetricsManager');
const object = goog.require('Blockly.utils.object');
/* eslint-disable-next-line no-unused-vars */
const {IFlyout} = goog.requireType('Blockly.IFlyout');
const {MetricsManager} = goog.require('Blockly.MetricsManager');
@@ -26,81 +25,82 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/**
* Calculates metrics for a flyout's workspace.
* The metrics are mainly used to size scrollbars for the flyout.
* @param {!WorkspaceSvg} workspace The flyout's workspace.
* @param {!IFlyout} flyout The flyout.
* @extends {MetricsManager}
* @constructor
* @alias Blockly.FlyoutMetricsManager
*/
const FlyoutMetricsManager = function(workspace, flyout) {
class FlyoutMetricsManager extends MetricsManager {
/**
* The flyout that owns the workspace to calculate metrics for.
* @type {!IFlyout}
* @protected
* @param {!WorkspaceSvg} workspace The flyout's workspace.
* @param {!IFlyout} flyout The flyout.
* @alias Blockly.FlyoutMetricsManager
*/
this.flyout_ = flyout;
constructor(workspace, flyout) {
super(workspace);
FlyoutMetricsManager.superClass_.constructor.call(this, workspace);
};
object.inherits(FlyoutMetricsManager, MetricsManager);
/**
* Gets the bounding box of the blocks on the flyout's workspace.
* This is in workspace coordinates.
* @return {!SVGRect|{height: number, y: number, width: number, x: number}} The
* bounding box of the blocks on the workspace.
* @private
*/
FlyoutMetricsManager.prototype.getBoundingBox_ = function() {
let blockBoundingBox;
try {
blockBoundingBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
// 2021 Update: It looks like this was fixed around Firefox 77 released in
// 2020.
blockBoundingBox = {height: 0, y: 0, width: 0, x: 0};
/**
* The flyout that owns the workspace to calculate metrics for.
* @type {!IFlyout}
* @protected
*/
this.flyout_ = flyout;
}
return blockBoundingBox;
};
/**
* @override
*/
FlyoutMetricsManager.prototype.getContentMetrics = function(
opt_getWorkspaceCoordinates) {
// The bounding box is in workspace coordinates.
const blockBoundingBox = this.getBoundingBox_();
const scale = opt_getWorkspaceCoordinates ? 1 : this.workspace_.scale;
/**
* Gets the bounding box of the blocks on the flyout's workspace.
* This is in workspace coordinates.
* @return {!SVGRect|{height: number, y: number, width: number, x: number}}
* The bounding box of the blocks on the workspace.
* @private
*/
getBoundingBox_() {
let blockBoundingBox;
try {
blockBoundingBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
// 2021 Update: It looks like this was fixed around Firefox 77 released in
// 2020.
blockBoundingBox = {height: 0, y: 0, width: 0, x: 0};
}
return blockBoundingBox;
}
return {
height: blockBoundingBox.height * scale,
width: blockBoundingBox.width * scale,
top: blockBoundingBox.y * scale,
left: blockBoundingBox.x * scale,
};
};
/**
* @override
*/
getContentMetrics(opt_getWorkspaceCoordinates) {
// The bounding box is in workspace coordinates.
const blockBoundingBox = this.getBoundingBox_();
const scale = opt_getWorkspaceCoordinates ? 1 : this.workspace_.scale;
/**
* @override
*/
FlyoutMetricsManager.prototype.getScrollMetrics = function(
opt_getWorkspaceCoordinates, opt_viewMetrics, opt_contentMetrics) {
const contentMetrics = opt_contentMetrics || this.getContentMetrics();
const margin = this.flyout_.MARGIN * this.workspace_.scale;
const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1;
return {
height: blockBoundingBox.height * scale,
width: blockBoundingBox.width * scale,
top: blockBoundingBox.y * scale,
left: blockBoundingBox.x * scale,
};
}
// The left padding isn't just the margin. Some blocks are also offset by
// tabWidth so that value and statement blocks line up.
// The contentMetrics.left value is equivalent to the variable left padding.
const leftPadding = contentMetrics.left;
/**
* @override
*/
getScrollMetrics(
opt_getWorkspaceCoordinates, opt_viewMetrics, opt_contentMetrics) {
const contentMetrics = opt_contentMetrics || this.getContentMetrics();
const margin = this.flyout_.MARGIN * this.workspace_.scale;
const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1;
return {
height: (contentMetrics.height + 2 * margin) / scale,
width: (contentMetrics.width + leftPadding + margin) / scale,
top: 0,
left: 0,
};
};
// The left padding isn't just the margin. Some blocks are also offset by
// tabWidth so that value and statement blocks line up.
// The contentMetrics.left value is equivalent to the variable left padding.
const leftPadding = contentMetrics.left;
return {
height: (contentMetrics.height + 2 * margin) / scale,
width: (contentMetrics.width + leftPadding + margin) / scale,
top: 0,
left: 0,
};
}
}
exports.FlyoutMetricsManager = FlyoutMetricsManager;

View File

@@ -17,7 +17,6 @@ goog.module('Blockly.VerticalFlyout');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const browserEvents = goog.require('Blockly.browserEvents');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const toolbox = goog.require('Blockly.utils.toolbox');
/* eslint-disable-next-line no-unused-vars */
@@ -36,16 +35,353 @@ goog.require('Blockly.constants');
/**
* Class for a flyout.
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
* @extends {Flyout}
* @constructor
* @alias Blockly.VerticalFlyout
*/
const VerticalFlyout = function(workspaceOptions) {
VerticalFlyout.superClass_.constructor.call(this, workspaceOptions);
};
object.inherits(VerticalFlyout, Flyout);
class VerticalFlyout extends Flyout {
/**
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
* @alias Blockly.VerticalFlyout
*/
constructor(workspaceOptions) {
super(workspaceOptions);
}
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a
* float between 0 and 1 specifying the degree of scrolling and a similar
* x property.
* @protected
*/
setMetrics_(xyRatio) {
if (!this.isVisible()) {
return;
}
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
if (typeof xyRatio.y === 'number') {
this.workspace_.scrollY =
-(scrollMetrics.top +
(scrollMetrics.height - viewMetrics.height) * xyRatio.y);
}
this.workspace_.translate(
this.workspace_.scrollX + absoluteMetrics.left,
this.workspace_.scrollY + absoluteMetrics.top);
}
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
*/
getX() {
if (!this.isVisible()) {
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
let x = 0;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = toolboxMetrics.width;
} else {
x = viewMetrics.width - this.width_;
}
// Simple (flyout-only) toolbox.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
} else {
// The simple flyout does not cover the workspace.
x = viewMetrics.width;
}
}
// Trashcan flyout is opposite the main flyout.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
} else {
// Because the anchor point of the flyout is on the left, but we want
// to align the right edge of the flyout with the right edge of the
// blocklyDiv, we calculate the full width of the div minus the width
// of the flyout.
x = viewMetrics.width + absoluteMetrics.left - this.width_;
}
}
return x;
}
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
*/
getY() {
// Y is always 0 since this is a vertical flyout.
return 0;
}
/**
* Move the flyout to the edge of the workspace.
*/
position() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the height for workspace metrics.
this.height_ = targetWorkspaceViewMetrics.height;
const edgeWidth = this.width_ - this.CORNER_RADIUS;
const edgeHeight =
targetWorkspaceViewMetrics.height - 2 * this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
const x = this.getX();
const y = this.getY();
this.positionAt_(this.width_, this.height_, x, y);
}
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
setBackgroundPath_(width, height) {
const atRight = this.toolboxPosition_ === toolbox.Position.RIGHT;
const totalWidth = width + this.CORNER_RADIUS;
// Decide whether to start on the left or right.
const path = ['M ' + (atRight ? totalWidth : 0) + ',0'];
// Top.
path.push('h', atRight ? -width : width);
// Rounded corner.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, this.CORNER_RADIUS);
// Side closest to workspace.
path.push('v', Math.max(0, height));
// Rounded corner.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, this.CORNER_RADIUS);
// Bottom.
path.push('h', atRight ? width : -width);
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
}
/**
* Scroll the flyout to the top.
*/
scrollToStart() {
this.workspace_.scrollbar.setY(0);
}
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @protected
*/
wheel_(e) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
if (scrollDelta.y) {
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.top - scrollMetrics.top) + scrollDelta.y;
this.workspace_.scrollbar.setY(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and
// DropDownDiv.
WidgetDiv.hide();
DropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
}
/**
* Lay out the blocks in the flyout.
* @param {!Array<!Object>} contents The blocks and buttons to lay out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
*/
layout_(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
const margin = this.MARGIN;
const cursorX = this.RTL ? margin : margin + this.tabWidth_;
let cursorY = margin;
for (let i = 0, item; (item = contents[i]); i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such
// a block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
const moveX =
block.outputConnection ? cursorX - this.tabWidth_ : cursorX;
block.moveBy(moveX, cursorY);
const rect = this.createRect_(
block, this.RTL ? moveX - blockHW.width : moveX, cursorY, blockHW,
i);
this.addBlockListeners_(root, block, rect);
cursorY += blockHW.height + gaps[i];
} else if (item.type === 'button') {
this.initFlyoutButton_(item.button, cursorX, cursorY);
cursorY += item.button.height + gaps[i];
}
}
}
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
*/
isDragTowardWorkspace(currentDragDeltaXY) {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
const dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
const range = this.dragAngleRange_;
// Check for left or right dragging.
if ((dragDirection < range && dragDirection > -range) ||
(dragDirection < -180 + range || dragDirection > 180 - range)) {
return true;
}
return false;
}
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
getClientRect() {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
return null;
}
const flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown
// flyout area are still deleted. Must be larger than the largest screen
// size, but be smaller than half Number.MAX_SAFE_INTEGER (not available on
// IE).
const BIG_NUM = 1000000000;
const left = flyoutRect.left;
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
const width = flyoutRect.width;
return new Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, left + width);
} else { // Right
return new Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM);
}
}
/**
* Compute width of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks and buttons to be right-aligned.
* @protected
*/
reflowInternal_() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutWidth = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
let width = block.getHeightWidth().width;
if (block.outputConnection) {
width -= this.tabWidth_;
}
flyoutWidth = Math.max(flyoutWidth, width);
}
for (let i = 0, button; (button = this.buttons_[i]); i++) {
flyoutWidth = Math.max(flyoutWidth, button.width);
}
flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_;
flyoutWidth *= this.workspace_.scale;
flyoutWidth += Scrollbar.scrollbarThickness;
if (this.width_ !== flyoutWidth) {
for (let i = 0, block; (block = blocks[i]); i++) {
if (this.RTL) {
// With the flyoutWidth known, right-align the blocks.
const oldX = block.getRelativeToSurfaceXY().x;
let newX = flyoutWidth / this.workspace_.scale - this.MARGIN;
if (!block.outputConnection) {
newX -= this.tabWidth_;
}
block.moveBy(newX - oldX, 0);
}
if (block.flyoutRect_) {
this.moveRectToBlock_(block.flyoutRect_, block);
}
}
if (this.RTL) {
// With the flyoutWidth known, right-align the buttons.
for (let i = 0, button; (button = this.buttons_[i]); i++) {
const y = button.getPosition().y;
const x = flyoutWidth / this.workspace_.scale - button.width -
this.MARGIN - this.tabWidth_;
button.moveTo(x, y);
}
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.LEFT &&
!this.targetWorkspace.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that
// (0,0) is in the correct position relative to the new absolute edge
// (ie toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX + flyoutWidth,
this.targetWorkspace.scrollY);
}
// Record the width for workspace metrics and .position.
this.width_ = flyoutWidth;
this.position();
this.targetWorkspace.recordDragTargets();
}
}
}
/**
* The name of the vertical flyout in the registry.
@@ -53,336 +389,6 @@ object.inherits(VerticalFlyout, Flyout);
*/
VerticalFlyout.registryName = 'verticalFlyout';
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling and a
* similar x property.
* @protected
*/
VerticalFlyout.prototype.setMetrics_ = function(xyRatio) {
if (!this.isVisible()) {
return;
}
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
if (typeof xyRatio.y === 'number') {
this.workspace_.scrollY =
-(scrollMetrics.top +
(scrollMetrics.height - viewMetrics.height) * xyRatio.y);
}
this.workspace_.translate(
this.workspace_.scrollX + absoluteMetrics.left,
this.workspace_.scrollY + absoluteMetrics.top);
};
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
*/
VerticalFlyout.prototype.getX = function() {
if (!this.isVisible()) {
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
let x = 0;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = toolboxMetrics.width;
} else {
x = viewMetrics.width - this.width_;
}
// Simple (flyout-only) toolbox.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
} else {
// The simple flyout does not cover the workspace.
x = viewMetrics.width;
}
}
// Trashcan flyout is opposite the main flyout.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
} else {
// Because the anchor point of the flyout is on the left, but we want
// to align the right edge of the flyout with the right edge of the
// blocklyDiv, we calculate the full width of the div minus the width
// of the flyout.
x = viewMetrics.width + absoluteMetrics.left - this.width_;
}
}
return x;
};
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
*/
VerticalFlyout.prototype.getY = function() {
// Y is always 0 since this is a vertical flyout.
return 0;
};
/**
* Move the flyout to the edge of the workspace.
*/
VerticalFlyout.prototype.position = function() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the height for workspace metrics.
this.height_ = targetWorkspaceViewMetrics.height;
const edgeWidth = this.width_ - this.CORNER_RADIUS;
const edgeHeight = targetWorkspaceViewMetrics.height - 2 * this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
const x = this.getX();
const y = this.getY();
this.positionAt_(this.width_, this.height_, x, y);
};
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
VerticalFlyout.prototype.setBackgroundPath_ = function(width, height) {
const atRight = this.toolboxPosition_ === toolbox.Position.RIGHT;
const totalWidth = width + this.CORNER_RADIUS;
// Decide whether to start on the left or right.
const path = ['M ' + (atRight ? totalWidth : 0) + ',0'];
// Top.
path.push('h', atRight ? -width : width);
// Rounded corner.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, this.CORNER_RADIUS);
// Side closest to workspace.
path.push('v', Math.max(0, height));
// Rounded corner.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, this.CORNER_RADIUS);
// Bottom.
path.push('h', atRight ? width : -width);
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Scroll the flyout to the top.
*/
VerticalFlyout.prototype.scrollToStart = function() {
this.workspace_.scrollbar.setY(0);
};
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @protected
*/
VerticalFlyout.prototype.wheel_ = function(e) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
if (scrollDelta.y) {
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.top - scrollMetrics.top) + scrollDelta.y;
this.workspace_.scrollbar.setY(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and DropDownDiv.
WidgetDiv.hide();
DropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
};
/**
* Lay out the blocks in the flyout.
* @param {!Array<!Object>} contents The blocks and buttons to lay out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
*/
VerticalFlyout.prototype.layout_ = function(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
const margin = this.MARGIN;
const cursorX = this.RTL ? margin : margin + this.tabWidth_;
let cursorY = margin;
for (let i = 0, item; (item = contents[i]); i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such a
// block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
const moveX = block.outputConnection ? cursorX - this.tabWidth_ : cursorX;
block.moveBy(moveX, cursorY);
const rect = this.createRect_(
block, this.RTL ? moveX - blockHW.width : moveX, cursorY, blockHW, i);
this.addBlockListeners_(root, block, rect);
cursorY += blockHW.height + gaps[i];
} else if (item.type === 'button') {
this.initFlyoutButton_(item.button, cursorX, cursorY);
cursorY += item.button.height + gaps[i];
}
}
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
*/
VerticalFlyout.prototype.isDragTowardWorkspace = function(currentDragDeltaXY) {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
const dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
const range = this.dragAngleRange_;
// Check for left or right dragging.
if ((dragDirection < range && dragDirection > -range) ||
(dragDirection < -180 + range || dragDirection > 180 - range)) {
return true;
}
return false;
};
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
*/
VerticalFlyout.prototype.getClientRect = function() {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
return null;
}
const flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout
// area are still deleted. Must be larger than the largest screen size,
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
const BIG_NUM = 1000000000;
const left = flyoutRect.left;
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
const width = flyoutRect.width;
return new Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, left + width);
} else { // Right
return new Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM);
}
};
/**
* Compute width of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks and buttons to be right-aligned.
* @protected
*/
VerticalFlyout.prototype.reflowInternal_ = function() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutWidth = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
let width = block.getHeightWidth().width;
if (block.outputConnection) {
width -= this.tabWidth_;
}
flyoutWidth = Math.max(flyoutWidth, width);
}
for (let i = 0, button; (button = this.buttons_[i]); i++) {
flyoutWidth = Math.max(flyoutWidth, button.width);
}
flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_;
flyoutWidth *= this.workspace_.scale;
flyoutWidth += Scrollbar.scrollbarThickness;
if (this.width_ !== flyoutWidth) {
for (let i = 0, block; (block = blocks[i]); i++) {
if (this.RTL) {
// With the flyoutWidth known, right-align the blocks.
const oldX = block.getRelativeToSurfaceXY().x;
let newX = flyoutWidth / this.workspace_.scale - this.MARGIN;
if (!block.outputConnection) {
newX -= this.tabWidth_;
}
block.moveBy(newX - oldX, 0);
}
if (block.flyoutRect_) {
this.moveRectToBlock_(block.flyoutRect_, block);
}
}
if (this.RTL) {
// With the flyoutWidth known, right-align the buttons.
for (let i = 0, button; (button = this.buttons_[i]); i++) {
const y = button.getPosition().y;
const x = flyoutWidth / this.workspace_.scale - button.width -
this.MARGIN - this.tabWidth_;
button.moveTo(x, y);
}
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.LEFT &&
!this.targetWorkspace.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that (0,0)
// is in the correct position relative to the new absolute edge (ie
// toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX + flyoutWidth,
this.targetWorkspace.scrollY);
}
// Record the width for workspace metrics and .position.
this.width_ = flyoutWidth;
this.position();
this.targetWorkspace.recordDragTargets();
}
};
registry.register(
registry.Type.FLYOUTS_VERTICAL_TOOLBOX, registry.DEFAULT, VerticalFlyout);

File diff suppressed because it is too large Load Diff

View File

@@ -24,200 +24,203 @@ const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for a workspace's grid.
* @param {!SVGElement} pattern The grid's SVG pattern, created during
* injection.
* @param {!Object} options A dictionary of normalized options for the grid.
* See grid documentation:
* https://developers.google.com/blockly/guides/configure/web/grid
* @constructor
* @alias Blockly.Grid
*/
const Grid = function(pattern, options) {
class Grid {
/**
* The scale of the grid, used to set stroke width on grid lines.
* This should always be the same as the workspace scale.
* @type {number}
* @private
* @param {!SVGElement} pattern The grid's SVG pattern, created during
* injection.
* @param {!Object} options A dictionary of normalized options for the grid.
* See grid documentation:
* https://developers.google.com/blockly/guides/configure/web/grid
* @alias Blockly.Grid
*/
this.scale_ = 1;
constructor(pattern, options) {
/**
* The scale of the grid, used to set stroke width on grid lines.
* This should always be the same as the workspace scale.
* @type {number}
* @private
*/
this.scale_ = 1;
/**
* The grid's SVG pattern, created during injection.
* @type {!SVGElement}
* @private
*/
this.gridPattern_ = pattern;
/**
* The grid's SVG pattern, created during injection.
* @type {!SVGElement}
* @private
*/
this.gridPattern_ = pattern;
/**
* The spacing of the grid lines (in px).
* @type {number}
* @private
*/
this.spacing_ = options['spacing'];
/**
* The spacing of the grid lines (in px).
* @type {number}
* @private
*/
this.spacing_ = options['spacing'];
/**
* How long the grid lines should be (in px).
* @type {number}
* @private
*/
this.length_ = options['length'];
/**
* How long the grid lines should be (in px).
* @type {number}
* @private
*/
this.length_ = options['length'];
/**
* The horizontal grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line1_ = /** @type {SVGElement} */ (pattern.firstChild);
/**
* The horizontal grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line1_ = /** @type {SVGElement} */ (pattern.firstChild);
/**
* The vertical grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line2_ =
this.line1_ && (/** @type {SVGElement} */ (this.line1_.nextSibling));
/**
* The vertical grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line2_ =
this.line1_ && (/** @type {SVGElement} */ (this.line1_.nextSibling));
/**
* Whether blocks should snap to the grid.
* @type {boolean}
* @private
*/
this.snapToGrid_ = options['snap'];
};
/**
* Dispose of this grid and unlink from the DOM.
* @package
* @suppress {checkTypes}
*/
Grid.prototype.dispose = function() {
this.gridPattern_ = null;
};
/**
* Whether blocks should snap to the grid, based on the initial configuration.
* @return {boolean} True if blocks should snap, false otherwise.
* @package
*/
Grid.prototype.shouldSnap = function() {
return this.snapToGrid_;
};
/**
* Get the spacing of the grid points (in px).
* @return {number} The spacing of the grid points.
* @package
*/
Grid.prototype.getSpacing = function() {
return this.spacing_;
};
/**
* Get the ID of the pattern element, which should be randomized to avoid
* conflicts with other Blockly instances on the page.
* @return {string} The pattern ID.
* @package
*/
Grid.prototype.getPatternId = function() {
return this.gridPattern_.id;
};
/**
* Update the grid with a new scale.
* @param {number} scale The new workspace scale.
* @package
*/
Grid.prototype.update = function(scale) {
this.scale_ = scale;
// MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
const safeSpacing = (this.spacing_ * scale) || 100;
this.gridPattern_.setAttribute('width', safeSpacing);
this.gridPattern_.setAttribute('height', safeSpacing);
let half = Math.floor(this.spacing_ / 2) + 0.5;
let start = half - this.length_ / 2;
let end = half + this.length_ / 2;
half *= scale;
start *= scale;
end *= scale;
this.setLineAttributes_(this.line1_, scale, start, end, half, half);
this.setLineAttributes_(this.line2_, scale, half, half, start, end);
};
/**
* Set the attributes on one of the lines in the grid. Use this to update the
* length and stroke width of the grid lines.
* @param {SVGElement} line Which line to update.
* @param {number} width The new stroke size (in px).
* @param {number} x1 The new x start position of the line (in px).
* @param {number} x2 The new x end position of the line (in px).
* @param {number} y1 The new y start position of the line (in px).
* @param {number} y2 The new y end position of the line (in px).
* @private
*/
Grid.prototype.setLineAttributes_ = function(line, width, x1, x2, y1, y2) {
if (line) {
line.setAttribute('stroke-width', width);
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
/**
* Whether blocks should snap to the grid.
* @type {boolean}
* @private
*/
this.snapToGrid_ = options['snap'];
}
};
/**
* Move the grid to a new x and y position, and make sure that change is
* visible.
* @param {number} x The new x position of the grid (in px).
* @param {number} y The new y position of the grid (in px).
* @package
*/
Grid.prototype.moveTo = function(x, y) {
this.gridPattern_.setAttribute('x', x);
this.gridPattern_.setAttribute('y', y);
if (userAgent.IE || userAgent.EDGE) {
// IE/Edge doesn't notice that the x/y offsets have changed.
// Force an update.
this.update(this.scale_);
/**
* Dispose of this grid and unlink from the DOM.
* @package
* @suppress {checkTypes}
*/
dispose() {
this.gridPattern_ = null;
}
};
/**
* Create the DOM for the grid described by options.
* @param {string} rnd A random ID to append to the pattern's ID.
* @param {!Object} gridOptions The object containing grid configuration.
* @param {!SVGElement} defs The root SVG element for this workspace's defs.
* @return {!SVGElement} The SVG element for the grid pattern.
* @package
*/
Grid.createDom = function(rnd, gridOptions, defs) {
/*
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
<rect stroke="#888" />
<rect stroke="#888" />
</pattern>
*/
const gridPattern = dom.createSvgElement(
Svg.PATTERN,
{'id': 'blocklyGridPattern' + rnd, 'patternUnits': 'userSpaceOnUse'},
defs);
if (gridOptions['length'] > 0 && gridOptions['spacing'] > 0) {
dom.createSvgElement(
Svg.LINE, {'stroke': gridOptions['colour']}, gridPattern);
if (gridOptions['length'] > 1) {
/**
* Whether blocks should snap to the grid, based on the initial configuration.
* @return {boolean} True if blocks should snap, false otherwise.
* @package
*/
shouldSnap() {
return this.snapToGrid_;
}
/**
* Get the spacing of the grid points (in px).
* @return {number} The spacing of the grid points.
* @package
*/
getSpacing() {
return this.spacing_;
}
/**
* Get the ID of the pattern element, which should be randomized to avoid
* conflicts with other Blockly instances on the page.
* @return {string} The pattern ID.
* @package
*/
getPatternId() {
return this.gridPattern_.id;
}
/**
* Update the grid with a new scale.
* @param {number} scale The new workspace scale.
* @package
*/
update(scale) {
this.scale_ = scale;
// MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
const safeSpacing = (this.spacing_ * scale) || 100;
this.gridPattern_.setAttribute('width', safeSpacing);
this.gridPattern_.setAttribute('height', safeSpacing);
let half = Math.floor(this.spacing_ / 2) + 0.5;
let start = half - this.length_ / 2;
let end = half + this.length_ / 2;
half *= scale;
start *= scale;
end *= scale;
this.setLineAttributes_(this.line1_, scale, start, end, half, half);
this.setLineAttributes_(this.line2_, scale, half, half, start, end);
}
/**
* Set the attributes on one of the lines in the grid. Use this to update the
* length and stroke width of the grid lines.
* @param {SVGElement} line Which line to update.
* @param {number} width The new stroke size (in px).
* @param {number} x1 The new x start position of the line (in px).
* @param {number} x2 The new x end position of the line (in px).
* @param {number} y1 The new y start position of the line (in px).
* @param {number} y2 The new y end position of the line (in px).
* @private
*/
setLineAttributes_(line, width, x1, x2, y1, y2) {
if (line) {
line.setAttribute('stroke-width', width);
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
}
}
/**
* Move the grid to a new x and y position, and make sure that change is
* visible.
* @param {number} x The new x position of the grid (in px).
* @param {number} y The new y position of the grid (in px).
* @package
*/
moveTo(x, y) {
this.gridPattern_.setAttribute('x', x);
this.gridPattern_.setAttribute('y', y);
if (userAgent.IE || userAgent.EDGE) {
// IE/Edge doesn't notice that the x/y offsets have changed.
// Force an update.
this.update(this.scale_);
}
}
/**
* Create the DOM for the grid described by options.
* @param {string} rnd A random ID to append to the pattern's ID.
* @param {!Object} gridOptions The object containing grid configuration.
* @param {!SVGElement} defs The root SVG element for this workspace's defs.
* @return {!SVGElement} The SVG element for the grid pattern.
* @package
*/
static createDom(rnd, gridOptions, defs) {
/*
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
<rect stroke="#888" />
<rect stroke="#888" />
</pattern>
*/
const gridPattern = dom.createSvgElement(
Svg.PATTERN,
{'id': 'blocklyGridPattern' + rnd, 'patternUnits': 'userSpaceOnUse'},
defs);
if (gridOptions['length'] > 0 && gridOptions['spacing'] > 0) {
dom.createSvgElement(
Svg.LINE, {'stroke': gridOptions['colour']}, gridPattern);
if (gridOptions['length'] > 1) {
dom.createSvgElement(
Svg.LINE, {'stroke': gridOptions['colour']}, gridPattern);
}
// x1, y1, x1, x2 properties will be set later in update.
} else {
// Edge 16 doesn't handle empty patterns
dom.createSvgElement(Svg.LINE, {}, gridPattern);
}
// x1, y1, x1, x2 properties will be set later in update.
} else {
// Edge 16 doesn't handle empty patterns
dom.createSvgElement(Svg.LINE, {}, gridPattern);
return gridPattern;
}
return gridPattern;
};
}
exports.Grid = Grid;

View File

@@ -15,18 +15,6 @@
*/
goog.module('Blockly.Input');
/**
* Enum for alignment of inputs.
* @enum {number}
* @alias Blockly.Input.Align
*/
const Align = {
LEFT: -1,
CENTRE: 0,
RIGHT: 1,
};
exports.Align = Align;
const fieldRegistry = goog.require('Blockly.fieldRegistry');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
@@ -42,286 +30,303 @@ const {inputTypes} = goog.require('Blockly.inputTypes');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');
/**
* Enum for alignment of inputs.
* @enum {number}
* @alias Blockly.Input.Align
*/
const Align = {
LEFT: -1,
CENTRE: 0,
RIGHT: 1,
};
exports.Align = Align;
/**
* Class for an input with an optional field.
* @param {number} type The type of the input.
* @param {string} name Language-neutral identifier which may used to find this
* input again.
* @param {!Block} block The block containing this input.
* @param {Connection} connection Optional connection for this input.
* @constructor
* @alias Blockly.Input
*/
const Input = function(type, name, block, connection) {
if (type !== inputTypes.DUMMY && !name) {
throw Error('Value inputs and statement inputs must have non-empty name.');
class Input {
/**
* @param {number} type The type of the input.
* @param {string} name Language-neutral identifier which may used to find
* this input again.
* @param {!Block} block The block containing this input.
* @param {Connection} connection Optional connection for this input.
* @alias Blockly.Input
*/
constructor(type, name, block, connection) {
if (type !== inputTypes.DUMMY && !name) {
throw Error(
'Value inputs and statement inputs must have non-empty name.');
}
/** @type {number} */
this.type = type;
/** @type {string} */
this.name = name;
/**
* @type {!Block}
* @private
*/
this.sourceBlock_ = block;
/** @type {Connection} */
this.connection = connection;
/** @type {!Array<!Field>} */
this.fieldRow = [];
/**
* Alignment of input's fields (left, right or centre).
* @type {number}
*/
this.align = Align.LEFT;
/**
* Is the input visible?
* @type {boolean}
* @private
*/
this.visible_ = true;
}
/** @type {number} */
this.type = type;
/** @type {string} */
this.name = name;
/**
* @type {!Block}
* @private
*/
this.sourceBlock_ = block;
/** @type {Connection} */
this.connection = connection;
/** @type {!Array<!Field>} */
this.fieldRow = [];
/**
* Alignment of input's fields (left, right or centre).
* @type {number}
* Get the source block for this input.
* @return {?Block} The source block, or null if there is none.
*/
this.align = Align.LEFT;
/**
* Is the input visible?
* @type {boolean}
* @private
*/
this.visible_ = true;
};
/**
* Get the source block for this input.
* @return {?Block} The source block, or null if there is none.
*/
Input.prototype.getSourceBlock = function() {
return this.sourceBlock_;
};
/**
* Add a field (or label from string), and all prefix and suffix fields, to the
* end of the input's field row.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to find
* this field again. Should be unique to the host block.
* @return {!Input} The input being append to (to allow chaining).
*/
Input.prototype.appendField = function(field, opt_name) {
this.insertFieldAt(this.fieldRow.length, field, opt_name);
return this;
};
/**
* Inserts a field (or label from string), and all prefix and suffix fields, at
* the location of the input's field row.
* @param {number} index The index at which to insert field.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to find
* this field again. Should be unique to the host block.
* @return {number} The index following the last inserted field.
*/
Input.prototype.insertFieldAt = function(index, field, opt_name) {
if (index < 0 || index > this.fieldRow.length) {
throw Error('index ' + index + ' out of bounds.');
getSourceBlock() {
return this.sourceBlock_;
}
// Falsy field values don't generate a field, unless the field is an empty
// string and named.
if (!field && !(field === '' && opt_name)) {
/**
* Add a field (or label from string), and all prefix and suffix fields, to
* the end of the input's field row.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to
* find this field again. Should be unique to the host block.
* @return {!Input} The input being append to (to allow chaining).
*/
appendField(field, opt_name) {
this.insertFieldAt(this.fieldRow.length, field, opt_name);
return this;
}
/**
* Inserts a field (or label from string), and all prefix and suffix fields,
* at the location of the input's field row.
* @param {number} index The index at which to insert field.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to
* find this field again. Should be unique to the host block.
* @return {number} The index following the last inserted field.
*/
insertFieldAt(index, field, opt_name) {
if (index < 0 || index > this.fieldRow.length) {
throw Error('index ' + index + ' out of bounds.');
}
// Falsy field values don't generate a field, unless the field is an empty
// string and named.
if (!field && !(field === '' && opt_name)) {
return index;
}
// Generate a FieldLabel when given a plain text field.
if (typeof field === 'string') {
field = /** @type {!Field} **/ (fieldRegistry.fromJson({
'type': 'field_label',
'text': field,
}));
}
field.setSourceBlock(this.sourceBlock_);
if (this.sourceBlock_.rendered) {
field.init();
field.applyColour();
}
field.name = opt_name;
field.setVisible(this.isVisible());
if (field.prefixField) {
// Add any prefix.
index = this.insertFieldAt(index, field.prefixField);
}
// Add the field to the field row.
this.fieldRow.splice(index, 0, field);
index++;
if (field.suffixField) {
// Add any suffix.
index = this.insertFieldAt(index, field.suffixField);
}
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
// Adding a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
}
return index;
}
// Generate a FieldLabel when given a plain text field.
if (typeof field === 'string') {
field = /** @type {!Field} **/ (fieldRegistry.fromJson({
'type': 'field_label',
'text': field,
}));
}
field.setSourceBlock(this.sourceBlock_);
if (this.sourceBlock_.rendered) {
field.init();
field.applyColour();
}
field.name = opt_name;
field.setVisible(this.isVisible());
if (field.prefixField) {
// Add any prefix.
index = this.insertFieldAt(index, field.prefixField);
}
// Add the field to the field row.
this.fieldRow.splice(index, 0, field);
index++;
if (field.suffixField) {
// Add any suffix.
index = this.insertFieldAt(index, field.suffixField);
}
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
// Adding a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
}
return index;
};
/**
* Remove a field from this input.
* @param {string} name The name of the field.
* @param {boolean=} opt_quiet True to prevent an error if field is not present.
* @return {boolean} True if operation succeeds, false if field is not present
* and opt_quiet is true.
* @throws {Error} if the field is not present and opt_quiet is false.
*/
Input.prototype.removeField = function(name, opt_quiet) {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
if (field.name === name) {
field.dispose();
this.fieldRow.splice(i, 1);
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
// Removing a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
/**
* Remove a field from this input.
* @param {string} name The name of the field.
* @param {boolean=} opt_quiet True to prevent an error if field is not
* present.
* @return {boolean} True if operation succeeds, false if field is not present
* and opt_quiet is true.
* @throws {Error} if the field is not present and opt_quiet is false.
*/
removeField(name, opt_quiet) {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
if (field.name === name) {
field.dispose();
this.fieldRow.splice(i, 1);
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
// Removing a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
}
return true;
}
return true;
}
if (opt_quiet) {
return false;
}
throw Error('Field "' + name + '" not found.');
}
if (opt_quiet) {
return false;
/**
* Gets whether this input is visible or not.
* @return {boolean} True if visible.
*/
isVisible() {
return this.visible_;
}
throw Error('Field "' + name + '" not found.');
};
/**
* Gets whether this input is visible or not.
* @return {boolean} True if visible.
*/
Input.prototype.isVisible = function() {
return this.visible_;
};
/**
* Sets whether this input is visible or not.
* Should only be used to collapse/uncollapse a block.
* @param {boolean} visible True if visible.
* @return {!Array<!BlockSvg>} List of blocks to render.
* @package
*/
setVisible(visible) {
// Note: Currently there are only unit tests for block.setCollapsed()
// because this function is package. If this function goes back to being a
// public API tests (lots of tests) should be added.
let renderList = [];
if (this.visible_ === visible) {
return renderList;
}
this.visible_ = visible;
/**
* Sets whether this input is visible or not.
* Should only be used to collapse/uncollapse a block.
* @param {boolean} visible True if visible.
* @return {!Array<!BlockSvg>} List of blocks to render.
* @package
*/
Input.prototype.setVisible = function(visible) {
// Note: Currently there are only unit tests for block.setCollapsed()
// because this function is package. If this function goes back to being a
// public API tests (lots of tests) should be added.
let renderList = [];
if (this.visible_ === visible) {
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
field.setVisible(visible);
}
if (this.connection) {
this.connection =
/** @type {!RenderedConnection} */ (this.connection);
// Has a connection.
if (visible) {
renderList = this.connection.startTrackingAll();
} else {
this.connection.stopTrackingAll();
}
const child = this.connection.targetBlock();
if (child) {
child.getSvgRoot().style.display = visible ? 'block' : 'none';
}
}
return renderList;
}
this.visible_ = visible;
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
field.setVisible(visible);
}
if (this.connection) {
this.connection =
/** @type {!RenderedConnection} */ (this.connection);
// Has a connection.
if (visible) {
renderList = this.connection.startTrackingAll();
} else {
this.connection.stopTrackingAll();
}
const child = this.connection.targetBlock();
if (child) {
child.getSvgRoot().style.display = visible ? 'block' : 'none';
/**
* Mark all fields on this input as dirty.
* @package
*/
markDirty() {
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
field.markDirty();
}
}
return renderList;
};
/**
* Mark all fields on this input as dirty.
* @package
*/
Input.prototype.markDirty = function() {
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
field.markDirty();
/**
* Change a connection's compatibility.
* @param {string|Array<string>|null} check Compatible value type or
* list of value types. Null if all types are compatible.
* @return {!Input} The input being modified (to allow chaining).
*/
setCheck(check) {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
this.connection.setCheck(check);
return this;
}
};
/**
* Change a connection's compatibility.
* @param {string|Array<string>|null} check Compatible value type or
* list of value types. Null if all types are compatible.
* @return {!Input} The input being modified (to allow chaining).
*/
Input.prototype.setCheck = function(check) {
if (!this.connection) {
throw Error('This input does not have a connection.');
/**
* Change the alignment of the connection's field(s).
* @param {number} align One of the values of Align
* In RTL mode directions are reversed, and Align.RIGHT aligns to the left.
* @return {!Input} The input being modified (to allow chaining).
*/
setAlign(align) {
this.align = align;
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
}
return this;
}
this.connection.setCheck(check);
return this;
};
/**
* Change the alignment of the connection's field(s).
* @param {number} align One of the values of Align
* In RTL mode directions are reversed, and Align.RIGHT aligns to the left.
* @return {!Input} The input being modified (to allow chaining).
*/
Input.prototype.setAlign = function(align) {
this.align = align;
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
/**
* Changes the connection's shadow block.
* @param {?Element} shadow DOM representation of a block or null.
* @return {!Input} The input being modified (to allow chaining).
*/
setShadowDom(shadow) {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
this.connection.setShadowDom(shadow);
return this;
}
return this;
};
/**
* Changes the connection's shadow block.
* @param {?Element} shadow DOM representation of a block or null.
* @return {!Input} The input being modified (to allow chaining).
*/
Input.prototype.setShadowDom = function(shadow) {
if (!this.connection) {
throw Error('This input does not have a connection.');
/**
* Returns the XML representation of the connection's shadow block.
* @return {?Element} Shadow DOM representation of a block or null.
*/
getShadowDom() {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
return this.connection.getShadowDom();
}
this.connection.setShadowDom(shadow);
return this;
};
/**
* Returns the XML representation of the connection's shadow block.
* @return {?Element} Shadow DOM representation of a block or null.
*/
Input.prototype.getShadowDom = function() {
if (!this.connection) {
throw Error('This input does not have a connection.');
/**
* Initialize the fields on this input.
*/
init() {
if (!this.sourceBlock_.workspace.rendered) {
return; // Headless blocks don't need fields initialized.
}
for (let i = 0; i < this.fieldRow.length; i++) {
this.fieldRow[i].init();
}
}
return this.connection.getShadowDom();
};
/**
* Initialize the fields on this input.
*/
Input.prototype.init = function() {
if (!this.sourceBlock_.workspace.rendered) {
return; // Headless blocks don't need fields initialized.
/**
* Sever all links to this input.
* @suppress {checkTypes}
*/
dispose() {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
field.dispose();
}
if (this.connection) {
this.connection.dispose();
}
this.sourceBlock_ = null;
}
for (let i = 0; i < this.fieldRow.length; i++) {
this.fieldRow[i].init();
}
};
/**
* Sever all links to this input.
* @suppress {checkTypes}
*/
Input.prototype.dispose = function() {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
field.dispose();
}
if (this.connection) {
this.connection.dispose();
}
this.sourceBlock_ = null;
};
}
exports.Input = Input;

File diff suppressed because it is too large Load Diff

View File

@@ -32,20 +32,409 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/**
* The manager for all workspace metrics calculations.
* @param {!WorkspaceSvg} workspace The workspace to calculate metrics
* for.
* @implements {IMetricsManager}
* @constructor
* @alias Blockly.MetricsManager
*/
const MetricsManager = function(workspace) {
class MetricsManager {
/**
* The workspace to calculate metrics for.
* @type {!WorkspaceSvg}
* @param {!WorkspaceSvg} workspace The workspace to calculate metrics
* for.
* @alias Blockly.MetricsManager
*/
constructor(workspace) {
/**
* The workspace to calculate metrics for.
* @type {!WorkspaceSvg}
* @protected
*/
this.workspace_ = workspace;
}
/**
* Gets the dimensions of the given workspace component, in pixel coordinates.
* @param {?IToolbox|?IFlyout} elem The element to get the
* dimensions of, or null. It should be a toolbox or flyout, and should
* implement getWidth() and getHeight().
* @return {!Size} An object containing width and height
* attributes, which will both be zero if elem did not exist.
* @protected
*/
this.workspace_ = workspace;
};
getDimensionsPx_(elem) {
let width = 0;
let height = 0;
if (elem) {
width = elem.getWidth();
height = elem.getHeight();
}
return new Size(width, height);
}
/**
* Gets the width and the height of the flyout on the workspace in pixel
* coordinates. Returns 0 for the width and height if the workspace has a
* category toolbox instead of a simple toolbox.
* @param {boolean=} opt_own Whether to only return the workspace's own
* flyout.
* @return {!MetricsManager.ToolboxMetrics} The width and height of the
* flyout.
* @public
*/
getFlyoutMetrics(opt_own) {
const flyoutDimensions =
this.getDimensionsPx_(this.workspace_.getFlyout(opt_own));
return {
width: flyoutDimensions.width,
height: flyoutDimensions.height,
position: this.workspace_.toolboxPosition,
};
}
/**
* Gets the width, height and position of the toolbox on the workspace in
* pixel coordinates. Returns 0 for the width and height if the workspace has
* a simple toolbox instead of a category toolbox. To get the width and height
* of a
* simple toolbox @see {@link getFlyoutMetrics}.
* @return {!MetricsManager.ToolboxMetrics} The object with the width,
* height and position of the toolbox.
* @public
*/
getToolboxMetrics() {
const toolboxDimensions =
this.getDimensionsPx_(this.workspace_.getToolbox());
return {
width: toolboxDimensions.width,
height: toolboxDimensions.height,
position: this.workspace_.toolboxPosition,
};
}
/**
* Gets the width and height of the workspace's parent SVG element in pixel
* coordinates. This area includes the toolbox and the visible workspace area.
* @return {!Size} The width and height of the workspace's parent
* SVG element.
* @public
*/
getSvgMetrics() {
return this.workspace_.getCachedParentSvgSize();
}
/**
* Gets the absolute left and absolute top in pixel coordinates.
* This is where the visible workspace starts in relation to the SVG
* container.
* @return {!MetricsManager.AbsoluteMetrics} The absolute metrics for
* the workspace.
* @public
*/
getAbsoluteMetrics() {
let absoluteLeft = 0;
const toolboxMetrics = this.getToolboxMetrics();
const flyoutMetrics = this.getFlyoutMetrics(true);
const doesToolboxExist = !!this.workspace_.getToolbox();
const doesFlyoutExist = !!this.workspace_.getFlyout(true);
const toolboxPosition =
doesToolboxExist ? toolboxMetrics.position : flyoutMetrics.position;
const atLeft = toolboxPosition === toolboxUtils.Position.LEFT;
const atTop = toolboxPosition === toolboxUtils.Position.TOP;
if (doesToolboxExist && atLeft) {
absoluteLeft = toolboxMetrics.width;
} else if (doesFlyoutExist && atLeft) {
absoluteLeft = flyoutMetrics.width;
}
let absoluteTop = 0;
if (doesToolboxExist && atTop) {
absoluteTop = toolboxMetrics.height;
} else if (doesFlyoutExist && atTop) {
absoluteTop = flyoutMetrics.height;
}
return {
top: absoluteTop,
left: absoluteLeft,
};
}
/**
* Gets the metrics for the visible workspace in either pixel or workspace
* coordinates. The visible workspace does not include the toolbox or flyout.
* @param {boolean=} opt_getWorkspaceCoordinates True to get the view metrics
* in workspace coordinates, false to get them in pixel coordinates.
* @return {!MetricsManager.ContainerRegion} The width, height, top and
* left of the viewport in either workspace coordinates or pixel
* coordinates.
* @public
*/
getViewMetrics(opt_getWorkspaceCoordinates) {
const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1;
const svgMetrics = this.getSvgMetrics();
const toolboxMetrics = this.getToolboxMetrics();
const flyoutMetrics = this.getFlyoutMetrics(true);
const doesToolboxExist = !!this.workspace_.getToolbox();
const toolboxPosition =
doesToolboxExist ? toolboxMetrics.position : flyoutMetrics.position;
if (this.workspace_.getToolbox()) {
if (toolboxPosition === toolboxUtils.Position.TOP ||
toolboxPosition === toolboxUtils.Position.BOTTOM) {
svgMetrics.height -= toolboxMetrics.height;
} else if (
toolboxPosition === toolboxUtils.Position.LEFT ||
toolboxPosition === toolboxUtils.Position.RIGHT) {
svgMetrics.width -= toolboxMetrics.width;
}
} else if (this.workspace_.getFlyout(true)) {
if (toolboxPosition === toolboxUtils.Position.TOP ||
toolboxPosition === toolboxUtils.Position.BOTTOM) {
svgMetrics.height -= flyoutMetrics.height;
} else if (
toolboxPosition === toolboxUtils.Position.LEFT ||
toolboxPosition === toolboxUtils.Position.RIGHT) {
svgMetrics.width -= flyoutMetrics.width;
}
}
return {
height: svgMetrics.height / scale,
width: svgMetrics.width / scale,
top: -this.workspace_.scrollY / scale,
left: -this.workspace_.scrollX / scale,
};
}
/**
* Gets content metrics in either pixel or workspace coordinates.
* The content area is a rectangle around all the top bounded elements on the
* workspace (workspace comments and blocks).
* @param {boolean=} opt_getWorkspaceCoordinates True to get the content
* metrics in workspace coordinates, false to get them in pixel
* coordinates.
* @return {!MetricsManager.ContainerRegion} The
* metrics for the content container.
* @public
*/
getContentMetrics(opt_getWorkspaceCoordinates) {
const scale = opt_getWorkspaceCoordinates ? 1 : this.workspace_.scale;
// Block bounding box is in workspace coordinates.
const blockBox = this.workspace_.getBlocksBoundingBox();
return {
height: (blockBox.bottom - blockBox.top) * scale,
width: (blockBox.right - blockBox.left) * scale,
top: blockBox.top * scale,
left: blockBox.left * scale,
};
}
/**
* Returns whether the scroll area has fixed edges.
* @return {boolean} Whether the scroll area has fixed edges.
* @package
*/
hasFixedEdges() {
// This exists for optimization of bump logic.
return !this.workspace_.isMovableHorizontally() ||
!this.workspace_.isMovableVertically();
}
/**
* Computes the fixed edges of the scroll area.
* @param {!MetricsManager.ContainerRegion=} opt_viewMetrics The view
* metrics if they have been previously computed. Passing in null may
* cause the view metrics to be computed again, if it is needed.
* @return {!MetricsManager.FixedEdges} The fixed edges of the scroll
* area.
* @protected
*/
getComputedFixedEdges_(opt_viewMetrics) {
if (!this.hasFixedEdges()) {
// Return early if there are no edges.
return {};
}
const hScrollEnabled = this.workspace_.isMovableHorizontally();
const vScrollEnabled = this.workspace_.isMovableVertically();
const viewMetrics = opt_viewMetrics || this.getViewMetrics(false);
const edges = {};
if (!vScrollEnabled) {
edges.top = viewMetrics.top;
edges.bottom = viewMetrics.top + viewMetrics.height;
}
if (!hScrollEnabled) {
edges.left = viewMetrics.left;
edges.right = viewMetrics.left + viewMetrics.width;
}
return edges;
}
/**
* Returns the content area with added padding.
* @param {!MetricsManager.ContainerRegion} viewMetrics The view
* metrics.
* @param {!MetricsManager.ContainerRegion} contentMetrics The content
* metrics.
* @return {{top: number, bottom: number, left: number, right: number}} The
* padded content area.
* @protected
*/
getPaddedContent_(viewMetrics, contentMetrics) {
const contentBottom = contentMetrics.top + contentMetrics.height;
const contentRight = contentMetrics.left + contentMetrics.width;
const viewWidth = viewMetrics.width;
const viewHeight = viewMetrics.height;
const halfWidth = viewWidth / 2;
const halfHeight = viewHeight / 2;
// Add a padding around the content that is at least half a screen wide.
// Ensure padding is wide enough that blocks can scroll over entire screen.
const top =
Math.min(contentMetrics.top - halfHeight, contentBottom - viewHeight);
const left =
Math.min(contentMetrics.left - halfWidth, contentRight - viewWidth);
const bottom =
Math.max(contentBottom + halfHeight, contentMetrics.top + viewHeight);
const right =
Math.max(contentRight + halfWidth, contentMetrics.left + viewWidth);
return {top: top, bottom: bottom, left: left, right: right};
}
/**
* Returns the metrics for the scroll area of the workspace.
* @param {boolean=} opt_getWorkspaceCoordinates True to get the scroll
* metrics in workspace coordinates, false to get them in pixel
* coordinates.
* @param {!MetricsManager.ContainerRegion=} opt_viewMetrics The view
* metrics if they have been previously computed. Passing in null may
* cause the view metrics to be computed again, if it is needed.
* @param {!MetricsManager.ContainerRegion=} opt_contentMetrics The
* content metrics if they have been previously computed. Passing in null
* may cause the content metrics to be computed again, if it is needed.
* @return {!MetricsManager.ContainerRegion} The metrics for the scroll
* container.
*/
getScrollMetrics(
opt_getWorkspaceCoordinates, opt_viewMetrics, opt_contentMetrics) {
const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1;
const viewMetrics = opt_viewMetrics || this.getViewMetrics(false);
const contentMetrics = opt_contentMetrics || this.getContentMetrics();
const fixedEdges = this.getComputedFixedEdges_(viewMetrics);
// Add padding around content.
const paddedContent = this.getPaddedContent_(viewMetrics, contentMetrics);
// Use combination of fixed bounds and padded content to make scroll area.
const top =
fixedEdges.top !== undefined ? fixedEdges.top : paddedContent.top;
const left =
fixedEdges.left !== undefined ? fixedEdges.left : paddedContent.left;
const bottom = fixedEdges.bottom !== undefined ? fixedEdges.bottom :
paddedContent.bottom;
const right =
fixedEdges.right !== undefined ? fixedEdges.right : paddedContent.right;
return {
top: top / scale,
left: left / scale,
width: (right - left) / scale,
height: (bottom - top) / scale,
};
}
/**
* Returns common metrics used by UI elements.
* @return {!MetricsManager.UiMetrics} The UI metrics.
*/
getUiMetrics() {
return {
viewMetrics: this.getViewMetrics(),
absoluteMetrics: this.getAbsoluteMetrics(),
toolboxMetrics: this.getToolboxMetrics(),
};
}
/**
* Returns an object with all the metrics required to size scrollbars for a
* top level workspace. The following properties are computed:
* Coordinate system: pixel coordinates, -left, -up, +right, +down
* .viewHeight: Height of the visible portion of the workspace.
* .viewWidth: Width of the visible portion of the workspace.
* .contentHeight: Height of the content.
* .contentWidth: Width of the content.
* .scrollHeight: Height of the scroll area.
* .scrollWidth: Width of the scroll area.
* .svgHeight: Height of the Blockly div (the view + the toolbox,
* simple or otherwise),
* .svgWidth: Width of the Blockly div (the view + the toolbox,
* simple or otherwise),
* .viewTop: Top-edge of the visible portion of the workspace, relative to
* the workspace origin.
* .viewLeft: Left-edge of the visible portion of the workspace, relative to
* the workspace origin.
* .contentTop: Top-edge of the content, relative to the workspace origin.
* .contentLeft: Left-edge of the content relative to the workspace origin.
* .scrollTop: Top-edge of the scroll area, relative to the workspace origin.
* .scrollLeft: Left-edge of the scroll area relative to the workspace origin.
* .absoluteTop: Top-edge of the visible portion of the workspace, relative
* to the blocklyDiv.
* .absoluteLeft: Left-edge of the visible portion of the workspace, relative
* to the blocklyDiv.
* .toolboxWidth: Width of the toolbox, if it exists. Otherwise zero.
* .toolboxHeight: Height of the toolbox, if it exists. Otherwise zero.
* .flyoutWidth: Width of the flyout if it is always open. Otherwise zero.
* .flyoutHeight: Height of the flyout if it is always open. Otherwise zero.
* .toolboxPosition: Top, bottom, left or right. Use TOOLBOX_AT constants to
* compare.
* @return {!Metrics} Contains size and position metrics of a top
* level workspace.
* @public
*/
getMetrics() {
const toolboxMetrics = this.getToolboxMetrics();
const flyoutMetrics = this.getFlyoutMetrics(true);
const svgMetrics = this.getSvgMetrics();
const absoluteMetrics = this.getAbsoluteMetrics();
const viewMetrics = this.getViewMetrics();
const contentMetrics = this.getContentMetrics();
const scrollMetrics =
this.getScrollMetrics(false, viewMetrics, contentMetrics);
return {
contentHeight: contentMetrics.height,
contentWidth: contentMetrics.width,
contentTop: contentMetrics.top,
contentLeft: contentMetrics.left,
scrollHeight: scrollMetrics.height,
scrollWidth: scrollMetrics.width,
scrollTop: scrollMetrics.top,
scrollLeft: scrollMetrics.left,
viewHeight: viewMetrics.height,
viewWidth: viewMetrics.width,
viewTop: viewMetrics.top,
viewLeft: viewMetrics.left,
absoluteTop: absoluteMetrics.top,
absoluteLeft: absoluteMetrics.left,
svgHeight: svgMetrics.height,
svgWidth: svgMetrics.width,
toolboxWidth: toolboxMetrics.width,
toolboxHeight: toolboxMetrics.height,
toolboxPosition: toolboxMetrics.position,
flyoutWidth: flyoutMetrics.width,
flyoutHeight: flyoutMetrics.height,
};
}
}
/**
* Describes the width, height and location of the toolbox on the main
@@ -57,6 +446,7 @@ const MetricsManager = function(workspace) {
* }}
*/
MetricsManager.ToolboxMetrics;
/**
* Describes where the viewport starts in relation to the workspace SVG.
* @typedef {{
@@ -65,8 +455,10 @@ MetricsManager.ToolboxMetrics;
* }}
*/
MetricsManager.AbsoluteMetrics;
/**
* All the measurements needed to describe the size and location of a container.
* All the measurements needed to describe the size and location of a
* container.
* @typedef {{
* height: number,
* width: number,
@@ -75,6 +467,7 @@ MetricsManager.AbsoluteMetrics;
* }}
*/
MetricsManager.ContainerRegion;
/**
* Describes fixed edges of the workspace.
* @typedef {{
@@ -85,6 +478,7 @@ MetricsManager.ContainerRegion;
* }}
*/
MetricsManager.FixedEdges;
/**
* Common metrics used for UI elements.
* @typedef {{
@@ -94,387 +488,6 @@ MetricsManager.FixedEdges;
* }}
*/
MetricsManager.UiMetrics;
/**
* Gets the dimensions of the given workspace component, in pixel coordinates.
* @param {?IToolbox|?IFlyout} elem The element to get the
* dimensions of, or null. It should be a toolbox or flyout, and should
* implement getWidth() and getHeight().
* @return {!Size} An object containing width and height
* attributes, which will both be zero if elem did not exist.
* @protected
*/
MetricsManager.prototype.getDimensionsPx_ = function(elem) {
let width = 0;
let height = 0;
if (elem) {
width = elem.getWidth();
height = elem.getHeight();
}
return new Size(width, height);
};
/**
* Gets the width and the height of the flyout on the workspace in pixel
* coordinates. Returns 0 for the width and height if the workspace has a
* category toolbox instead of a simple toolbox.
* @param {boolean=} opt_own Whether to only return the workspace's own flyout.
* @return {!MetricsManager.ToolboxMetrics} The width and height of the
* flyout.
* @public
*/
MetricsManager.prototype.getFlyoutMetrics = function(opt_own) {
const flyoutDimensions =
this.getDimensionsPx_(this.workspace_.getFlyout(opt_own));
return {
width: flyoutDimensions.width,
height: flyoutDimensions.height,
position: this.workspace_.toolboxPosition,
};
};
/**
* Gets the width, height and position of the toolbox on the workspace in pixel
* coordinates. Returns 0 for the width and height if the workspace has a simple
* toolbox instead of a category toolbox. To get the width and height of a
* simple toolbox @see {@link getFlyoutMetrics}.
* @return {!MetricsManager.ToolboxMetrics} The object with the width,
* height and position of the toolbox.
* @public
*/
MetricsManager.prototype.getToolboxMetrics = function() {
const toolboxDimensions = this.getDimensionsPx_(this.workspace_.getToolbox());
return {
width: toolboxDimensions.width,
height: toolboxDimensions.height,
position: this.workspace_.toolboxPosition,
};
};
/**
* Gets the width and height of the workspace's parent SVG element in pixel
* coordinates. This area includes the toolbox and the visible workspace area.
* @return {!Size} The width and height of the workspace's parent
* SVG element.
* @public
*/
MetricsManager.prototype.getSvgMetrics = function() {
return this.workspace_.getCachedParentSvgSize();
};
/**
* Gets the absolute left and absolute top in pixel coordinates.
* This is where the visible workspace starts in relation to the SVG container.
* @return {!MetricsManager.AbsoluteMetrics} The absolute metrics for
* the workspace.
* @public
*/
MetricsManager.prototype.getAbsoluteMetrics = function() {
let absoluteLeft = 0;
const toolboxMetrics = this.getToolboxMetrics();
const flyoutMetrics = this.getFlyoutMetrics(true);
const doesToolboxExist = !!this.workspace_.getToolbox();
const doesFlyoutExist = !!this.workspace_.getFlyout(true);
const toolboxPosition =
doesToolboxExist ? toolboxMetrics.position : flyoutMetrics.position;
const atLeft = toolboxPosition === toolboxUtils.Position.LEFT;
const atTop = toolboxPosition === toolboxUtils.Position.TOP;
if (doesToolboxExist && atLeft) {
absoluteLeft = toolboxMetrics.width;
} else if (doesFlyoutExist && atLeft) {
absoluteLeft = flyoutMetrics.width;
}
let absoluteTop = 0;
if (doesToolboxExist && atTop) {
absoluteTop = toolboxMetrics.height;
} else if (doesFlyoutExist && atTop) {
absoluteTop = flyoutMetrics.height;
}
return {
top: absoluteTop,
left: absoluteLeft,
};
};
/**
* Gets the metrics for the visible workspace in either pixel or workspace
* coordinates. The visible workspace does not include the toolbox or flyout.
* @param {boolean=} opt_getWorkspaceCoordinates True to get the view metrics in
* workspace coordinates, false to get them in pixel coordinates.
* @return {!MetricsManager.ContainerRegion} The width, height, top and
* left of the viewport in either workspace coordinates or pixel
* coordinates.
* @public
*/
MetricsManager.prototype.getViewMetrics = function(
opt_getWorkspaceCoordinates) {
const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1;
const svgMetrics = this.getSvgMetrics();
const toolboxMetrics = this.getToolboxMetrics();
const flyoutMetrics = this.getFlyoutMetrics(true);
const doesToolboxExist = !!this.workspace_.getToolbox();
const toolboxPosition =
doesToolboxExist ? toolboxMetrics.position : flyoutMetrics.position;
if (this.workspace_.getToolbox()) {
if (toolboxPosition === toolboxUtils.Position.TOP ||
toolboxPosition === toolboxUtils.Position.BOTTOM) {
svgMetrics.height -= toolboxMetrics.height;
} else if (
toolboxPosition === toolboxUtils.Position.LEFT ||
toolboxPosition === toolboxUtils.Position.RIGHT) {
svgMetrics.width -= toolboxMetrics.width;
}
} else if (this.workspace_.getFlyout(true)) {
if (toolboxPosition === toolboxUtils.Position.TOP ||
toolboxPosition === toolboxUtils.Position.BOTTOM) {
svgMetrics.height -= flyoutMetrics.height;
} else if (
toolboxPosition === toolboxUtils.Position.LEFT ||
toolboxPosition === toolboxUtils.Position.RIGHT) {
svgMetrics.width -= flyoutMetrics.width;
}
}
return {
height: svgMetrics.height / scale,
width: svgMetrics.width / scale,
top: -this.workspace_.scrollY / scale,
left: -this.workspace_.scrollX / scale,
};
};
/**
* Gets content metrics in either pixel or workspace coordinates.
* The content area is a rectangle around all the top bounded elements on the
* workspace (workspace comments and blocks).
* @param {boolean=} opt_getWorkspaceCoordinates True to get the content metrics
* in workspace coordinates, false to get them in pixel coordinates.
* @return {!MetricsManager.ContainerRegion} The
* metrics for the content container.
* @public
*/
MetricsManager.prototype.getContentMetrics = function(
opt_getWorkspaceCoordinates) {
const scale = opt_getWorkspaceCoordinates ? 1 : this.workspace_.scale;
// Block bounding box is in workspace coordinates.
const blockBox = this.workspace_.getBlocksBoundingBox();
return {
height: (blockBox.bottom - blockBox.top) * scale,
width: (blockBox.right - blockBox.left) * scale,
top: blockBox.top * scale,
left: blockBox.left * scale,
};
};
/**
* Returns whether the scroll area has fixed edges.
* @return {boolean} Whether the scroll area has fixed edges.
* @package
*/
MetricsManager.prototype.hasFixedEdges = function() {
// This exists for optimization of bump logic.
return !this.workspace_.isMovableHorizontally() ||
!this.workspace_.isMovableVertically();
};
/**
* Computes the fixed edges of the scroll area.
* @param {!MetricsManager.ContainerRegion=} opt_viewMetrics The view
* metrics if they have been previously computed. Passing in null may cause
* the view metrics to be computed again, if it is needed.
* @return {!MetricsManager.FixedEdges} The fixed edges of the scroll
* area.
* @protected
*/
MetricsManager.prototype.getComputedFixedEdges_ = function(opt_viewMetrics) {
if (!this.hasFixedEdges()) {
// Return early if there are no edges.
return {};
}
const hScrollEnabled = this.workspace_.isMovableHorizontally();
const vScrollEnabled = this.workspace_.isMovableVertically();
const viewMetrics = opt_viewMetrics || this.getViewMetrics(false);
const edges = {};
if (!vScrollEnabled) {
edges.top = viewMetrics.top;
edges.bottom = viewMetrics.top + viewMetrics.height;
}
if (!hScrollEnabled) {
edges.left = viewMetrics.left;
edges.right = viewMetrics.left + viewMetrics.width;
}
return edges;
};
/**
* Returns the content area with added padding.
* @param {!MetricsManager.ContainerRegion} viewMetrics The view
* metrics.
* @param {!MetricsManager.ContainerRegion} contentMetrics The content
* metrics.
* @return {{top: number, bottom: number, left: number, right: number}} The
* padded content area.
* @protected
*/
MetricsManager.prototype.getPaddedContent_ = function(
viewMetrics, contentMetrics) {
const contentBottom = contentMetrics.top + contentMetrics.height;
const contentRight = contentMetrics.left + contentMetrics.width;
const viewWidth = viewMetrics.width;
const viewHeight = viewMetrics.height;
const halfWidth = viewWidth / 2;
const halfHeight = viewHeight / 2;
// Add a padding around the content that is at least half a screen wide.
// Ensure padding is wide enough that blocks can scroll over entire screen.
const top =
Math.min(contentMetrics.top - halfHeight, contentBottom - viewHeight);
const left =
Math.min(contentMetrics.left - halfWidth, contentRight - viewWidth);
const bottom =
Math.max(contentBottom + halfHeight, contentMetrics.top + viewHeight);
const right =
Math.max(contentRight + halfWidth, contentMetrics.left + viewWidth);
return {top: top, bottom: bottom, left: left, right: right};
};
/**
* Returns the metrics for the scroll area of the workspace.
* @param {boolean=} opt_getWorkspaceCoordinates True to get the scroll metrics
* in workspace coordinates, false to get them in pixel coordinates.
* @param {!MetricsManager.ContainerRegion=} opt_viewMetrics The view
* metrics if they have been previously computed. Passing in null may cause
* the view metrics to be computed again, if it is needed.
* @param {!MetricsManager.ContainerRegion=} opt_contentMetrics The
* content metrics if they have been previously computed. Passing in null
* may cause the content metrics to be computed again, if it is needed.
* @return {!MetricsManager.ContainerRegion} The metrics for the scroll
* container.
*/
MetricsManager.prototype.getScrollMetrics = function(
opt_getWorkspaceCoordinates, opt_viewMetrics, opt_contentMetrics) {
const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1;
const viewMetrics = opt_viewMetrics || this.getViewMetrics(false);
const contentMetrics = opt_contentMetrics || this.getContentMetrics();
const fixedEdges = this.getComputedFixedEdges_(viewMetrics);
// Add padding around content.
const paddedContent = this.getPaddedContent_(viewMetrics, contentMetrics);
// Use combination of fixed bounds and padded content to make scroll area.
const top = fixedEdges.top !== undefined ? fixedEdges.top : paddedContent.top;
const left =
fixedEdges.left !== undefined ? fixedEdges.left : paddedContent.left;
const bottom = fixedEdges.bottom !== undefined ? fixedEdges.bottom :
paddedContent.bottom;
const right =
fixedEdges.right !== undefined ? fixedEdges.right : paddedContent.right;
return {
top: top / scale,
left: left / scale,
width: (right - left) / scale,
height: (bottom - top) / scale,
};
};
/**
* Returns common metrics used by UI elements.
* @return {!MetricsManager.UiMetrics} The UI metrics.
*/
MetricsManager.prototype.getUiMetrics = function() {
return {
viewMetrics: this.getViewMetrics(),
absoluteMetrics: this.getAbsoluteMetrics(),
toolboxMetrics: this.getToolboxMetrics(),
};
};
/**
* Returns an object with all the metrics required to size scrollbars for a
* top level workspace. The following properties are computed:
* Coordinate system: pixel coordinates, -left, -up, +right, +down
* .viewHeight: Height of the visible portion of the workspace.
* .viewWidth: Width of the visible portion of the workspace.
* .contentHeight: Height of the content.
* .contentWidth: Width of the content.
* .scrollHeight: Height of the scroll area.
* .scrollWidth: Width of the scroll area.
* .svgHeight: Height of the Blockly div (the view + the toolbox,
* simple or otherwise),
* .svgWidth: Width of the Blockly div (the view + the toolbox,
* simple or otherwise),
* .viewTop: Top-edge of the visible portion of the workspace, relative to
* the workspace origin.
* .viewLeft: Left-edge of the visible portion of the workspace, relative to
* the workspace origin.
* .contentTop: Top-edge of the content, relative to the workspace origin.
* .contentLeft: Left-edge of the content relative to the workspace origin.
* .scrollTop: Top-edge of the scroll area, relative to the workspace origin.
* .scrollLeft: Left-edge of the scroll area relative to the workspace origin.
* .absoluteTop: Top-edge of the visible portion of the workspace, relative
* to the blocklyDiv.
* .absoluteLeft: Left-edge of the visible portion of the workspace, relative
* to the blocklyDiv.
* .toolboxWidth: Width of the toolbox, if it exists. Otherwise zero.
* .toolboxHeight: Height of the toolbox, if it exists. Otherwise zero.
* .flyoutWidth: Width of the flyout if it is always open. Otherwise zero.
* .flyoutHeight: Height of the flyout if it is always open. Otherwise zero.
* .toolboxPosition: Top, bottom, left or right. Use TOOLBOX_AT constants to
* compare.
* @return {!Metrics} Contains size and position metrics of a top
* level workspace.
* @public
*/
MetricsManager.prototype.getMetrics = function() {
const toolboxMetrics = this.getToolboxMetrics();
const flyoutMetrics = this.getFlyoutMetrics(true);
const svgMetrics = this.getSvgMetrics();
const absoluteMetrics = this.getAbsoluteMetrics();
const viewMetrics = this.getViewMetrics();
const contentMetrics = this.getContentMetrics();
const scrollMetrics =
this.getScrollMetrics(false, viewMetrics, contentMetrics);
return {
contentHeight: contentMetrics.height,
contentWidth: contentMetrics.width,
contentTop: contentMetrics.top,
contentLeft: contentMetrics.left,
scrollHeight: scrollMetrics.height,
scrollWidth: scrollMetrics.width,
scrollTop: scrollMetrics.top,
scrollLeft: scrollMetrics.left,
viewHeight: viewMetrics.height,
viewWidth: viewMetrics.width,
viewTop: viewMetrics.top,
viewLeft: viewMetrics.left,
absoluteTop: absoluteMetrics.top,
absoluteLeft: absoluteMetrics.left,
svgHeight: svgMetrics.height,
svgWidth: svgMetrics.width,
toolboxWidth: toolboxMetrics.width,
toolboxHeight: toolboxMetrics.height,
toolboxPosition: toolboxMetrics.position,
flyoutWidth: flyoutMetrics.width,
flyoutHeight: flyoutMetrics.height,
};
};
registry.register(
registry.Type.METRICS_MANAGER, registry.DEFAULT, MetricsManager);

View File

@@ -19,7 +19,6 @@ goog.module('Blockly.TouchGesture');
const Touch = goog.require('Blockly.Touch');
const browserEvents = goog.require('Blockly.browserEvents');
const object = goog.require('Blockly.utils.object');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
const {Gesture} = goog.require('Blockly.Gesture');
/* eslint-disable-next-line no-unused-vars */
@@ -31,300 +30,304 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
* events. "End" refers to touchend, mouseup, and pointerend events.
*/
/**
* Class for one gesture.
* @param {!Event} e The event that kicked off this gesture.
* @param {!WorkspaceSvg} creatorWorkspace The workspace that created
* this gesture and has a reference to it.
* @extends {Gesture}
* @constructor
* @alias Blockly.TouchGesture
*/
const TouchGesture = function(e, creatorWorkspace) {
TouchGesture.superClass_.constructor.call(this, e, creatorWorkspace);
/**
* Boolean for whether or not this gesture is a multi-touch gesture.
* @type {boolean}
* @private
*/
this.isMultiTouch_ = false;
/**
* A map of cached points used for tracking multi-touch gestures.
* @type {!Object<number|string, Coordinate>}
* @private
*/
this.cachedPoints_ = Object.create(null);
/**
* This is the ratio between the starting distance between the touch points
* and the most recent distance between the touch points.
* Scales between 0 and 1 mean the most recent zoom was a zoom out.
* Scales above 1.0 mean the most recent zoom was a zoom in.
* @type {number}
* @private
*/
this.previousScale_ = 0;
/**
* The starting distance between two touch points.
* @type {number}
* @private
*/
this.startDistance_ = 0;
/**
* A handle to use to unbind the second touch start or pointer down listener
* at the end of a drag.
* Opaque data returned from Blockly.bindEventWithChecks_.
* @type {?browserEvents.Data}
* @private
*/
this.onStartWrapper_ = null;
/**
* Boolean for whether or not the workspace supports pinch-zoom.
* @type {?boolean}
* @private
*/
this.isPinchZoomEnabled_ = null;
};
object.inherits(TouchGesture, Gesture);
/**
* A multiplier used to convert the gesture scale to a zoom in delta.
* @const
*/
TouchGesture.ZOOM_IN_MULTIPLIER = 5;
const ZOOM_IN_MULTIPLIER = 5;
/**
* A multiplier used to convert the gesture scale to a zoom out delta.
* @const
*/
TouchGesture.ZOOM_OUT_MULTIPLIER = 6;
const ZOOM_OUT_MULTIPLIER = 6;
/**
* Start a gesture: update the workspace to indicate that a gesture is in
* progress and bind mousemove and mouseup handlers.
* @param {!Event} e A mouse down, touch start or pointer down event.
* @package
* Class for one gesture.
* @extends {Gesture}
*/
TouchGesture.prototype.doStart = function(e) {
this.isPinchZoomEnabled_ = this.startWorkspace_.options.zoomOptions &&
this.startWorkspace_.options.zoomOptions.pinch;
TouchGesture.superClass_.doStart.call(this, e);
if (!this.isEnding_ && Touch.isTouchEvent(e)) {
this.handleTouchStart(e);
class TouchGesture extends Gesture {
/**
* @param {!Event} e The event that kicked off this gesture.
* @param {!WorkspaceSvg} creatorWorkspace The workspace that created
* this gesture and has a reference to it.
* @alias Blockly.TouchGesture
*/
constructor(e, creatorWorkspace) {
super(e, creatorWorkspace);
/**
* Boolean for whether or not this gesture is a multi-touch gesture.
* @type {boolean}
* @private
*/
this.isMultiTouch_ = false;
/**
* A map of cached points used for tracking multi-touch gestures.
* @type {!Object<number|string, Coordinate>}
* @private
*/
this.cachedPoints_ = Object.create(null);
/**
* This is the ratio between the starting distance between the touch points
* and the most recent distance between the touch points.
* Scales between 0 and 1 mean the most recent zoom was a zoom out.
* Scales above 1.0 mean the most recent zoom was a zoom in.
* @type {number}
* @private
*/
this.previousScale_ = 0;
/**
* The starting distance between two touch points.
* @type {number}
* @private
*/
this.startDistance_ = 0;
/**
* A handle to use to unbind the second touch start or pointer down listener
* at the end of a drag.
* Opaque data returned from Blockly.bindEventWithChecks_.
* @type {?browserEvents.Data}
* @private
*/
this.onStartWrapper_ = null;
/**
* Boolean for whether or not the workspace supports pinch-zoom.
* @type {?boolean}
* @private
*/
this.isPinchZoomEnabled_ = null;
}
};
/**
* Bind gesture events.
* Overriding the gesture definition of this function, binding the same
* functions for onMoveWrapper_ and onUpWrapper_ but passing
* opt_noCaptureIdentifier.
* In addition, binding a second mouse down event to detect multi-touch events.
* @param {!Event} e A mouse down or touch start event.
* @package
*/
TouchGesture.prototype.bindMouseEvents = function(e) {
this.onStartWrapper_ = browserEvents.conditionalBind(
document, 'mousedown', null, this.handleStart.bind(this),
/* opt_noCaptureIdentifier */ true);
this.onMoveWrapper_ = browserEvents.conditionalBind(
document, 'mousemove', null, this.handleMove.bind(this),
/* opt_noCaptureIdentifier */ true);
this.onUpWrapper_ = browserEvents.conditionalBind(
document, 'mouseup', null, this.handleUp.bind(this),
/* opt_noCaptureIdentifier */ true);
e.preventDefault();
e.stopPropagation();
};
/**
* Handle a mouse down, touch start, or pointer down event.
* @param {!Event} e A mouse down, touch start, or pointer down event.
* @package
*/
TouchGesture.prototype.handleStart = function(e) {
if (this.isDragging()) {
// A drag has already started, so this can no longer be a pinch-zoom.
return;
}
if (Touch.isTouchEvent(e)) {
this.handleTouchStart(e);
if (this.isMultiTouch()) {
Touch.longStop();
/**
* Start a gesture: update the workspace to indicate that a gesture is in
* progress and bind mousemove and mouseup handlers.
* @param {!Event} e A mouse down, touch start or pointer down event.
* @package
*/
doStart(e) {
this.isPinchZoomEnabled_ = this.startWorkspace_.options.zoomOptions &&
this.startWorkspace_.options.zoomOptions.pinch;
super.doStart(e);
if (!this.isEnding_ && Touch.isTouchEvent(e)) {
this.handleTouchStart(e);
}
}
};
/**
* Handle a mouse move, touch move, or pointer move event.
* @param {!Event} e A mouse move, touch move, or pointer move event.
* @package
*/
TouchGesture.prototype.handleMove = function(e) {
if (this.isDragging()) {
// We are in the middle of a drag, only handle the relevant events
if (Touch.shouldHandleEvent(e)) {
TouchGesture.superClass_.handleMove.call(this, e);
}
return;
}
if (this.isMultiTouch()) {
if (Touch.isTouchEvent(e)) {
this.handleTouchMove(e);
}
Touch.longStop();
} else {
TouchGesture.superClass_.handleMove.call(this, e);
}
};
/**
* Bind gesture events.
* Overriding the gesture definition of this function, binding the same
* functions for onMoveWrapper_ and onUpWrapper_ but passing
* opt_noCaptureIdentifier.
* In addition, binding a second mouse down event to detect multi-touch
* events.
* @param {!Event} e A mouse down or touch start event.
* @package
*/
bindMouseEvents(e) {
this.onStartWrapper_ = browserEvents.conditionalBind(
document, 'mousedown', null, this.handleStart.bind(this),
/* opt_noCaptureIdentifier */ true);
this.onMoveWrapper_ = browserEvents.conditionalBind(
document, 'mousemove', null, this.handleMove.bind(this),
/* opt_noCaptureIdentifier */ true);
this.onUpWrapper_ = browserEvents.conditionalBind(
document, 'mouseup', null, this.handleUp.bind(this),
/* opt_noCaptureIdentifier */ true);
/**
* Handle a mouse up, touch end, or pointer up event.
* @param {!Event} e A mouse up, touch end, or pointer up event.
* @package
*/
TouchGesture.prototype.handleUp = function(e) {
if (Touch.isTouchEvent(e) && !this.isDragging()) {
this.handleTouchEnd(e);
}
if (!this.isMultiTouch() || this.isDragging()) {
if (!Touch.shouldHandleEvent(e)) {
return;
}
TouchGesture.superClass_.handleUp.call(this, e);
} else {
e.preventDefault();
e.stopPropagation();
this.dispose();
}
};
/**
* Whether this gesture is part of a multi-touch gesture.
* @return {boolean} Whether this gesture is part of a multi-touch gesture.
* @package
*/
TouchGesture.prototype.isMultiTouch = function() {
return this.isMultiTouch_;
};
/**
* Handle a mouse down, touch start, or pointer down event.
* @param {!Event} e A mouse down, touch start, or pointer down event.
* @package
*/
handleStart(e) {
if (this.isDragging()) {
// A drag has already started, so this can no longer be a pinch-zoom.
return;
}
if (Touch.isTouchEvent(e)) {
this.handleTouchStart(e);
/**
* Sever all links from this object.
* @package
*/
TouchGesture.prototype.dispose = function() {
TouchGesture.superClass_.dispose.call(this);
if (this.onStartWrapper_) {
browserEvents.unbind(this.onStartWrapper_);
if (this.isMultiTouch()) {
Touch.longStop();
}
}
}
};
/**
* Handle a touch start or pointer down event and keep track of current
* pointers.
* @param {!Event} e A touch start, or pointer down event.
* @package
*/
TouchGesture.prototype.handleTouchStart = function(e) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
// store the pointerId in the current list of pointers
this.cachedPoints_[pointerId] = this.getTouchPoint(e);
const pointers = Object.keys(this.cachedPoints_);
// If two pointers are down, store info
if (pointers.length === 2) {
/**
* Handle a mouse move, touch move, or pointer move event.
* @param {!Event} e A mouse move, touch move, or pointer move event.
* @package
*/
handleMove(e) {
if (this.isDragging()) {
// We are in the middle of a drag, only handle the relevant events
if (Touch.shouldHandleEvent(e)) {
super.handleMove(e);
}
return;
}
if (this.isMultiTouch()) {
if (Touch.isTouchEvent(e)) {
this.handleTouchMove(e);
}
Touch.longStop();
} else {
super.handleMove(e);
}
}
/**
* Handle a mouse up, touch end, or pointer up event.
* @param {!Event} e A mouse up, touch end, or pointer up event.
* @package
*/
handleUp(e) {
if (Touch.isTouchEvent(e) && !this.isDragging()) {
this.handleTouchEnd(e);
}
if (!this.isMultiTouch() || this.isDragging()) {
if (!Touch.shouldHandleEvent(e)) {
return;
}
super.handleUp(e);
} else {
e.preventDefault();
e.stopPropagation();
this.dispose();
}
}
/**
* Whether this gesture is part of a multi-touch gesture.
* @return {boolean} Whether this gesture is part of a multi-touch gesture.
* @package
*/
isMultiTouch() {
return this.isMultiTouch_;
}
/**
* Sever all links from this object.
* @package
*/
dispose() {
super.dispose();
if (this.onStartWrapper_) {
browserEvents.unbind(this.onStartWrapper_);
}
}
/**
* Handle a touch start or pointer down event and keep track of current
* pointers.
* @param {!Event} e A touch start, or pointer down event.
* @package
*/
handleTouchStart(e) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
// store the pointerId in the current list of pointers
this.cachedPoints_[pointerId] = this.getTouchPoint(e);
const pointers = Object.keys(this.cachedPoints_);
// If two pointers are down, store info
if (pointers.length === 2) {
const point0 =
/** @type {!Coordinate} */ (this.cachedPoints_[pointers[0]]);
const point1 =
/** @type {!Coordinate} */ (this.cachedPoints_[pointers[1]]);
this.startDistance_ = Coordinate.distance(point0, point1);
this.isMultiTouch_ = true;
e.preventDefault();
}
}
/**
* Handle a touch move or pointer move event and zoom in/out if two pointers
* are on the screen.
* @param {!Event} e A touch move, or pointer move event.
* @package
*/
handleTouchMove(e) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
// Update the cache
this.cachedPoints_[pointerId] = this.getTouchPoint(e);
const pointers = Object.keys(this.cachedPoints_);
if (this.isPinchZoomEnabled_ && pointers.length === 2) {
this.handlePinch_(e);
} else {
super.handleMove(e);
}
}
/**
* Handle pinch zoom gesture.
* @param {!Event} e A touch move, or pointer move event.
* @private
*/
handlePinch_(e) {
const pointers = Object.keys(this.cachedPoints_);
// Calculate the distance between the two pointers
const point0 = /** @type {!Coordinate} */ (this.cachedPoints_[pointers[0]]);
const point1 = /** @type {!Coordinate} */ (this.cachedPoints_[pointers[1]]);
this.startDistance_ = Coordinate.distance(point0, point1);
this.isMultiTouch_ = true;
const moveDistance = Coordinate.distance(point0, point1);
const scale = moveDistance / this.startDistance_;
if (this.previousScale_ > 0 && this.previousScale_ < Infinity) {
const gestureScale = scale - this.previousScale_;
const delta = gestureScale > 0 ? gestureScale * ZOOM_IN_MULTIPLIER :
gestureScale * ZOOM_OUT_MULTIPLIER;
const workspace = this.startWorkspace_;
const position = browserEvents.mouseToSvg(
e, workspace.getParentSvg(), workspace.getInverseScreenCTM());
workspace.zoom(position.x, position.y, delta);
}
this.previousScale_ = scale;
e.preventDefault();
}
};
/**
* Handle a touch move or pointer move event and zoom in/out if two pointers
* are on the screen.
* @param {!Event} e A touch move, or pointer move event.
* @package
*/
TouchGesture.prototype.handleTouchMove = function(e) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
// Update the cache
this.cachedPoints_[pointerId] = this.getTouchPoint(e);
const pointers = Object.keys(this.cachedPoints_);
if (this.isPinchZoomEnabled_ && pointers.length === 2) {
this.handlePinch_(e);
} else {
TouchGesture.superClass_.handleMove.call(this, e);
/**
* Handle a touch end or pointer end event and end the gesture.
* @param {!Event} e A touch end, or pointer end event.
* @package
*/
handleTouchEnd(e) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
if (this.cachedPoints_[pointerId]) {
delete this.cachedPoints_[pointerId];
}
if (Object.keys(this.cachedPoints_).length < 2) {
this.cachedPoints_ = Object.create(null);
this.previousScale_ = 0;
}
}
};
/**
* Handle pinch zoom gesture.
* @param {!Event} e A touch move, or pointer move event.
* @private
*/
TouchGesture.prototype.handlePinch_ = function(e) {
const pointers = Object.keys(this.cachedPoints_);
// Calculate the distance between the two pointers
const point0 = /** @type {!Coordinate} */ (this.cachedPoints_[pointers[0]]);
const point1 = /** @type {!Coordinate} */ (this.cachedPoints_[pointers[1]]);
const moveDistance = Coordinate.distance(point0, point1);
const scale = moveDistance / this.startDistance_;
if (this.previousScale_ > 0 && this.previousScale_ < Infinity) {
const gestureScale = scale - this.previousScale_;
const delta = gestureScale > 0 ?
gestureScale * TouchGesture.ZOOM_IN_MULTIPLIER :
gestureScale * TouchGesture.ZOOM_OUT_MULTIPLIER;
const workspace = this.startWorkspace_;
const position = browserEvents.mouseToSvg(
e, workspace.getParentSvg(), workspace.getInverseScreenCTM());
workspace.zoom(position.x, position.y, delta);
/**
* Helper function returning the current touch point coordinate.
* @param {!Event} e A touch or pointer event.
* @return {?Coordinate} The current touch point coordinate
* @package
*/
getTouchPoint(e) {
if (!this.startWorkspace_) {
return null;
}
return new Coordinate(
(e.changedTouches ? e.changedTouches[0].pageX : e.pageX),
(e.changedTouches ? e.changedTouches[0].pageY : e.pageY));
}
this.previousScale_ = scale;
e.preventDefault();
};
/**
* Handle a touch end or pointer end event and end the gesture.
* @param {!Event} e A touch end, or pointer end event.
* @package
*/
TouchGesture.prototype.handleTouchEnd = function(e) {
const pointerId = Touch.getTouchIdentifierFromEvent(e);
if (this.cachedPoints_[pointerId]) {
delete this.cachedPoints_[pointerId];
}
if (Object.keys(this.cachedPoints_).length < 2) {
this.cachedPoints_ = Object.create(null);
this.previousScale_ = 0;
}
};
/**
* Helper function returning the current touch point coordinate.
* @param {!Event} e A touch or pointer event.
* @return {?Coordinate} The current touch point coordinate
* @package
*/
TouchGesture.prototype.getTouchPoint = function(e) {
if (!this.startWorkspace_) {
return null;
}
return new Coordinate(
(e.changedTouches ? e.changedTouches[0].pageX : e.pageX),
(e.changedTouches ? e.changedTouches[0].pageY : e.pageY));
};
}
exports.TouchGesture = TouchGesture;

File diff suppressed because it is too large Load Diff