mirror of
https://github.com/google/blockly.git
synced 2026-01-08 17:40:09 +01:00
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:
2089
core/flyout_base.js
2089
core/flyout_base.js
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
1878
core/gesture.js
1878
core/gesture.js
File diff suppressed because it is too large
Load Diff
359
core/grid.js
359
core/grid.js
@@ -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;
|
||||
|
||||
523
core/input.js
523
core/input.js
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
1190
core/trashcan.js
1190
core/trashcan.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user