diff --git a/core/css.js b/core/css.js
index ef7434d94..e6ad5b584 100644
--- a/core/css.js
+++ b/core/css.js
@@ -159,6 +159,14 @@ Blockly.Css.CONTENT = [
'-ms-user-select: none;',
'}',
+ '.blocklyWsDragSurface {',
+ 'display: none;',
+ 'position: absolute;',
+ 'overflow: visible;',
+ 'top: 0;',
+ 'left: 0;',
+ '}',
+
'.blocklyBlockDragSurface {',
'display: none;',
'position: absolute;',
diff --git a/core/inject.js b/core/inject.js
index 1c5b5c880..11b8da18c 100644
--- a/core/inject.js
+++ b/core/inject.js
@@ -30,6 +30,7 @@ goog.require('Blockly.BlockDragSurfaceSvg');
goog.require('Blockly.Css');
goog.require('Blockly.Options');
goog.require('Blockly.WorkspaceSvg');
+goog.require('Blockly.WorkspaceDragSurfaceSvg');
goog.require('goog.dom');
goog.require('goog.ui.Component');
goog.require('goog.userAgent');
@@ -55,9 +56,15 @@ Blockly.inject = function(container, opt_options) {
var subContainer = goog.dom.createDom('div', 'injectionDiv');
container.appendChild(subContainer);
var svg = Blockly.createDom_(subContainer, options);
+
+ // Create surfaces for dragging things. These are optimizations
+ // so that the broowser does not repaint during the drag.
var blockDragSurface = new Blockly.BlockDragSurfaceSvg(subContainer);
blockDragSurface.createDom();
- var workspace = Blockly.createMainWorkspace_(svg, options, blockDragSurface);
+ var workspaceDragSurface = new Blockly.workspaceDragSurfaceSvg(subContainer);
+
+ var workspace = Blockly.createMainWorkspace_(svg, options, blockDragSurface,
+ workspaceDragSurface);
Blockly.init_(workspace);
workspace.markFocused();
Blockly.bindEventWithChecks_(svg, 'focus', workspace, workspace.markFocused);
@@ -186,13 +193,16 @@ Blockly.createDom_ = function(container, options) {
* Create a main workspace and add it to the SVG.
* @param {!Element} svg SVG element with pattern defined.
* @param {!Blockly.Options} options Dictionary of options.
- * @param {!Blockly.BlockDragSurfaceSvg} blockDragSurface Drag surface SVG for the workspace.
+ * @param {!Blockly.BlockDragSurfaceSvg} blockDragSurface Drag surface SVG
+ * for the blocks.
+ * @param {!Blockly.WorkspaceDragSurfaceSvg} workspaceDragSurface Drag surface
+ * SVG for the workspace.
* @return {!Blockly.Workspace} Newly created main workspace.
* @private
*/
-Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface) {
+Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, workspaceDragSurface) {
options.parentWorkspace = null;
- var mainWorkspace = new Blockly.WorkspaceSvg(options, blockDragSurface);
+ var mainWorkspace = new Blockly.WorkspaceSvg(options, blockDragSurface, workspaceDragSurface);
mainWorkspace.scale = options.zoomOptions.startScale;
svg.appendChild(mainWorkspace.createDom('blocklyMainBackground'));
diff --git a/core/touch.js b/core/touch.js
index 5e8390ff5..885d7e7a7 100644
--- a/core/touch.js
+++ b/core/touch.js
@@ -107,6 +107,9 @@ Blockly.onMouseUp_ = function(e) {
return;
}
Blockly.Touch.clearTouchIdentifier();
+
+ // TODO(#781): Check whether this needs to be called for all drag modes.
+ workspace.resetDragSurface();
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
workspace.dragMode_ = Blockly.DRAG_NONE;
// Unbind the touch event if it exists.
diff --git a/core/utils.js b/core/utils.js
index 4ee21b1c2..da72cd0c4 100644
--- a/core/utils.js
+++ b/core/utils.js
@@ -81,6 +81,19 @@ Blockly.utils.removeClass = function(element, className) {
return true;
};
+/**
+ * Checks if an element has the specified CSS class.
+ * Similar to Closure's goog.dom.classes.has, except it handles SVG elements.
+ * @param {!Element} element DOM element to check.
+ * @param {string} className Name of class to check.
+ * @return {boolean} True if class exists, false otherwise.
+ * @private
+ */
+ Blockly.utils.hasClass = function(element, className) {
+ var classes = element.getAttribute('class');
+ return (' ' + classes + ' ').indexOf(' ' + className + ' ') != -1;
+ };
+
/**
* Don't do anything for this event, just halt propagation.
* @param {!Event} e An event.
diff --git a/core/workspace_drag_surface_svg.js b/core/workspace_drag_surface_svg.js
new file mode 100644
index 000000000..c2090d07f
--- /dev/null
+++ b/core/workspace_drag_surface_svg.js
@@ -0,0 +1,191 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 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 An SVG that floats on top of the workspace.
+ * Blocks are moved into this SVG during a drag, improving performance.
+ * The entire SVG is translated using css translation instead of SVG so the
+ * blocks are never repainted during drag improving performance.
+ * @author katelyn@google.com (Katelyn Mann)
+ */
+
+'use strict';
+
+goog.provide('Blockly.WorkspaceDragSurfaceSvg');
+
+goog.require('Blockly.utils');
+
+goog.require('goog.asserts');
+goog.require('goog.math.Coordinate');
+
+
+/**
+ * Blocks are moved into this SVG during a drag, improving performance.
+ * The entire SVG is translated using css transforms instead of SVG so the
+ * blocks are never repainted during drag improving performance.
+ * @param {!Element} container Containing element.
+ * @constructor
+ */
+Blockly.workspaceDragSurfaceSvg = function(container) {
+ this.container_ = container;
+ this.createDom();
+};
+
+/**
+ * The SVG drag surface. Set once by Blockly.workspaceDragSurfaceSvg.createDom.
+ * @type {Element}
+ * @private
+ */
+Blockly.workspaceDragSurfaceSvg.prototype.SVG_ = null;
+
+/**
+ * SVG group inside the drag surface that holds blocks while a drag is in
+ * progress. Blocks are moved here by the workspace at start of a drag and moved
+ * back into the main SVG at the end of a drag.
+ *
+ * @type {Element}
+ * @private
+ */
+Blockly.workspaceDragSurfaceSvg.prototype.dragGroup_ = null;
+
+/**
+ * Containing HTML element; parent of the workspace and the drag surface.
+ * @type {Element}
+ * @private
+ */
+Blockly.workspaceDragSurfaceSvg.prototype.container_ = null;
+
+/**
+ * Create the drag surface and inject it into the container.
+ */
+Blockly.workspaceDragSurfaceSvg.prototype.createDom = function() {
+ if (this.SVG_) {
+ return; // Already created.
+ }
+
+ /**
+ * Dom structure when the workspace is being dragged. If there is no drag in
+ * progress, the SVG is empty and display: none.
+ *
+ */
+ this.SVG_ = Blockly.utils.createSvgElement('svg', {
+ 'xmlns': Blockly.SVG_NS,
+ 'xmlns:html': Blockly.HTML_NS,
+ 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
+ 'version': '1.1',
+ 'class': 'blocklyWsDragSurface'
+ }, null);
+ this.container_.appendChild(this.SVG_);
+};
+
+/**
+ * Translate the entire drag surface during a drag.
+ * We translate the drag surface instead of the blocks inside the surface
+ * so that the browser avoids repainting the SVG.
+ * Because of this, the drag coordinates must be adjusted by scale.
+ * @param {number} x X translation for the entire surface
+ * @param {number} y Y translation for the entire surface
+ * @package
+ */
+Blockly.workspaceDragSurfaceSvg.prototype.translateSurface = function(x, y) {
+ // This is a work-around to prevent a the blocks from rendering
+ // fuzzy while they are being moved on the drag surface.
+ x = x.toFixed(0);
+ y = y.toFixed(0);
+
+ var transform =
+ 'transform: translate3d(' + x + 'px, ' + y + 'px, 0px); display: block;';
+ this.SVG_.setAttribute('style', transform);
+};
+
+/**
+ * Reports the surface translation in scaled workspace coordinates.
+ * Use this when finishing a drag to return blocks to the correct position.
+ * @return {!goog.math.Coordinate} Current translation of the surface
+ * @package
+ */
+Blockly.workspaceDragSurfaceSvg.prototype.getSurfaceTranslation = function() {
+ return Blockly.utils.getRelativeXY(this.SVG_);
+};
+
+/**
+ * Move the blockCanvas and bubbleCanvas out of the surface SVG and on to
+ * newSurface.
+ * @param {!SVGElement} newSurface The element to put the drag surface contents
+ * into.
+ * @package
+ */
+Blockly.workspaceDragSurfaceSvg.prototype.clearAndHide = function(newSurface) {
+ var blockCanvas = this.SVG_.childNodes[0];
+ var bubbleCanvas = this.SVG_.childNodes[1];
+ if (!blockCanvas || !bubbleCanvas ||
+ !Blockly.utils.hasClass(blockCanvas, 'blocklyBlockCanvas') ||
+ !Blockly.utils.hasClass(bubbleCanvas, 'blocklyBubbleCanvas')) {
+ throw 'Couldn\'t clear and hide the drag surface. A node was missing.';
+ }
+
+ // If there is a previous sibling, put the blockCanvas back right afterwards,
+ // otherwise insert it as the first child node in newSurface.
+ if (this.previousSibling_ != null) {
+ Blockly.utils.insertAfter_(blockCanvas, this.previousSibling_);
+ } else {
+ newSurface.insertBefore(blockCanvas, newSurface.firstChild);
+ }
+
+ // Reattach the bubble canvas after the blockCanvas.
+ Blockly.utils.insertAfter_(bubbleCanvas, blockCanvas);
+ // Hide the drag surface.
+ this.SVG_.style.display = 'none';
+ goog.asserts.assert(this.SVG_.childNodes.length == 0,
+ 'Drag surface was not cleared.');
+ this.SVG_.style.transform = '';
+ this.previousSibling_ = null;
+};
+
+/**
+ * Set the SVG to have the block canvas and bubble canvas in it and then
+ * show the surface.
+ * @param {!Element} blockCanvas The block canvas element from the workspace.
+ * @param {!Element} bubbleCanvas The element that contains the bubbles.
+ * @param {?Element} previousSibling The element to insert the block canvas &
+ bubble canvas after when it goes back in the dom at the end of a drag.
+ * @param {number} width The width of the workspace svg element.
+ * @param {number} height The height of the workspace svg element.
+ * @param {number} scale The scale of the workspace being dragged.
+ * @package
+ */
+Blockly.workspaceDragSurfaceSvg.prototype.setContentsAndShow = function(
+ blockCanvas, bubbleCanvas, previousSibling, width, height, scale) {
+ goog.asserts.assert(this.SVG_.childNodes.length == 0,
+ 'Already dragging a block.');
+ this.previousSibling_ = previousSibling;
+ // Make sure the blocks and bubble canvas are scaled appropriately.
+ blockCanvas.setAttribute('transform', 'translate(0, 0) scale(' + scale + ')');
+ bubbleCanvas.setAttribute('transform',
+ 'translate(0, 0) scale(' + scale + ')');
+ this.SVG_.setAttribute('width', width);
+ this.SVG_.setAttribute('height', height);
+ this.SVG_.appendChild(blockCanvas);
+ this.SVG_.appendChild(bubbleCanvas);
+ this.SVG_.style.display = 'block';
+};
diff --git a/core/workspace_svg.js b/core/workspace_svg.js
index efdf5c37d..283e30579 100644
--- a/core/workspace_svg.js
+++ b/core/workspace_svg.js
@@ -35,6 +35,7 @@ goog.require('Blockly.ScrollbarPair');
goog.require('Blockly.Touch');
goog.require('Blockly.Trashcan');
goog.require('Blockly.Workspace');
+goog.require('Blockly.WorkspaceDragSurfaceSvg');
goog.require('Blockly.Xml');
goog.require('Blockly.ZoomControls');
@@ -49,11 +50,13 @@ goog.require('goog.userAgent');
* scrollbars, bubbles, and dragging.
* @param {!Blockly.Options} options Dictionary of options.
* @param {Blockly.BlockDragSurfaceSvg=} opt_blockDragSurface Drag surface for
- * the workspace.
+ * blocks.
+ * @param {Blockly.workspaceDragSurfaceSvg=} opt_wsDragSurface Drag surface for
+ * the workspace.
* @extends {Blockly.Workspace}
* @constructor
*/
-Blockly.WorkspaceSvg = function(options, opt_blockDragSurface) {
+Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface) {
Blockly.WorkspaceSvg.superClass_.constructor.call(this, options);
this.getMetrics =
options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_;
@@ -66,6 +69,13 @@ Blockly.WorkspaceSvg = function(options, opt_blockDragSurface) {
this.blockDragSurface_ = opt_blockDragSurface;
}
+ if (opt_wsDragSurface) {
+ this.workspaceDragSurface_ = opt_wsDragSurface;
+ }
+
+ this.useWorkspaceDragSurface_ =
+ this.workspaceDragSurface_ && Blockly.utils.is3dSupported();
+
/**
* Database of pre-loaded sounds.
* @private
@@ -177,12 +187,27 @@ Blockly.WorkspaceSvg.prototype.trashcan = null;
Blockly.WorkspaceSvg.prototype.scrollbar = null;
/**
- * This workspace's drag surface, if it exists.
+ * 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;
+
/**
* Time that the last sound was played.
* @type {Date}
@@ -581,15 +606,63 @@ Blockly.WorkspaceSvg.prototype.getParentSvg = function() {
* @param {number} y Vertical translation.
*/
Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
- var translation = 'translate(' + x + ',' + y + ') ' +
- 'scale(' + this.scale + ')';
- this.svgBlockCanvas_.setAttribute('transform', translation);
- this.svgBubbleCanvas_.setAttribute('transform', translation);
+ if (this.useWorkspaceDragSurface_ && this.dragMode_ != Blockly.DRAG_NONE) {
+ 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;
+ }
+
+ 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;
+ }
+
+ // 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 = this.getParentSvg().getAttribute("width")
+ var height = this.getParentSvg().getAttribute("height")
+ 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);
+};
+
/**
* Returns the horizontal offset of the workspace.
* Intended for LTR/RTL compatibility in XML.
@@ -833,6 +906,10 @@ Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
if (Blockly.utils.isRightButton(e)) {
// Right-click.
this.showContextMenu_(e);
+ // This is to handle the case where the event is pretending to be a right
+ // click event but it was really a long press. In that case, we want to make
+ // sure any in progress drags are stopped.
+ Blockly.onMouseUp_(e);
// Since this was a click, not a drag, end the gesture immediately.
Blockly.Touch.clearTouchIdentifier();
} else if (this.scrollbar) {
@@ -844,6 +921,7 @@ Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
this.startScrollX = this.scrollX;
this.startScrollY = this.scrollY;
+ this.setupDragSurface();
// If this is a touch event then bind to the mouseup so workspace drag mode
// is turned off and double move events are not performed on a block.
// See comment in inject.js Blockly.init_ as to why mouseup events are
diff --git a/tests/jsunit/utils_test.js b/tests/jsunit/utils_test.js
index a96cec57b..5aaa2cc78 100644
--- a/tests/jsunit/utils_test.js
+++ b/tests/jsunit/utils_test.js
@@ -42,6 +42,16 @@ function test_addClass() {
assertEquals('Adding "three"', 'one two three', p.className);
}
+function test_hasClass() {
+ var p = document.createElement('p');
+ p.className = ' one three two three ';
+ assertTrue('Has "one"', Blockly.utils.hasClass(p, 'one'));
+ assertTrue('Has "two"', Blockly.utils.hasClass(p, 'two'));
+ assertTrue('Has "three"', Blockly.utils.hasClass(p, 'three'));
+ assertFalse('Has no "four"', Blockly.utils.hasClass(p, 'four'));
+ assertFalse('Has no "t"', Blockly.utils.hasClass(p, 't'));
+ }
+
function test_removeClass() {
var p = document.createElement('p');
p.className = ' one three two three ';