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. + * + * + * /g> + * + */ + 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 ';