mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-06-06 01:44:35 -04:00
2267 lines
72 KiB
JavaScript
2267 lines
72 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2014 Google Inc.
|
|
* https://developers.google.com/blockly/
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Object representing a workspace rendered as SVG.
|
|
* @author fraser@google.com (Neil Fraser)
|
|
*/
|
|
'use strict';
|
|
|
|
goog.provide('Blockly.WorkspaceSvg');
|
|
|
|
// TODO(scr): Fix circular dependencies
|
|
//goog.require('Blockly.BlockSvg');
|
|
goog.require('Blockly.Colours');
|
|
goog.require('Blockly.ConnectionDB');
|
|
goog.require('Blockly.constants');
|
|
goog.require('Blockly.DataCategory');
|
|
goog.require('Blockly.DropDownDiv');
|
|
goog.require('Blockly.Events.BlockCreate');
|
|
goog.require('Blockly.Gesture');
|
|
goog.require('Blockly.Grid');
|
|
goog.require('Blockly.Options');
|
|
goog.require('Blockly.scratchBlocksUtils');
|
|
goog.require('Blockly.ScrollbarPair');
|
|
goog.require('Blockly.Touch');
|
|
goog.require('Blockly.Trashcan');
|
|
//goog.require('Blockly.VerticalFlyout');
|
|
goog.require('Blockly.Workspace');
|
|
goog.require('Blockly.WorkspaceAudio');
|
|
goog.require('Blockly.WorkspaceComment');
|
|
goog.require('Blockly.WorkspaceCommentSvg');
|
|
goog.require('Blockly.WorkspaceCommentSvg.render');
|
|
goog.require('Blockly.WorkspaceDragSurfaceSvg');
|
|
goog.require('Blockly.Xml');
|
|
goog.require('Blockly.ZoomControls');
|
|
|
|
goog.require('goog.array');
|
|
goog.require('goog.dom');
|
|
goog.require('goog.math.Coordinate');
|
|
goog.require('goog.userAgent');
|
|
goog.require('goog.math.Rect');
|
|
|
|
/**
|
|
* Class for a workspace. This is an onscreen area with optional trashcan,
|
|
* scrollbars, bubbles, and dragging.
|
|
* @param {!Blockly.Options} options Dictionary of options.
|
|
* @param {Blockly.BlockDragSurfaceSvg=} opt_blockDragSurface Drag surface for
|
|
* blocks.
|
|
* @param {Blockly.WorkspaceDragSurfaceSvg=} opt_wsDragSurface Drag surface for
|
|
* the workspace.
|
|
* @extends {Blockly.Workspace}
|
|
* @constructor
|
|
*/
|
|
Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface) {
|
|
Blockly.WorkspaceSvg.superClass_.constructor.call(this, options);
|
|
this.getMetrics =
|
|
options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_;
|
|
this.setMetrics =
|
|
options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_;
|
|
|
|
Blockly.ConnectionDB.init(this);
|
|
|
|
if (opt_blockDragSurface) {
|
|
this.blockDragSurface_ = opt_blockDragSurface;
|
|
}
|
|
|
|
if (opt_wsDragSurface) {
|
|
this.workspaceDragSurface_ = opt_wsDragSurface;
|
|
}
|
|
|
|
this.useWorkspaceDragSurface_ =
|
|
this.workspaceDragSurface_ && Blockly.utils.is3dSupported();
|
|
|
|
/**
|
|
* List of currently highlighted blocks. Block highlighting is often used to
|
|
* visually mark blocks currently being executed.
|
|
* @type !Array.<!Blockly.BlockSvg>
|
|
* @private
|
|
*/
|
|
this.highlightedBlocks_ = [];
|
|
|
|
/**
|
|
* Object in charge of loading, storing, and playing audio for a workspace.
|
|
* @type {Blockly.WorkspaceAudio}
|
|
* @private
|
|
*/
|
|
this.audioManager_ = new Blockly.WorkspaceAudio(options.parentWorkspace);
|
|
|
|
/**
|
|
* This workspace's grid object or null.
|
|
* @type {Blockly.Grid}
|
|
* @private
|
|
*/
|
|
this.grid_ = this.options.gridPattern ?
|
|
new Blockly.Grid(options.gridPattern, options.gridOptions) : null;
|
|
|
|
this.registerToolboxCategoryCallback(Blockly.VARIABLE_CATEGORY_NAME,
|
|
Blockly.DataCategory);
|
|
this.registerToolboxCategoryCallback(Blockly.PROCEDURE_CATEGORY_NAME,
|
|
Blockly.Procedures.flyoutCategory);
|
|
};
|
|
goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace);
|
|
|
|
/**
|
|
* A wrapper function called when a resize event occurs.
|
|
* You can pass the result to `unbindEvent_`.
|
|
* @type {Array.<!Array>}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null;
|
|
|
|
/**
|
|
* The render status of an SVG workspace.
|
|
* Returns `false` for headless workspaces and true for instances of
|
|
* `Blockly.WorkspaceSvg`.
|
|
* @type {boolean}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.rendered = true;
|
|
|
|
/**
|
|
* Whether the workspace is visible. False if the workspace has been hidden
|
|
* by calling `setVisible(false)`.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isVisible_ = true;
|
|
|
|
/**
|
|
* Is this workspace the surface for a flyout?
|
|
* @type {boolean}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isFlyout = false;
|
|
|
|
/**
|
|
* Is this workspace the surface for a mutator?
|
|
* @type {boolean}
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isMutator = false;
|
|
|
|
/**
|
|
* Whether this workspace has resizes enabled.
|
|
* Disable during batch operations for a performance improvement.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.resizesEnabled_ = true;
|
|
|
|
/**
|
|
* Whether this workspace has toolbox/flyout refreshes enabled.
|
|
* Disable during batch operations for a performance improvement.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.toolboxRefreshEnabled_ = true;
|
|
|
|
/**
|
|
* Current horizontal scrolling offset in pixel units.
|
|
* @type {number}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.scrollX = 0;
|
|
|
|
/**
|
|
* Current vertical scrolling offset in pixel units.
|
|
* @type {number}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.scrollY = 0;
|
|
|
|
/**
|
|
* Horizontal scroll value when scrolling started in pixel units.
|
|
* @type {number}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.startScrollX = 0;
|
|
|
|
/**
|
|
* Vertical scroll value when scrolling started in pixel units.
|
|
* @type {number}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.startScrollY = 0;
|
|
|
|
/**
|
|
* Distance from mouse to object being dragged.
|
|
* @type {goog.math.Coordinate}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.dragDeltaXY_ = null;
|
|
|
|
/**
|
|
* Current scale.
|
|
* @type {number}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.scale = 1;
|
|
|
|
/**
|
|
* The workspace's trashcan (if any).
|
|
* @type {Blockly.Trashcan}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.trashcan = null;
|
|
|
|
/**
|
|
* This workspace's scrollbars, if they exist.
|
|
* @type {Blockly.ScrollbarPair}
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.scrollbar = null;
|
|
|
|
/**
|
|
* The current gesture in progress on this workspace, if any.
|
|
* @type {Blockly.Gesture}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.currentGesture_ = null;
|
|
|
|
/**
|
|
* This workspace's surface for dragging blocks, if it exists.
|
|
* @type {Blockly.BlockDragSurfaceSvg}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.blockDragSurface_ = null;
|
|
|
|
/**
|
|
* This workspace's drag surface, if it exists.
|
|
* @type {Blockly.WorkspaceDragSurfaceSvg}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.workspaceDragSurface_ = null;
|
|
|
|
/**
|
|
* Whether to move workspace to the drag surface when it is dragged.
|
|
* True if it should move, false if it should be translated directly.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.useWorkspaceDragSurface_ = false;
|
|
|
|
/**
|
|
* Whether the drag surface is actively in use. When true, calls to
|
|
* translate will translate the drag surface instead of the translating the
|
|
* workspace directly.
|
|
* This is set to true in setupDragSurface and to false in resetDragSurface.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isDragSurfaceActive_ = false;
|
|
|
|
/**
|
|
* The first parent div with 'injectionDiv' in the name, or null if not set.
|
|
* Access this with getInjectionDiv.
|
|
* @type {!Element}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.injectionDiv_ = null;
|
|
|
|
/**
|
|
* Last known position of the page scroll.
|
|
* This is used to determine whether we have recalculated screen coordinate
|
|
* stuff since the page scrolled.
|
|
* @type {!goog.math.Coordinate}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.lastRecordedPageScroll_ = null;
|
|
|
|
/**
|
|
* Map from function names to callbacks, for deciding what to do when a button
|
|
* is clicked.
|
|
* @type {!Object.<string, function(!Blockly.FlyoutButton)>}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.flyoutButtonCallbacks_ = {};
|
|
|
|
/**
|
|
* Map from function names to callbacks, for deciding what to do when a custom
|
|
* toolbox category is opened.
|
|
* @type {!Object.<string, function(!Blockly.Workspace):!Array.<!Element>>}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.toolboxCategoryCallbacks_ = {};
|
|
|
|
/**
|
|
* Inverted screen CTM, for use in mouseToSvg.
|
|
* @type {SVGMatrix}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.inverseScreenCTM_ = null;
|
|
|
|
/**
|
|
* Inverted screen CTM is dirty.
|
|
* @type {Boolean}
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.inverseScreenCTMDirty_ = true;
|
|
|
|
/**
|
|
* Getter for the inverted screen CTM.
|
|
* @return {SVGMatrix} The matrix to use in mouseToSvg
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getInverseScreenCTM = function() {
|
|
|
|
// Defer getting the screen CTM until we actually need it, this should
|
|
// avoid forced reflows from any calls to updateInverseScreenCTM.
|
|
if (this.inverseScreenCTMDirty_) {
|
|
var ctm = this.getParentSvg().getScreenCTM();
|
|
if (ctm) {
|
|
this.inverseScreenCTM_ = ctm.inverse();
|
|
this.inverseScreenCTMDirty_ = false;
|
|
}
|
|
}
|
|
|
|
return this.inverseScreenCTM_;
|
|
};
|
|
|
|
/**
|
|
* Getter for isVisible
|
|
* @return {boolean} Whether the workspace is visible. False if the workspace has been hidden
|
|
* by calling `setVisible(false)`.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isVisible = function() {
|
|
return this.isVisible_;
|
|
};
|
|
|
|
/**
|
|
* Mark the inverse screen CTM as dirty.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.updateInverseScreenCTM = function() {
|
|
this.inverseScreenCTMDirty_ = true;
|
|
};
|
|
|
|
/**
|
|
* Return the absolute coordinates of the top-left corner of this element,
|
|
* scales that after canvas SVG element, if it's a descendant.
|
|
* The origin (0,0) is the top-left corner of the Blockly SVG.
|
|
* @param {!Element} element Element to find the coordinates of.
|
|
* @return {!goog.math.Coordinate} Object with .x and .y properties.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getSvgXY = function(element) {
|
|
var x = 0;
|
|
var y = 0;
|
|
var scale = 1;
|
|
if (goog.dom.contains(this.getCanvas(), element) ||
|
|
goog.dom.contains(this.getBubbleCanvas(), element)) {
|
|
// Before the SVG canvas, scale the coordinates.
|
|
scale = this.scale;
|
|
}
|
|
do {
|
|
// Loop through this block and every parent.
|
|
var xy = Blockly.utils.getRelativeXY(element);
|
|
if (element == this.getCanvas() ||
|
|
element == this.getBubbleCanvas()) {
|
|
// After the SVG canvas, don't scale the coordinates.
|
|
scale = 1;
|
|
}
|
|
x += xy.x * scale;
|
|
y += xy.y * scale;
|
|
element = element.parentNode;
|
|
} while (element && element != this.getParentSvg());
|
|
return new goog.math.Coordinate(x, y);
|
|
};
|
|
|
|
/**
|
|
* Return the position of the workspace origin relative to the injection div
|
|
* origin in pixels.
|
|
* The workspace origin is where a block would render at position (0, 0).
|
|
* It is not the upper left corner of the workspace SVG.
|
|
* @return {!goog.math.Coordinate} Offset in pixels.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getOriginOffsetInPixels = function() {
|
|
return Blockly.utils.getInjectionDivXY_(this.svgBlockCanvas_);
|
|
};
|
|
|
|
/**
|
|
* Return the injection div that is a parent of this workspace.
|
|
* Walks the DOM the first time it's called, then returns a cached value.
|
|
* @return {!Element} The first parent div with 'injectionDiv' in the name.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getInjectionDiv = function() {
|
|
// NB: it would be better to pass this in at createDom, but is more likely to
|
|
// break existing uses of Blockly.
|
|
if (!this.injectionDiv_) {
|
|
var element = this.svgGroup_;
|
|
while (element) {
|
|
var classes = element.getAttribute('class') || '';
|
|
if ((' ' + classes + ' ').indexOf(' injectionDiv ') != -1) {
|
|
this.injectionDiv_ = element;
|
|
break;
|
|
}
|
|
element = element.parentNode;
|
|
}
|
|
}
|
|
return this.injectionDiv_;
|
|
};
|
|
|
|
/**
|
|
* Save resize handler data so we can delete it later in dispose.
|
|
* @param {!Array.<!Array>} handler Data that can be passed to unbindEvent_.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) {
|
|
this.resizeHandlerWrapper_ = handler;
|
|
};
|
|
|
|
/**
|
|
* Create the workspace DOM elements.
|
|
* @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
|
|
* 'blocklyMutatorBackground'.
|
|
* @return {!Element} The workspace's SVG group.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
|
|
/**
|
|
* <g class="blocklyWorkspace">
|
|
* <rect class="blocklyMainBackground" height="100%" width="100%"></rect>
|
|
* [Trashcan and/or flyout may go here]
|
|
* <g class="blocklyBlockCanvas"></g>
|
|
* <g class="blocklyBubbleCanvas"></g>
|
|
* </g>
|
|
* @type {SVGElement}
|
|
*/
|
|
this.svgGroup_ = Blockly.utils.createSvgElement('g',
|
|
{'class': 'blocklyWorkspace'}, null);
|
|
|
|
// Note that a <g> alone does not receive mouse events--it must have a
|
|
// valid target inside it. If no background class is specified, as in the
|
|
// flyout, the workspace will not receive mouse events.
|
|
if (opt_backgroundClass) {
|
|
/** @type {SVGElement} */
|
|
this.svgBackground_ = Blockly.utils.createSvgElement('rect',
|
|
{'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
|
|
this.svgGroup_);
|
|
|
|
if (opt_backgroundClass == 'blocklyMainBackground' && this.grid_) {
|
|
this.svgBackground_.style.fill =
|
|
'url(#' + this.grid_.getPatternId() + ')';
|
|
}
|
|
}
|
|
/** @type {SVGElement} */
|
|
this.svgBlockCanvas_ = Blockly.utils.createSvgElement('g',
|
|
{'class': 'blocklyBlockCanvas'}, this.svgGroup_, this);
|
|
/** @type {SVGElement} */
|
|
this.svgBubbleCanvas_ = Blockly.utils.createSvgElement('g',
|
|
{'class': 'blocklyBubbleCanvas'}, this.svgGroup_, this);
|
|
var bottom = Blockly.Scrollbar.scrollbarThickness;
|
|
if (this.options.hasTrashcan) {
|
|
bottom = this.addTrashcan_(bottom);
|
|
}
|
|
if (this.options.zoomOptions && this.options.zoomOptions.controls) {
|
|
this.addZoomControls_(bottom);
|
|
}
|
|
|
|
if (!this.isFlyout) {
|
|
Blockly.bindEventWithChecks_(this.svgGroup_, 'mousedown', this,
|
|
this.onMouseDown_);
|
|
if (this.options.zoomOptions && this.options.zoomOptions.wheel) {
|
|
// Mouse-wheel.
|
|
Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this,
|
|
this.onMouseWheel_);
|
|
}
|
|
}
|
|
|
|
// Determine if there needs to be a category tree, or a simple list of
|
|
// blocks. This cannot be changed later, since the UI is very different.
|
|
if (this.options.hasCategories) {
|
|
/**
|
|
* @type {Blockly.Toolbox}
|
|
* @private
|
|
*/
|
|
this.toolbox_ = new Blockly.Toolbox(this);
|
|
}
|
|
if (this.grid_) {
|
|
this.grid_.update(this.scale);
|
|
}
|
|
this.recordCachedAreas();
|
|
return this.svgGroup_;
|
|
};
|
|
|
|
/**
|
|
* Dispose of this workspace.
|
|
* Unlink from all DOM elements to prevent memory leaks.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.dispose = function() {
|
|
// Stop rerendering.
|
|
this.rendered = false;
|
|
if (this.currentGesture_) {
|
|
this.currentGesture_.cancel();
|
|
}
|
|
Blockly.WorkspaceSvg.superClass_.dispose.call(this);
|
|
if (this.svgGroup_) {
|
|
goog.dom.removeNode(this.svgGroup_);
|
|
this.svgGroup_ = null;
|
|
}
|
|
this.svgBlockCanvas_ = null;
|
|
this.svgBubbleCanvas_ = null;
|
|
if (this.toolbox_) {
|
|
this.toolbox_.dispose();
|
|
this.toolbox_ = null;
|
|
}
|
|
if (this.flyout_) {
|
|
this.flyout_.dispose();
|
|
this.flyout_ = null;
|
|
}
|
|
if (this.trashcan) {
|
|
this.trashcan.dispose();
|
|
this.trashcan = null;
|
|
}
|
|
if (this.scrollbar) {
|
|
this.scrollbar.dispose();
|
|
this.scrollbar = null;
|
|
}
|
|
if (this.zoomControls_) {
|
|
this.zoomControls_.dispose();
|
|
this.zoomControls_ = null;
|
|
}
|
|
|
|
if (this.audioManager_) {
|
|
this.audioManager_.dispose();
|
|
this.audioManager_ = null;
|
|
}
|
|
|
|
if (this.grid_) {
|
|
this.grid_.dispose();
|
|
this.grid_ = null;
|
|
}
|
|
|
|
if (this.toolboxCategoryCallbacks_) {
|
|
this.toolboxCategoryCallbacks_ = null;
|
|
}
|
|
if (this.flyoutButtonCallbacks_) {
|
|
this.flyoutButtonCallbacks_ = null;
|
|
}
|
|
if (!this.options.parentWorkspace) {
|
|
// Top-most workspace. Dispose of the div that the
|
|
// SVG is injected into (i.e. injectionDiv).
|
|
goog.dom.removeNode(this.getParentSvg().parentNode);
|
|
}
|
|
if (this.resizeHandlerWrapper_) {
|
|
Blockly.unbindEvent_(this.resizeHandlerWrapper_);
|
|
this.resizeHandlerWrapper_ = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Obtain a newly created block.
|
|
* @param {?string} prototypeName Name of the language object containing
|
|
* type-specific functions for this block.
|
|
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
|
|
* create a new ID.
|
|
* @return {!Blockly.BlockSvg} The created block.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
|
|
return new Blockly.BlockSvg(this, prototypeName, opt_id);
|
|
};
|
|
|
|
/**
|
|
* Add a trashcan.
|
|
* @param {number} bottom Distance from workspace bottom to bottom of trashcan.
|
|
* @return {number} Distance from workspace bottom to the top of trashcan.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.addTrashcan_ = function(bottom) {
|
|
/** @type {Blockly.Trashcan} */
|
|
this.trashcan = new Blockly.Trashcan(this);
|
|
var svgTrashcan = this.trashcan.createDom();
|
|
this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
|
|
return this.trashcan.init(bottom);
|
|
};
|
|
|
|
/**
|
|
* Add zoom controls.
|
|
* @param {number} bottom Distance from workspace bottom to bottom of controls.
|
|
* @return {number} Distance from workspace bottom to the top of controls.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) {
|
|
/** @type {Blockly.ZoomControls} */
|
|
this.zoomControls_ = new Blockly.ZoomControls(this);
|
|
var svgZoomControls = this.zoomControls_.createDom();
|
|
this.svgGroup_.appendChild(svgZoomControls);
|
|
return this.zoomControls_.init(bottom);
|
|
};
|
|
|
|
/**
|
|
* Add a flyout element in an element with the given tag name.
|
|
* @param {string} tagName What type of tag the flyout belongs in.
|
|
* @return {!Element} The element containing the flyout DOM.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.addFlyout_ = function(tagName) {
|
|
var workspaceOptions = {
|
|
disabledPatternId: this.options.disabledPatternId,
|
|
parentWorkspace: this,
|
|
RTL: this.RTL,
|
|
oneBasedIndex: this.options.oneBasedIndex,
|
|
horizontalLayout: this.horizontalLayout,
|
|
toolboxPosition: this.options.toolboxPosition,
|
|
stackGlowFilterId: this.options.stackGlowFilterId
|
|
};
|
|
if (this.horizontalLayout) {
|
|
this.flyout_ = new Blockly.HorizontalFlyout(workspaceOptions);
|
|
} else {
|
|
this.flyout_ = new Blockly.VerticalFlyout(workspaceOptions);
|
|
}
|
|
this.flyout_.autoClose = false;
|
|
|
|
// Return the element so that callers can place it in their desired
|
|
// spot in the DOM. For example, mutator flyouts do not go in the same place
|
|
// as main workspace flyouts.
|
|
return this.flyout_.createDom(tagName);
|
|
};
|
|
|
|
/**
|
|
* Getter for the flyout associated with this workspace. This flyout may be
|
|
* owned by either the toolbox or the workspace, depending on toolbox
|
|
* configuration. It will be null if there is no flyout.
|
|
* @return {Blockly.Flyout} The flyout on this workspace.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getFlyout = function() {
|
|
if (this.flyout_) {
|
|
return this.flyout_;
|
|
}
|
|
if (this.toolbox_) {
|
|
return this.toolbox_.flyout_;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Getter for the toolbox associated with this workspace, if one exists.
|
|
* @return {Blockly.Toolbox} The toolbox on this workspace.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getToolbox = function() {
|
|
return this.toolbox_;
|
|
};
|
|
|
|
/**
|
|
* Update items that use screen coordinate calculations
|
|
* because something has changed (e.g. scroll position, window size).
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.updateScreenCalculations_ = function() {
|
|
this.updateInverseScreenCTM();
|
|
this.recordCachedAreas();
|
|
};
|
|
|
|
/**
|
|
* If enabled, resize the parts of the workspace that change when the workspace
|
|
* contents (e.g. block positions) change. This will also scroll the
|
|
* workspace contents if needed.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.resizeContents = function() {
|
|
if (!this.resizesEnabled_ || !this.rendered) {
|
|
return;
|
|
}
|
|
if (this.scrollbar) {
|
|
// TODO(picklesrus): Once rachel-fenichel's scrollbar refactoring
|
|
// is complete, call the method that only resizes scrollbar
|
|
// based on contents.
|
|
this.scrollbar.resize();
|
|
}
|
|
this.updateInverseScreenCTM();
|
|
};
|
|
|
|
/**
|
|
* Resize and reposition all of the workspace chrome (toolbox,
|
|
* trash, scrollbars etc.)
|
|
* This should be called when something changes that
|
|
* requires recalculating dimensions and positions of the
|
|
* trash, zoom, toolbox, etc. (e.g. window resize).
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.resize = function() {
|
|
if (this.toolbox_) {
|
|
this.toolbox_.position();
|
|
}
|
|
if (this.flyout_) {
|
|
this.flyout_.position();
|
|
}
|
|
if (this.trashcan) {
|
|
this.trashcan.position();
|
|
}
|
|
if (this.zoomControls_) {
|
|
this.zoomControls_.position();
|
|
}
|
|
if (this.scrollbar) {
|
|
this.scrollbar.resize();
|
|
}
|
|
this.updateScreenCalculations_();
|
|
};
|
|
|
|
/**
|
|
* Resizes and repositions workspace chrome if the page has a new
|
|
* scroll position.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.updateScreenCalculationsIfScrolled
|
|
= function() {
|
|
/* eslint-disable indent */
|
|
var currScroll = goog.dom.getDocumentScroll();
|
|
if (!goog.math.Coordinate.equals(this.lastRecordedPageScroll_,
|
|
currScroll)) {
|
|
this.lastRecordedPageScroll_ = currScroll;
|
|
this.updateScreenCalculations_();
|
|
}
|
|
}; /* eslint-enable indent */
|
|
|
|
/**
|
|
* Get the SVG element that forms the drawing surface.
|
|
* @return {!Element} SVG element.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getCanvas = function() {
|
|
return this.svgBlockCanvas_;
|
|
};
|
|
|
|
/**
|
|
* Get the SVG element that forms the bubble surface.
|
|
* @return {!SVGGElement} SVG element.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getBubbleCanvas = function() {
|
|
return this.svgBubbleCanvas_;
|
|
};
|
|
|
|
/**
|
|
* Get the SVG element that contains this workspace.
|
|
* @return {!Element} SVG element.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getParentSvg = function() {
|
|
if (this.cachedParentSvg_) {
|
|
return this.cachedParentSvg_;
|
|
}
|
|
var element = this.svgGroup_;
|
|
while (element) {
|
|
if (element.tagName == 'svg') {
|
|
this.cachedParentSvg_ = element;
|
|
return element;
|
|
}
|
|
element = element.parentNode;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Translate this workspace to new coordinates.
|
|
* @param {number} x Horizontal translation.
|
|
* @param {number} y Vertical translation.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
|
|
if (this.useWorkspaceDragSurface_ && this.isDragSurfaceActive_) {
|
|
this.workspaceDragSurface_.translateSurface(x,y);
|
|
} else {
|
|
var translation = 'translate(' + x + ',' + y + ') ' +
|
|
'scale(' + this.scale + ')';
|
|
this.svgBlockCanvas_.setAttribute('transform', translation);
|
|
this.svgBubbleCanvas_.setAttribute('transform', translation);
|
|
}
|
|
// Now update the block drag surface if we're using one.
|
|
if (this.blockDragSurface_) {
|
|
this.blockDragSurface_.translateAndScaleGroup(x, y, this.scale);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called at the end of a workspace drag to take the contents
|
|
* out of the drag surface and put them back into the workspace SVG.
|
|
* Does nothing if the workspace drag surface is not enabled.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.resetDragSurface = function() {
|
|
// Don't do anything if we aren't using a drag surface.
|
|
if (!this.useWorkspaceDragSurface_) {
|
|
return;
|
|
}
|
|
|
|
this.isDragSurfaceActive_ = false;
|
|
|
|
var trans = this.workspaceDragSurface_.getSurfaceTranslation();
|
|
this.workspaceDragSurface_.clearAndHide(this.svgGroup_);
|
|
var translation = 'translate(' + trans.x + ',' + trans.y + ') ' +
|
|
'scale(' + this.scale + ')';
|
|
this.svgBlockCanvas_.setAttribute('transform', translation);
|
|
this.svgBubbleCanvas_.setAttribute('transform', translation);
|
|
};
|
|
|
|
/**
|
|
* Called at the beginning of a workspace drag to move contents of
|
|
* the workspace to the drag surface.
|
|
* Does nothing if the drag surface is not enabled.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.setupDragSurface = function() {
|
|
// Don't do anything if we aren't using a drag surface.
|
|
if (!this.useWorkspaceDragSurface_) {
|
|
return;
|
|
}
|
|
|
|
// This can happen if the user starts a drag, mouses up outside of the
|
|
// document where the mouseup listener is registered (e.g. outside of an
|
|
// iframe) and then moves the mouse back in the workspace. On mobile and ff,
|
|
// we get the mouseup outside the frame. On chrome and safari desktop we do
|
|
// not.
|
|
if (this.isDragSurfaceActive_) {
|
|
return;
|
|
}
|
|
|
|
this.isDragSurfaceActive_ = true;
|
|
|
|
// Figure out where we want to put the canvas back. The order
|
|
// in the is important because things are layered.
|
|
var previousElement = this.svgBlockCanvas_.previousSibling;
|
|
var width = parseInt(this.getParentSvg().getAttribute('width'), 10);
|
|
var height = parseInt(this.getParentSvg().getAttribute('height'), 10);
|
|
var coord = Blockly.utils.getRelativeXY(this.svgBlockCanvas_);
|
|
this.workspaceDragSurface_.setContentsAndShow(this.svgBlockCanvas_,
|
|
this.svgBubbleCanvas_, previousElement, width, height, this.scale);
|
|
this.workspaceDragSurface_.translateSurface(coord.x, coord.y);
|
|
};
|
|
|
|
/**
|
|
* @return {?Blockly.BlockDragSurfaceSvg} This workspace's block drag surface,
|
|
* if one is in use.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getBlockDragSurface = function() {
|
|
return this.blockDragSurface_;
|
|
};
|
|
|
|
/**
|
|
* Returns the horizontal offset of the workspace.
|
|
* Intended for LTR/RTL compatibility in XML.
|
|
* @return {number} Width.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getWidth = function() {
|
|
var metrics = this.getMetrics();
|
|
return metrics ? metrics.viewWidth / this.scale : 0;
|
|
};
|
|
|
|
/**
|
|
* Toggles the visibility of the workspace.
|
|
* Currently only intended for main workspace.
|
|
* @param {boolean} isVisible True if workspace should be visible.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.setVisible = function(isVisible) {
|
|
|
|
// Tell the scrollbar whether its container is visible so it can
|
|
// tell when to hide itself.
|
|
if (this.scrollbar) {
|
|
this.scrollbar.setContainerVisible(isVisible);
|
|
}
|
|
|
|
// Tell the flyout whether its container is visible so it can
|
|
// tell when to hide itself.
|
|
if (this.getFlyout()) {
|
|
this.getFlyout().setContainerVisible(isVisible);
|
|
}
|
|
|
|
this.getParentSvg().style.display = isVisible ? 'block' : 'none';
|
|
if (this.toolbox_) {
|
|
// Currently does not support toolboxes in mutators.
|
|
this.toolbox_.HtmlDiv.style.display = isVisible ? 'block' : 'none';
|
|
}
|
|
if (isVisible) {
|
|
this.render();
|
|
// The window may have changed size while the workspace was hidden.
|
|
// Resize recalculates scrollbar position, delete areas, etc.
|
|
this.resize();
|
|
} else {
|
|
Blockly.hideChaff(true);
|
|
Blockly.DropDownDiv.hideWithoutAnimation();
|
|
}
|
|
this.isVisible_ = isVisible;
|
|
};
|
|
|
|
/**
|
|
* Render all blocks in workspace.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.render = function() {
|
|
// Generate list of all blocks.
|
|
var blocks = this.getAllBlocks();
|
|
// Render each block.
|
|
for (var i = blocks.length - 1; i >= 0; i--) {
|
|
blocks[i].render(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Was used back when block highlighting (for execution) and block selection
|
|
* (for editing) were the same thing.
|
|
* Any calls of this function can be deleted.
|
|
* @deprecated October 2016
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.traceOn = function() {
|
|
console.warn('Deprecated call to traceOn, delete this.');
|
|
};
|
|
|
|
/**
|
|
* Highlight or unhighlight a block in the workspace. Block highlighting is
|
|
* often used to visually mark blocks currently being executed.
|
|
* @param {?string} id ID of block to highlight/unhighlight,
|
|
* or null for no block (used to unhighlight all blocks).
|
|
* @param {boolean=} opt_state If undefined, highlight specified block and
|
|
* automatically unhighlight all others. If true or false, manually
|
|
* highlight/unhighlight the specified block.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.highlightBlock = function(id, opt_state) {
|
|
if (opt_state === undefined) {
|
|
// Unhighlight all blocks.
|
|
for (var i = 0, block; block = this.highlightedBlocks_[i]; i++) {
|
|
block.setHighlighted(false);
|
|
}
|
|
this.highlightedBlocks_.length = 0;
|
|
}
|
|
// Highlight/unhighlight the specified block.
|
|
var block = id ? this.getBlockById(id) : null;
|
|
if (block) {
|
|
var state = (opt_state === undefined) || opt_state;
|
|
// Using Set here would be great, but at the cost of IE10 support.
|
|
if (!state) {
|
|
goog.array.remove(this.highlightedBlocks_, block);
|
|
} else if (this.highlightedBlocks_.indexOf(block) == -1) {
|
|
this.highlightedBlocks_.push(block);
|
|
}
|
|
block.setHighlighted(state);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Glow/unglow a block in the workspace.
|
|
* @param {?string} id ID of block to find.
|
|
* @param {boolean} isGlowingBlock Whether to glow the block.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.glowBlock = function(id, isGlowingBlock) {
|
|
var block = null;
|
|
if (id) {
|
|
block = this.getBlockById(id);
|
|
if (!block) {
|
|
throw 'Tried to glow block that does not exist.';
|
|
}
|
|
}
|
|
block.setGlowBlock(isGlowingBlock);
|
|
};
|
|
|
|
/**
|
|
* Glow/unglow a stack in the workspace.
|
|
* @param {?string} id ID of block which starts the stack.
|
|
* @param {boolean} isGlowingStack Whether to glow the stack.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.glowStack = function(id, isGlowingStack) {
|
|
var block = null;
|
|
if (id) {
|
|
block = this.getBlockById(id);
|
|
if (!block) {
|
|
throw 'Tried to glow stack on block that does not exist.';
|
|
}
|
|
}
|
|
block.setGlowStack(isGlowingStack);
|
|
};
|
|
|
|
/**
|
|
* Visually report a value associated with a block.
|
|
* In Scratch, appears as a pop-up next to the block when a reporter block is clicked.
|
|
* @param {?string} id ID of block to report associated value.
|
|
* @param {?string} value String value to visually report.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.reportValue = function(id, value) {
|
|
var block = this.getBlockById(id);
|
|
if (!block) {
|
|
throw 'Tried to report value on block that does not exist.';
|
|
}
|
|
Blockly.DropDownDiv.hideWithoutAnimation();
|
|
Blockly.DropDownDiv.clearContent();
|
|
var contentDiv = Blockly.DropDownDiv.getContentDiv();
|
|
var valueReportBox = goog.dom.createElement('div');
|
|
valueReportBox.setAttribute('class', 'valueReportBox');
|
|
valueReportBox.innerHTML = Blockly.scratchBlocksUtils.encodeEntities(value);
|
|
contentDiv.appendChild(valueReportBox);
|
|
Blockly.DropDownDiv.setColour(
|
|
Blockly.Colours.valueReportBackground,
|
|
Blockly.Colours.valueReportBorder
|
|
);
|
|
Blockly.DropDownDiv.showPositionedByBlock(this, block);
|
|
};
|
|
|
|
/**
|
|
* Paste the provided block onto the workspace.
|
|
* @param {!Element} xmlBlock XML block element.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
|
|
if (!this.rendered) {
|
|
return;
|
|
}
|
|
if (this.currentGesture_) {
|
|
this.currentGesture_.cancel(); // Dragging while pasting? No.
|
|
}
|
|
if (xmlBlock.tagName.toLowerCase() == 'comment') {
|
|
this.pasteWorkspaceComment_(xmlBlock);
|
|
} else {
|
|
this.pasteBlock_(xmlBlock);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Paste the provided block onto the workspace.
|
|
* @param {!Element} xmlBlock XML block element.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock) {
|
|
Blockly.Events.disable();
|
|
try {
|
|
var block = Blockly.Xml.domToBlock(xmlBlock, this);
|
|
// Scratch-specific: Give shadow dom new IDs to prevent duplicating on paste
|
|
Blockly.scratchBlocksUtils.changeObscuredShadowIds(block);
|
|
// Move the duplicate to original position.
|
|
var blockX = parseInt(xmlBlock.getAttribute('x'), 10);
|
|
var blockY = parseInt(xmlBlock.getAttribute('y'), 10);
|
|
if (!isNaN(blockX) && !isNaN(blockY)) {
|
|
if (this.RTL) {
|
|
blockX = -blockX;
|
|
}
|
|
// Offset block until not clobbering another block and not in connection
|
|
// distance with neighbouring blocks.
|
|
do {
|
|
var collide = false;
|
|
var allBlocks = this.getAllBlocks();
|
|
for (var i = 0, otherBlock; otherBlock = allBlocks[i]; i++) {
|
|
var otherXY = otherBlock.getRelativeToSurfaceXY();
|
|
if (Math.abs(blockX - otherXY.x) <= 1 &&
|
|
Math.abs(blockY - otherXY.y) <= 1) {
|
|
collide = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!collide) {
|
|
// Check for blocks in snap range to any of its connections.
|
|
var connections = block.getConnections_(false);
|
|
for (var i = 0, connection; connection = connections[i]; i++) {
|
|
var neighbour = connection.closest(Blockly.SNAP_RADIUS,
|
|
new goog.math.Coordinate(blockX, blockY));
|
|
if (neighbour.connection) {
|
|
collide = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (collide) {
|
|
if (this.RTL) {
|
|
blockX -= Blockly.SNAP_RADIUS;
|
|
} else {
|
|
blockX += Blockly.SNAP_RADIUS;
|
|
}
|
|
blockY += Blockly.SNAP_RADIUS * 2;
|
|
}
|
|
} while (collide);
|
|
block.moveBy(blockX, blockY);
|
|
}
|
|
} finally {
|
|
Blockly.Events.enable();
|
|
}
|
|
if (Blockly.Events.isEnabled() && !block.isShadow()) {
|
|
Blockly.Events.fire(new Blockly.Events.BlockCreate(block));
|
|
}
|
|
block.select();
|
|
};
|
|
|
|
/**
|
|
* Paste the provided comment onto the workspace.
|
|
* @param {!Element} xmlComment XML workspace comment element.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.pasteWorkspaceComment_ = function(xmlComment) {
|
|
Blockly.Events.disable();
|
|
try {
|
|
var comment = Blockly.WorkspaceCommentSvg.fromXml(xmlComment, this);
|
|
// Move the duplicate to original position.
|
|
var commentX = parseInt(xmlComment.getAttribute('x'), 10);
|
|
var commentY = parseInt(xmlComment.getAttribute('y'), 10);
|
|
if (!isNaN(commentX) && !isNaN(commentY)) {
|
|
if (this.RTL) {
|
|
commentX = -commentX;
|
|
}
|
|
// Offset workspace comment.
|
|
// TODO: (github.com/google/blockly/issues/1719) properly offset comment
|
|
// such that it's not interfereing with any blocks
|
|
commentX += 50;
|
|
commentY += 50;
|
|
comment.moveBy(commentX, commentY);
|
|
}
|
|
} finally {
|
|
Blockly.Events.enable();
|
|
}
|
|
if (Blockly.Events.isEnabled()) {
|
|
Blockly.WorkspaceComment.fireCreateEvent(comment);
|
|
}
|
|
comment.select();
|
|
};
|
|
|
|
/**
|
|
* Refresh the toolbox unless there's a drag in progress.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.refreshToolboxSelection_ = function() {
|
|
// Updating the toolbox can be expensive. Don't do it when when it is
|
|
// disabled.
|
|
if (this.toolbox_) {
|
|
if (this.toolbox_.flyout_ && !this.currentGesture_ &&
|
|
this.toolboxRefreshEnabled_) {
|
|
this.toolbox_.refreshSelection();
|
|
}
|
|
} else {
|
|
var thisTarget = this.targetWorkspace;
|
|
if (thisTarget && thisTarget.toolbox_ && thisTarget.toolbox_.flyout_ &&
|
|
!thisTarget.currentGesture_ && thisTarget.toolboxRefreshEnabled_) {
|
|
thisTarget.toolbox_.refreshSelection();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Rename a variable by updating its name in the variable map. Update the
|
|
* flyout to show the renamed variable immediately.
|
|
* @param {string} id ID of the variable to rename.
|
|
* @param {string} newName New variable name.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.renameVariableById = function(id, newName) {
|
|
Blockly.WorkspaceSvg.superClass_.renameVariableById.call(this, id, newName);
|
|
this.refreshToolboxSelection_();
|
|
};
|
|
|
|
/**
|
|
* Delete a variable by the passed in ID. Update the flyout to show
|
|
* immediately that the variable is deleted.
|
|
* @param {string} id ID of variable to delete.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.deleteVariableById = function(id) {
|
|
Blockly.WorkspaceSvg.superClass_.deleteVariableById.call(this, id);
|
|
this.refreshToolboxSelection_();
|
|
};
|
|
|
|
/**
|
|
* Create a new variable with the given name. Update the flyout to show the new
|
|
* variable immediately.
|
|
* @param {string} name The new variable's name.
|
|
* @param {string=} opt_type The type of the variable like 'int' or 'string'.
|
|
* Does not need to be unique. Field_variable can filter variables based on
|
|
* their type. This will default to '' which is a specific type.
|
|
* @param {string=} opt_id The unique ID of the variable. This will default to
|
|
* a UUID.
|
|
* @param {boolean=} opt_isLocal Whether the variable is locally scoped.
|
|
* @param {boolean=} opt_isCloud Whether the variable is a cloud variable.
|
|
* @return {?Blockly.VariableModel} The newly created variable.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.createVariable = function(name, opt_type, opt_id,
|
|
opt_isLocal, opt_isCloud) {
|
|
var variableInMap = (this.getVariable(name, opt_type) != null);
|
|
var newVar = Blockly.WorkspaceSvg.superClass_.createVariable.call(
|
|
this, name, opt_type, opt_id, opt_isLocal, opt_isCloud);
|
|
// For performance reasons, only refresh the the toolbox for new variables.
|
|
// Variables that already exist should already be there.
|
|
if (!variableInMap && (opt_type != Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE)) {
|
|
this.refreshToolboxSelection_();
|
|
}
|
|
return newVar;
|
|
};
|
|
|
|
/**
|
|
* Update cached areas for this workspace.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.recordCachedAreas = function() {
|
|
this.recordBlocksArea_();
|
|
this.recordDeleteAreas_();
|
|
};
|
|
|
|
/**
|
|
* Make a list of all the delete areas for this workspace.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.recordDeleteAreas_ = function() {
|
|
if (this.trashcan) {
|
|
this.deleteAreaTrash_ = this.trashcan.getClientRect();
|
|
} else {
|
|
this.deleteAreaTrash_ = null;
|
|
}
|
|
if (this.flyout_) {
|
|
this.deleteAreaToolbox_ = this.flyout_.getClientRect();
|
|
} else if (this.toolbox_) {
|
|
this.deleteAreaToolbox_ = this.toolbox_.getClientRect();
|
|
} else {
|
|
this.deleteAreaToolbox_ = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Record where all of blocks GUI is on the screen
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.recordBlocksArea_ = function() {
|
|
var parentSvg = this.getParentSvg();
|
|
if (parentSvg) {
|
|
var bounds = parentSvg.getBoundingClientRect();
|
|
this.blocksArea_ = new goog.math.Rect(bounds.left, bounds.top, bounds.width, bounds.height);
|
|
} else {
|
|
this.blocksArea_ = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Is the mouse event over a delete area (toolbox or non-closing flyout)?
|
|
* @param {!Event} e Mouse move event.
|
|
* @return {?number} Null if not over a delete area, or an enum representing
|
|
* which delete area the event is over.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
|
|
var xy = new goog.math.Coordinate(e.clientX, e.clientY);
|
|
if (this.deleteAreaTrash_ && this.deleteAreaTrash_.contains(xy)) {
|
|
return Blockly.DELETE_AREA_TRASH;
|
|
}
|
|
if (this.deleteAreaToolbox_ && this.deleteAreaToolbox_.contains(xy)) {
|
|
return Blockly.DELETE_AREA_TOOLBOX;
|
|
}
|
|
return Blockly.DELETE_AREA_NONE;
|
|
};
|
|
|
|
/**
|
|
* Is the mouse event inside the blocks UI?
|
|
* @param {!Event} e Mouse move event.
|
|
* @return {boolean} True if event is within the bounds of the blocks UI or delete area
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isInsideBlocksArea = function(e) {
|
|
var xy = new goog.math.Coordinate(e.clientX, e.clientY);
|
|
if (this.isDeleteArea(e) || (this.blocksArea_ && this.blocksArea_.contains(xy))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Handle a mouse-down on SVG drawing surface.
|
|
* @param {!Event} e Mouse down event.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
|
|
var gesture = this.getGesture(e);
|
|
if (gesture) {
|
|
gesture.handleWsStart(e, this);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Start tracking a drag of an object on this workspace.
|
|
* @param {!Event} e Mouse down event.
|
|
* @param {!goog.math.Coordinate} xy Starting location of object.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) {
|
|
// Record the starting offset between the bubble's location and the mouse.
|
|
var point = Blockly.utils.mouseToSvg(e, this.getParentSvg(),
|
|
this.getInverseScreenCTM());
|
|
// Fix scale of mouse event.
|
|
point.x /= this.scale;
|
|
point.y /= this.scale;
|
|
this.dragDeltaXY_ = goog.math.Coordinate.difference(xy, point);
|
|
};
|
|
|
|
/**
|
|
* Track a drag of an object on this workspace.
|
|
* @param {!Event} e Mouse move event.
|
|
* @return {!goog.math.Coordinate} New location of object.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.moveDrag = function(e) {
|
|
var point = Blockly.utils.mouseToSvg(e, this.getParentSvg(),
|
|
this.getInverseScreenCTM());
|
|
// Fix scale of mouse event.
|
|
point.x /= this.scale;
|
|
point.y /= this.scale;
|
|
return goog.math.Coordinate.sum(this.dragDeltaXY_, point);
|
|
};
|
|
|
|
/**
|
|
* Is the user currently dragging a block or scrolling the flyout/workspace?
|
|
* @return {boolean} True if currently dragging or scrolling.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isDragging = function() {
|
|
return this.currentGesture_ && this.currentGesture_.isDragging();
|
|
};
|
|
|
|
/**
|
|
* Is this workspace draggable and scrollable?
|
|
* @return {boolean} True if this workspace may be dragged.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.isDraggable = function() {
|
|
return !!this.scrollbar;
|
|
};
|
|
|
|
/**
|
|
* Handle a mouse-wheel on SVG drawing surface.
|
|
* @param {!Event} e Mouse wheel event.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
|
|
// TODO: Remove gesture cancellation and compensate for coordinate skew during
|
|
// zoom.
|
|
if (this.currentGesture_) {
|
|
this.currentGesture_.cancel();
|
|
}
|
|
|
|
// Multiplier variable, so that non-pixel-deltaModes are supported.
|
|
// See LLK/scratch-blocks#1190.
|
|
var multiplier = e.deltaMode === 0x1 ? Blockly.LINE_SCROLL_MULTIPLIER : 1;
|
|
|
|
if (e.ctrlKey) {
|
|
// The vertical scroll distance that corresponds to a click of a zoom button.
|
|
var PIXELS_PER_ZOOM_STEP = 50;
|
|
var delta = -e.deltaY / PIXELS_PER_ZOOM_STEP * multiplier;
|
|
var position = Blockly.utils.mouseToSvg(e, this.getParentSvg(),
|
|
this.getInverseScreenCTM());
|
|
this.zoom(position.x, position.y, delta);
|
|
} else {
|
|
// This is a regular mouse wheel event - scroll the workspace
|
|
// First hide the WidgetDiv without animation
|
|
// (mouse scroll makes field out of place with div)
|
|
Blockly.WidgetDiv.hide(true);
|
|
Blockly.DropDownDiv.hideWithoutAnimation();
|
|
|
|
var x = this.scrollX - e.deltaX * multiplier;
|
|
var y = this.scrollY - e.deltaY * multiplier;
|
|
|
|
if (e.shiftKey && e.deltaX === 0) {
|
|
// Scroll horizontally (based on vertical scroll delta)
|
|
// This is needed as for some browser/system combinations which do not
|
|
// set deltaX. See #1662.
|
|
x = this.scrollX - e.deltaY * multiplier;
|
|
y = this.scrollY; // Don't scroll vertically
|
|
}
|
|
|
|
this.startDragMetrics = this.getMetrics();
|
|
this.scroll(x, y);
|
|
}
|
|
e.preventDefault();
|
|
};
|
|
|
|
/**
|
|
* Calculate the bounding box for the blocks on the workspace.
|
|
* Coordinate system: workspace coordinates.
|
|
*
|
|
* @return {Object} Contains the position and size of the bounding box
|
|
* containing the blocks on the workspace.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox = function() {
|
|
var topBlocks = this.getTopBlocks(false);
|
|
var topComments = this.getTopComments(false);
|
|
var topElements = topBlocks.concat(topComments);
|
|
// There are no blocks, return empty rectangle.
|
|
if (!topElements.length) {
|
|
return {x: 0, y: 0, width: 0, height: 0};
|
|
}
|
|
|
|
// Initialize boundary using the first block.
|
|
var boundary = topElements[0].getBoundingRectangle();
|
|
|
|
// Start at 1 since the 0th block was used for initialization
|
|
for (var i = 1; i < topElements.length; i++) {
|
|
var blockBoundary = topElements[i].getBoundingRectangle();
|
|
if (blockBoundary.topLeft.x < boundary.topLeft.x) {
|
|
boundary.topLeft.x = blockBoundary.topLeft.x;
|
|
}
|
|
if (blockBoundary.bottomRight.x > boundary.bottomRight.x) {
|
|
boundary.bottomRight.x = blockBoundary.bottomRight.x;
|
|
}
|
|
if (blockBoundary.topLeft.y < boundary.topLeft.y) {
|
|
boundary.topLeft.y = blockBoundary.topLeft.y;
|
|
}
|
|
if (blockBoundary.bottomRight.y > boundary.bottomRight.y) {
|
|
boundary.bottomRight.y = blockBoundary.bottomRight.y;
|
|
}
|
|
}
|
|
return {
|
|
x: boundary.topLeft.x,
|
|
y: boundary.topLeft.y,
|
|
width: boundary.bottomRight.x - boundary.topLeft.x,
|
|
height: boundary.bottomRight.y - boundary.topLeft.y
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Clean up the workspace by ordering all the blocks in a column.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.cleanUp = function() {
|
|
this.setResizesEnabled(false);
|
|
Blockly.Events.setGroup(true);
|
|
var topBlocks = this.getTopBlocks(true);
|
|
var cursorY = 0;
|
|
for (var i = 0, block; block = topBlocks[i]; i++) {
|
|
var xy = block.getRelativeToSurfaceXY();
|
|
block.moveBy(-xy.x, cursorY - xy.y);
|
|
block.snapToGrid();
|
|
cursorY = block.getRelativeToSurfaceXY().y +
|
|
block.getHeightWidth().height + Blockly.BlockSvg.MIN_BLOCK_Y;
|
|
}
|
|
Blockly.Events.setGroup(false);
|
|
this.setResizesEnabled(true);
|
|
};
|
|
|
|
/**
|
|
* Show the context menu for the workspace.
|
|
* @param {!Event} e Mouse event.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
|
|
if (this.options.readOnly || this.isFlyout) {
|
|
return;
|
|
}
|
|
var menuOptions = [];
|
|
var topBlocks = this.getTopBlocks(true);
|
|
var eventGroup = Blockly.utils.genUid();
|
|
var ws = this;
|
|
|
|
// Options to undo/redo previous action.
|
|
menuOptions.push(Blockly.ContextMenu.wsUndoOption(this));
|
|
menuOptions.push(Blockly.ContextMenu.wsRedoOption(this));
|
|
|
|
// Option to clean up blocks.
|
|
if (this.scrollbar) {
|
|
menuOptions.push(
|
|
Blockly.ContextMenu.wsCleanupOption(this,topBlocks.length));
|
|
}
|
|
|
|
if (this.options.collapse) {
|
|
var hasCollapsedBlocks = false;
|
|
var hasExpandedBlocks = false;
|
|
for (var i = 0; i < topBlocks.length; i++) {
|
|
var block = topBlocks[i];
|
|
while (block) {
|
|
if (block.isCollapsed()) {
|
|
hasCollapsedBlocks = true;
|
|
} else {
|
|
hasExpandedBlocks = true;
|
|
}
|
|
block = block.getNextBlock();
|
|
}
|
|
}
|
|
|
|
menuOptions.push(Blockly.ContextMenu.wsCollapseOption(hasExpandedBlocks,
|
|
topBlocks));
|
|
|
|
menuOptions.push(Blockly.ContextMenu.wsExpandOption(hasCollapsedBlocks,
|
|
topBlocks));
|
|
}
|
|
|
|
// Option to add a workspace comment.
|
|
if (this.options.comments) {
|
|
menuOptions.push(Blockly.ContextMenu.workspaceCommentOption(ws, e));
|
|
}
|
|
|
|
// Option to delete all blocks.
|
|
// Count the number of blocks that are deletable.
|
|
var deleteList = Blockly.WorkspaceSvg.buildDeleteList_(topBlocks);
|
|
// Scratch-specific: don't count shadow blocks in delete count
|
|
var deleteCount = 0;
|
|
for (var i = 0; i < deleteList.length; i++) {
|
|
if (!deleteList[i].isShadow()) {
|
|
deleteCount++;
|
|
}
|
|
}
|
|
|
|
var DELAY = 10;
|
|
function deleteNext() {
|
|
Blockly.Events.setGroup(eventGroup);
|
|
var block = deleteList.shift();
|
|
if (block) {
|
|
if (block.workspace) {
|
|
block.dispose(false, true);
|
|
setTimeout(deleteNext, DELAY);
|
|
} else {
|
|
deleteNext();
|
|
}
|
|
}
|
|
Blockly.Events.setGroup(false);
|
|
}
|
|
|
|
var deleteOption = {
|
|
text: deleteCount == 1 ? Blockly.Msg.DELETE_BLOCK :
|
|
Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(deleteCount)),
|
|
enabled: deleteCount > 0,
|
|
callback: function() {
|
|
if (ws.currentGesture_) {
|
|
ws.currentGesture_.cancel();
|
|
}
|
|
if (deleteCount < 2 ) {
|
|
deleteNext();
|
|
} else {
|
|
Blockly.confirm(
|
|
Blockly.Msg.DELETE_ALL_BLOCKS.replace('%1', String(deleteCount)),
|
|
function(ok) {
|
|
if (ok) {
|
|
deleteNext();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
menuOptions.push(deleteOption);
|
|
|
|
Blockly.ContextMenu.show(e, menuOptions, this.RTL);
|
|
};
|
|
|
|
/**
|
|
* Build a list of all deletable blocks that are reachable from the given
|
|
* list of top blocks.
|
|
* @param {!Array.<!Blockly.BlockSvg>} topBlocks The list of top blocks on the
|
|
* workspace.
|
|
* @return {!Array.<!Blockly.BlockSvg>} A list of deletable blocks on the
|
|
* workspace.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.buildDeleteList_ = function(topBlocks) {
|
|
var deleteList = [];
|
|
function addDeletableBlocks(block) {
|
|
if (block.isDeletable()) {
|
|
deleteList = deleteList.concat(block.getDescendants(false));
|
|
} else {
|
|
var children = block.getChildren();
|
|
for (var i = 0; i < children.length; i++) {
|
|
addDeletableBlocks(children[i]);
|
|
}
|
|
}
|
|
}
|
|
for (var i = 0; i < topBlocks.length; i++) {
|
|
addDeletableBlocks(topBlocks[i]);
|
|
}
|
|
return deleteList;
|
|
};
|
|
|
|
/**
|
|
* Modify the block tree on the existing toolbox.
|
|
* @param {Node|string} tree DOM tree of blocks, or text representation of same.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) {
|
|
tree = Blockly.Options.parseToolboxTree(tree);
|
|
if (!tree) {
|
|
if (this.options.languageTree) {
|
|
throw 'Can\'t nullify an existing toolbox.';
|
|
}
|
|
return; // No change (null to null).
|
|
}
|
|
if (!this.options.languageTree) {
|
|
throw 'Existing toolbox is null. Can\'t create new toolbox.';
|
|
}
|
|
if (tree.getElementsByTagName('category').length) {
|
|
if (!this.toolbox_) {
|
|
throw 'Existing toolbox has no categories. Can\'t change mode.';
|
|
}
|
|
this.options.languageTree = tree;
|
|
this.toolbox_.populate_(tree);
|
|
this.toolbox_.position();
|
|
} else {
|
|
if (!this.flyout_) {
|
|
throw 'Existing toolbox has categories. Can\'t change mode.';
|
|
}
|
|
this.options.languageTree = tree;
|
|
this.flyout_.show(tree.childNodes);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Mark this workspace as the currently focused main workspace.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.markFocused = function() {
|
|
if (this.options.parentWorkspace) {
|
|
this.options.parentWorkspace.markFocused();
|
|
} else {
|
|
Blockly.mainWorkspace = this;
|
|
// We call e.preventDefault in many event handlers which means we
|
|
// need to explicitly grab focus (e.g from a textarea) because
|
|
// the browser will not do it for us. How to do this is browser dependant.
|
|
this.setBrowserFocus();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set the workspace to have focus in the browser.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.setBrowserFocus = function() {
|
|
// Blur whatever was focused since explcitly grabbing focus below does not
|
|
// work in Edge.
|
|
if (document.activeElement) {
|
|
document.activeElement.blur();
|
|
}
|
|
try {
|
|
// Focus the workspace SVG - this is for Chrome and Firefox.
|
|
this.getParentSvg().focus();
|
|
} catch (e) {
|
|
// IE and Edge do not support focus on SVG elements. When that fails
|
|
// above, get the injectionDiv (the workspace's parent) and focus that
|
|
// instead. This doesn't work in Chrome.
|
|
try {
|
|
// In IE11, use setActive (which is IE only) so the page doesn't scroll
|
|
// to the workspace gaining focus.
|
|
this.getParentSvg().parentNode.setActive();
|
|
} catch (e) {
|
|
// setActive support was discontinued in Edge so when that fails, call
|
|
// focus instead.
|
|
this.getParentSvg().parentNode.focus();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Zooming the blocks centered in (x, y) coordinate with zooming in or out.
|
|
* @param {number} x X coordinate of center.
|
|
* @param {number} y Y coordinate of center.
|
|
* @param {number} amount Amount of zooming
|
|
* (negative zooms out and positive zooms in).
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.zoom = function(x, y, amount) {
|
|
var speed = this.options.zoomOptions.scaleSpeed;
|
|
var metrics = this.getMetrics();
|
|
var center = this.getParentSvg().createSVGPoint();
|
|
center.x = x;
|
|
center.y = y;
|
|
center = center.matrixTransform(this.getCanvas().getCTM().inverse());
|
|
x = center.x;
|
|
y = center.y;
|
|
var canvas = this.getCanvas();
|
|
// Scale factor.
|
|
var scaleChange = Math.pow(speed, amount);
|
|
// Clamp scale within valid range.
|
|
var newScale = this.scale * scaleChange;
|
|
if (newScale > this.options.zoomOptions.maxScale) {
|
|
scaleChange = this.options.zoomOptions.maxScale / this.scale;
|
|
} else if (newScale < this.options.zoomOptions.minScale) {
|
|
scaleChange = this.options.zoomOptions.minScale / this.scale;
|
|
}
|
|
if (this.scale == newScale) {
|
|
return; // No change in zoom.
|
|
}
|
|
if (this.scrollbar) {
|
|
var matrix = canvas.getCTM()
|
|
.translate(x * (1 - scaleChange), y * (1 - scaleChange))
|
|
.scale(scaleChange);
|
|
// newScale and matrix.a should be identical (within a rounding error).
|
|
// ScrollX and scrollY are in pixels.
|
|
this.scrollX = matrix.e - metrics.absoluteLeft;
|
|
this.scrollY = matrix.f - metrics.absoluteTop;
|
|
}
|
|
this.setScale(newScale);
|
|
// Hide the WidgetDiv without animation (zoom makes field out of place with div)
|
|
Blockly.WidgetDiv.hide(true);
|
|
Blockly.DropDownDiv.hideWithoutAnimation();
|
|
};
|
|
|
|
/**
|
|
* Zooming the blocks centered in the center of view with zooming in or out.
|
|
* @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) {
|
|
var metrics = this.getMetrics();
|
|
var x = metrics.viewWidth / 2;
|
|
var y = metrics.viewHeight / 2;
|
|
this.zoom(x, y, type);
|
|
};
|
|
|
|
/**
|
|
* Zoom the blocks to fit in the workspace if possible.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
|
|
var metrics = this.getMetrics();
|
|
var blocksBox = this.getBlocksBoundingBox();
|
|
var blocksWidth = blocksBox.width;
|
|
var blocksHeight = blocksBox.height;
|
|
if (!blocksWidth) {
|
|
return; // Prevents zooming to infinity.
|
|
}
|
|
var workspaceWidth = metrics.viewWidth;
|
|
var workspaceHeight = metrics.viewHeight;
|
|
if (this.flyout_) {
|
|
workspaceWidth -= this.flyout_.width_;
|
|
}
|
|
if (!this.scrollbar) {
|
|
// Origin point of 0,0 is fixed, blocks will not scroll to center.
|
|
blocksWidth += metrics.contentLeft;
|
|
blocksHeight += metrics.contentTop;
|
|
}
|
|
var ratioX = workspaceWidth / blocksWidth;
|
|
var ratioY = workspaceHeight / blocksHeight;
|
|
this.setScale(Math.min(ratioX, ratioY));
|
|
this.scrollCenter();
|
|
};
|
|
|
|
/**
|
|
* Center the workspace.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.scrollCenter = function() {
|
|
if (!this.scrollbar) {
|
|
// Can't center a non-scrolling workspace.
|
|
console.warn('Tried to scroll a non-scrollable workspace.');
|
|
return;
|
|
}
|
|
// Hide the WidgetDiv without animation (zoom makes field out of place with div)
|
|
Blockly.WidgetDiv.hide(true);
|
|
Blockly.DropDownDiv.hideWithoutAnimation();
|
|
Blockly.hideChaff(false);
|
|
var metrics = this.getMetrics();
|
|
var x = (metrics.contentWidth - metrics.viewWidth) / 2;
|
|
if (this.flyout_) {
|
|
x -= this.flyout_.width_ / 2;
|
|
}
|
|
var y = (metrics.contentHeight - metrics.viewHeight) / 2;
|
|
this.scrollbar.set(x, y);
|
|
};
|
|
|
|
/**
|
|
* Scroll the workspace to center on the given block.
|
|
* @param {?string} id ID of block center on.
|
|
* @public
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.centerOnBlock = function(id) {
|
|
if (!this.scrollbar) {
|
|
console.warn('Tried to scroll a non-scrollable workspace.');
|
|
return;
|
|
}
|
|
|
|
var block = this.getBlockById(id);
|
|
if (!block) {
|
|
return;
|
|
}
|
|
|
|
// XY is in workspace coordinates.
|
|
var xy = block.getRelativeToSurfaceXY();
|
|
// Height/width is in workspace units.
|
|
var heightWidth = block.getHeightWidth();
|
|
|
|
// Find the enter of the block in workspace units.
|
|
var blockCenterY = xy.y + heightWidth.height / 2;
|
|
|
|
// In RTL the block's position is the top right of the block, not top left.
|
|
var multiplier = this.RTL ? -1 : 1;
|
|
var blockCenterX = xy.x + (multiplier * heightWidth.width / 2);
|
|
|
|
// Workspace scale, used to convert from workspace coordinates to pixels.
|
|
var scale = this.scale;
|
|
|
|
// Center in pixels. 0, 0 is at the workspace origin. These numbers may
|
|
// be negative.
|
|
var pixelX = blockCenterX * scale;
|
|
var pixelY = blockCenterY * scale;
|
|
|
|
var metrics = this.getMetrics();
|
|
|
|
// Scrolling to here would put the block in the top-left corner of the
|
|
// visible workspace.
|
|
var scrollToBlockX = pixelX - metrics.contentLeft;
|
|
var scrollToBlockY = pixelY - metrics.contentTop;
|
|
|
|
// viewHeight and viewWidth are in pixels.
|
|
var halfViewWidth = metrics.viewWidth / 2;
|
|
var halfViewHeight = metrics.viewHeight / 2;
|
|
|
|
// Put the block in the center of the visible workspace instead.
|
|
var scrollToCenterX = scrollToBlockX - halfViewWidth;
|
|
var scrollToCenterY = scrollToBlockY - halfViewHeight;
|
|
|
|
Blockly.hideChaff();
|
|
this.scrollbar.set(scrollToCenterX, scrollToCenterY);
|
|
};
|
|
|
|
/**
|
|
* Set the workspace's zoom factor.
|
|
* @param {number} newScale Zoom factor.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
|
|
if (this.options.zoomOptions.maxScale &&
|
|
newScale > this.options.zoomOptions.maxScale) {
|
|
newScale = this.options.zoomOptions.maxScale;
|
|
} else if (this.options.zoomOptions.minScale &&
|
|
newScale < this.options.zoomOptions.minScale) {
|
|
newScale = this.options.zoomOptions.minScale;
|
|
}
|
|
this.scale = newScale;
|
|
if (this.grid_) {
|
|
this.grid_.update(this.scale);
|
|
}
|
|
if (this.scrollbar) {
|
|
this.scrollbar.resize();
|
|
} else {
|
|
this.translate(this.scrollX, this.scrollY);
|
|
}
|
|
Blockly.hideChaff(false);
|
|
if (this.flyout_) {
|
|
// No toolbox, resize flyout.
|
|
this.flyout_.reflow();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Scroll the workspace by a specified amount, keeping in the bounds.
|
|
* Be sure to set this.startDragMetrics with cached metrics before calling.
|
|
* @param {number} x Target X to scroll to
|
|
* @param {number} y Target Y to scroll to
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.scroll = function(x, y) {
|
|
var metrics = this.startDragMetrics; // Cached values
|
|
x = Math.min(x, -metrics.contentLeft);
|
|
y = Math.min(y, -metrics.contentTop);
|
|
x = Math.max(x, metrics.viewWidth - metrics.contentLeft -
|
|
metrics.contentWidth);
|
|
y = Math.max(y, metrics.viewHeight - metrics.contentTop -
|
|
metrics.contentHeight);
|
|
// When the workspace starts scrolling, hide the WidgetDiv without animation.
|
|
// This is to prevent a dispoal animation from happening in the wrong location.
|
|
Blockly.WidgetDiv.hide(true);
|
|
Blockly.DropDownDiv.hideWithoutAnimation();
|
|
// Move the scrollbars and the page will scroll automatically.
|
|
this.scrollbar.set(-x - metrics.contentLeft, -y - metrics.contentTop);
|
|
};
|
|
|
|
/**
|
|
* Update the workspace's stack glow radius to be proportional to scale.
|
|
* Ensures that stack glows always appear to be a fixed size.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.updateStackGlowScale_ = function() {
|
|
// No such def in the flyout workspace.
|
|
if (this.options.stackGlowBlur) {
|
|
this.options.stackGlowBlur.setAttribute('stdDeviation',
|
|
Blockly.Colours.stackGlowSize / this.scale);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the dimensions of the given workspace component, in pixels.
|
|
* @param {Blockly.Toolbox|Blockly.Flyout} elem The element to get the
|
|
* dimensions of, or null. It should be a toolbox or flyout, and should
|
|
* implement getWidth() and getHeight().
|
|
* @return {!Object} An object containing width and height attributes, which
|
|
* will both be zero if elem did not exist.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.getDimensionsPx_ = function(elem) {
|
|
var width = 0;
|
|
var height = 0;
|
|
if (elem) {
|
|
width = elem.getWidth();
|
|
height = elem.getHeight();
|
|
}
|
|
return {
|
|
width: width,
|
|
height: height
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Get the content dimensions of the given workspace, taking into account
|
|
* whether or not it is scrollable and what size the workspace div is on screen.
|
|
* @param {!Blockly.WorkspaceSvg} ws The workspace to measure.
|
|
* @param {!Object} svgSize An object containing height and width attributes in
|
|
* CSS pixels. Together they specify the size of the visible workspace, not
|
|
* including areas covered up by the toolbox.
|
|
* @return {!Object} The dimensions of the contents of the given workspace, as
|
|
* an object containing at least
|
|
* - height and width in pixels
|
|
* - left and top in pixels relative to the workspace origin.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.getContentDimensions_ = function(ws, svgSize) {
|
|
if (ws.scrollbar) {
|
|
return Blockly.WorkspaceSvg.getContentDimensionsBounded_(ws, svgSize);
|
|
} else {
|
|
return Blockly.WorkspaceSvg.getContentDimensionsExact_(ws);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the bounding box for all workspace contents, in pixels.
|
|
* @param {!Blockly.WorkspaceSvg} ws The workspace to inspect.
|
|
* @return {!Object} The dimensions of the contents of the given workspace, as
|
|
* an object containing
|
|
* - height and width in pixels
|
|
* - left, right, top and bottom in pixels relative to the workspace origin.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.getContentDimensionsExact_ = function(ws) {
|
|
// Block bounding box is in workspace coordinates.
|
|
var blockBox = ws.getBlocksBoundingBox();
|
|
var scale = ws.scale;
|
|
|
|
// Convert to pixels.
|
|
var width = blockBox.width * scale;
|
|
var height = blockBox.height * scale;
|
|
var left = blockBox.x * scale;
|
|
var top = blockBox.y * scale;
|
|
|
|
return {
|
|
left: left,
|
|
top: top,
|
|
right: left + width,
|
|
bottom: top + height,
|
|
width: width,
|
|
height: height
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Calculate the size of a scrollable workspace, which should include room for a
|
|
* half screen border around the workspace contents.
|
|
* @param {!Blockly.WorkspaceSvg} ws The workspace to measure.
|
|
* @param {!Object} svgSize An object containing height and width attributes in
|
|
* CSS pixels. Together they specify the size of the visible workspace, not
|
|
* including areas covered up by the toolbox.
|
|
* @return {!Object} The dimensions of the contents of the given workspace, as
|
|
* an object containing
|
|
* - height and width in pixels
|
|
* - left and top in pixels relative to the workspace origin.
|
|
* @private
|
|
*/
|
|
Blockly.WorkspaceSvg.getContentDimensionsBounded_ = function(ws, svgSize) {
|
|
var content = Blockly.WorkspaceSvg.getContentDimensionsExact_(ws);
|
|
|
|
// View height and width are both in pixels, and are the same as the SVG size.
|
|
var viewWidth = svgSize.width;
|
|
var viewHeight = svgSize.height;
|
|
var halfWidth = viewWidth / 2;
|
|
var halfHeight = viewHeight / 2;
|
|
|
|
// Add a border around the content that is at least half a screenful wide.
|
|
// Ensure border is wide enough that blocks can scroll over entire screen.
|
|
var left = Math.min(content.left - halfWidth, content.right - viewWidth);
|
|
var right = Math.max(content.right + halfWidth, content.left + viewWidth);
|
|
|
|
var top = Math.min(content.top - halfHeight, content.bottom - viewHeight);
|
|
var bottom = Math.max(content.bottom + halfHeight, content.top + viewHeight);
|
|
|
|
var dimensions = {
|
|
left: left,
|
|
top: top,
|
|
height: bottom - top,
|
|
width: right - left
|
|
};
|
|
return dimensions;
|
|
};
|
|
|
|
/**
|
|
* Return an object with all the metrics required to size scrollbars for a
|
|
* top level workspace. The following properties are computed:
|
|
* Coordinate system: pixel coordinates.
|
|
* .viewHeight: Height of the visible rectangle,
|
|
* .viewWidth: Width of the visible rectangle,
|
|
* .contentHeight: Height of the contents,
|
|
* .contentWidth: Width of the content,
|
|
* .viewTop: Offset of top edge of visible rectangle from parent,
|
|
* .viewLeft: Offset of left edge of visible rectangle from parent,
|
|
* .contentTop: Offset of the top-most content from the y=0 coordinate,
|
|
* .contentLeft: Offset of the left-most content from the x=0 coordinate.
|
|
* .absoluteTop: Top-edge of view.
|
|
* .absoluteLeft: Left-edge of view.
|
|
* .toolboxWidth: Width of toolbox, if it exists. Otherwise zero.
|
|
* .toolboxHeight: Height of toolbox, if it exists. Otherwise zero.
|
|
* .flyoutWidth: Width of the flyout if it is always open. Otherwise zero.
|
|
* .flyoutHeight: Height of flyout if it is always open. Otherwise zero.
|
|
* .toolboxPosition: Top, bottom, left or right.
|
|
* @return {!Object} Contains size and position metrics of a top level
|
|
* workspace.
|
|
* @private
|
|
* @this Blockly.WorkspaceSvg
|
|
*/
|
|
Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() {
|
|
|
|
var toolboxDimensions =
|
|
Blockly.WorkspaceSvg.getDimensionsPx_(this.toolbox_);
|
|
var flyoutDimensions =
|
|
Blockly.WorkspaceSvg.getDimensionsPx_(this.flyout_);
|
|
|
|
// Contains height and width in CSS pixels.
|
|
// svgSize is equivalent to the size of the injectionDiv at this point.
|
|
var svgSize = Blockly.svgSize(this.getParentSvg());
|
|
if (this.toolbox_) {
|
|
if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP ||
|
|
this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
|
|
svgSize.height -= toolboxDimensions.height;
|
|
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT ||
|
|
this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
|
|
svgSize.width -= toolboxDimensions.width;
|
|
}
|
|
}
|
|
|
|
// svgSize is now the space taken up by the Blockly workspace, not including
|
|
// the toolbox.
|
|
var contentDimensions =
|
|
Blockly.WorkspaceSvg.getContentDimensions_(this, svgSize);
|
|
|
|
var absoluteLeft = 0;
|
|
if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
|
|
absoluteLeft = toolboxDimensions.width;
|
|
}
|
|
var absoluteTop = 0;
|
|
if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
|
|
absoluteTop = toolboxDimensions.height;
|
|
}
|
|
|
|
var metrics = {
|
|
contentHeight: contentDimensions.height,
|
|
contentWidth: contentDimensions.width,
|
|
contentTop: contentDimensions.top,
|
|
contentLeft: contentDimensions.left,
|
|
|
|
viewHeight: svgSize.height,
|
|
viewWidth: svgSize.width,
|
|
viewTop: -this.scrollY, // Must be in pixels, somehow.
|
|
viewLeft: -this.scrollX, // Must be in pixels, somehow.
|
|
|
|
absoluteTop: absoluteTop,
|
|
absoluteLeft: absoluteLeft,
|
|
|
|
toolboxWidth: toolboxDimensions.width,
|
|
toolboxHeight: toolboxDimensions.height,
|
|
|
|
flyoutWidth: flyoutDimensions.width,
|
|
flyoutHeight: flyoutDimensions.height,
|
|
|
|
toolboxPosition: this.toolboxPosition
|
|
};
|
|
return metrics;
|
|
};
|
|
|
|
/**
|
|
* Sets the X/Y translations of a top level workspace to match the scrollbars.
|
|
* @param {!Object} xyRatio Contains an x and/or y property which is a float
|
|
* between 0 and 1 specifying the degree of scrolling.
|
|
* @private
|
|
* @this Blockly.WorkspaceSvg
|
|
*/
|
|
Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) {
|
|
if (!this.scrollbar) {
|
|
throw 'Attempt to set top level workspace scroll without scrollbars.';
|
|
}
|
|
var metrics = this.getMetrics();
|
|
if (goog.isNumber(xyRatio.x)) {
|
|
this.scrollX = -metrics.contentWidth * xyRatio.x - metrics.contentLeft;
|
|
}
|
|
if (goog.isNumber(xyRatio.y)) {
|
|
this.scrollY = -metrics.contentHeight * xyRatio.y - metrics.contentTop;
|
|
}
|
|
var x = this.scrollX + metrics.absoluteLeft;
|
|
var y = this.scrollY + metrics.absoluteTop;
|
|
this.translate(x, y);
|
|
if (this.grid_) {
|
|
this.grid_.moveTo(x, y);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update whether this workspace has resizes enabled.
|
|
* If enabled, workspace will resize when appropriate.
|
|
* If disabled, workspace will not resize until re-enabled.
|
|
* Use to avoid resizing during a batch operation, for performance.
|
|
* @param {boolean} enabled Whether resizes should be enabled.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.setResizesEnabled = function(enabled) {
|
|
var reenabled = (!this.resizesEnabled_ && enabled);
|
|
this.resizesEnabled_ = enabled;
|
|
if (reenabled) {
|
|
// Newly enabled. Trigger a resize.
|
|
this.resizeContents();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update whether this workspace has toolbox refreshes enabled.
|
|
* If enabled, the toolbox will refresh when appropriate.
|
|
* If disabled, workspace will not refresh until re-enabled.
|
|
* Use to avoid refreshing during a batch operation, for performance.
|
|
* @param {boolean} enabled Whether refreshes should be enabled.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.setToolboxRefreshEnabled = function(enabled) {
|
|
var reenabled = (!this.toolboxRefreshEnabled_ && enabled);
|
|
this.toolboxRefreshEnabled_ = enabled;
|
|
if (reenabled) {
|
|
// Newly enabled. Trigger a refresh.
|
|
this.refreshToolboxSelection_();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Dispose of all blocks in workspace, with an optimization to prevent resizes.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.clear = function() {
|
|
this.setResizesEnabled(false);
|
|
Blockly.WorkspaceSvg.superClass_.clear.call(this);
|
|
this.setResizesEnabled(true);
|
|
};
|
|
|
|
/**
|
|
* Register a callback function associated with a given key, for clicks on
|
|
* buttons and labels in the flyout.
|
|
* For instance, a button specified by the XML
|
|
* <button text="create variable" callbackKey="CREATE_VARIABLE"></button>
|
|
* should be matched by a call to
|
|
* registerButtonCallback("CREATE_VARIABLE", yourCallbackFunction).
|
|
* @param {string} key The name to use to look up this function.
|
|
* @param {function(!Blockly.FlyoutButton)} func The function to call when the
|
|
* given button is clicked.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.registerButtonCallback = function(key, func) {
|
|
goog.asserts.assert(goog.isFunction(func),
|
|
'Button callbacks must be functions.');
|
|
this.flyoutButtonCallbacks_[key] = func;
|
|
};
|
|
|
|
/**
|
|
* Get the callback function associated with a given key, for clicks on buttons
|
|
* and labels in the flyout.
|
|
* @param {string} key The name to use to look up the function.
|
|
* @return {?function(!Blockly.FlyoutButton)} The function corresponding to the
|
|
* given key for this workspace; null if no callback is registered.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getButtonCallback = function(key) {
|
|
var result = this.flyoutButtonCallbacks_[key];
|
|
return result ? result : null;
|
|
};
|
|
|
|
/**
|
|
* Remove a callback for a click on a button in the flyout.
|
|
* @param {string} key The name associated with the callback function.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.removeButtonCallback = function(key) {
|
|
this.flyoutButtonCallbacks_[key] = null;
|
|
};
|
|
|
|
/**
|
|
* Register a callback function associated with a given key, for populating
|
|
* custom toolbox categories in this workspace. See the variable and procedure
|
|
* categories as an example.
|
|
* @param {string} key The name to use to look up this function.
|
|
* @param {function(!Blockly.Workspace):!Array.<!Element>} func The function to
|
|
* call when the given toolbox category is opened.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.registerToolboxCategoryCallback = function(key,
|
|
func) {
|
|
goog.asserts.assert(goog.isFunction(func),
|
|
'Toolbox category callbacks must be functions.');
|
|
this.toolboxCategoryCallbacks_[key] = func;
|
|
};
|
|
|
|
/**
|
|
* Get the callback function associated with a given key, for populating
|
|
* custom toolbox categories in this workspace.
|
|
* @param {string} key The name to use to look up the function.
|
|
* @return {?function(!Blockly.Workspace):!Array.<!Element>} The function
|
|
* corresponding to the given key for this workspace, or null if no function
|
|
* is registered.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getToolboxCategoryCallback = function(key) {
|
|
var result = this.toolboxCategoryCallbacks_[key];
|
|
return result ? result : null;
|
|
};
|
|
|
|
/**
|
|
* Remove a callback for a click on a custom category's name in the toolbox.
|
|
* @param {string} key The name associated with the callback function.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.removeToolboxCategoryCallback = function(key) {
|
|
this.toolboxCategoryCallbacks_[key] = null;
|
|
};
|
|
|
|
/**
|
|
* Look up the gesture that is tracking this touch stream on this workspace.
|
|
* May create a new gesture.
|
|
* @param {!Event} e Mouse event or touch event
|
|
* @return {Blockly.Gesture} The gesture that is tracking this touch stream,
|
|
* or null if no valid gesture exists.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getGesture = function(e) {
|
|
var isStart = (e.type == 'mousedown' || e.type == 'touchstart');
|
|
|
|
var gesture = this.currentGesture_;
|
|
if (gesture) {
|
|
if (isStart && gesture.hasStarted()) {
|
|
// That's funny. We must have missed a mouse up.
|
|
// Cancel it, rather than try to retrieve all of the state we need.
|
|
gesture.cancel();
|
|
return null;
|
|
}
|
|
return gesture;
|
|
}
|
|
|
|
// No gesture existed on this workspace, but this looks like the start of a
|
|
// new gesture.
|
|
if (isStart) {
|
|
this.currentGesture_ = new Blockly.Gesture(e, this);
|
|
return this.currentGesture_;
|
|
}
|
|
// No gesture existed and this event couldn't be the start of a new gesture.
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Clear the reference to the current gesture.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.clearGesture = function() {
|
|
this.currentGesture_ = null;
|
|
};
|
|
|
|
/**
|
|
* Cancel the current gesture, if one exists.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.cancelCurrentGesture = function() {
|
|
if (this.currentGesture_) {
|
|
this.currentGesture_.cancel();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Don't even think about using this function before talking to rachel-fenichel.
|
|
*
|
|
* Force a drag to start without clicking and dragging the block itself. Used
|
|
* to attach duplicated blocks to the mouse pointer.
|
|
* @param {!Object} fakeEvent An object with the properties needed to start a
|
|
* drag, including clientX and clientY.
|
|
* @param {!Blockly.BlockSvg} block The block to start dragging.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.startDragWithFakeEvent = function(fakeEvent,
|
|
block) {
|
|
Blockly.Touch.clearTouchIdentifier();
|
|
Blockly.Touch.checkTouchIdentifier(fakeEvent);
|
|
var gesture = block.workspace.getGesture(fakeEvent);
|
|
gesture.forceStartBlockDrag(fakeEvent, block);
|
|
};
|
|
|
|
/**
|
|
* Get the audio manager for this workspace.
|
|
* @return {Blockly.WorkspaceAudio} The audio manager for this workspace.
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getAudioManager = function() {
|
|
return this.audioManager_;
|
|
};
|
|
|
|
/**
|
|
* Get the grid object for this workspace, or null if there is none.
|
|
* @return {Blockly.Grid} The grid object for this workspace.
|
|
* @package
|
|
*/
|
|
Blockly.WorkspaceSvg.prototype.getGrid = function() {
|
|
return this.grid_;
|
|
};
|
|
|
|
// Export symbols that would otherwise be renamed by Closure compiler.
|
|
Blockly.WorkspaceSvg.prototype['setVisible'] =
|
|
Blockly.WorkspaceSvg.prototype.setVisible;
|