revert: "refactor: Remove block and workspace drag surfaces (#6758)" (#6888)

This reverts commit 332c0fd2f2.
This commit is contained in:
Maribeth Bottorff
2023-03-09 13:43:12 -08:00
committed by GitHub
parent c0934216f8
commit cdb1215d95
15 changed files with 791 additions and 30 deletions

245
core/block_drag_surface.ts Normal file
View File

@@ -0,0 +1,245 @@
/**
* @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* A class that manages a surface for dragging blocks. When a
* block drag is started, we move the block (and children) to a separate DOM
* element that we move around using translate3d. At the end of the drag, the
* blocks are put back in into the SVG they came from. This helps
* performance by avoiding repainting the entire SVG on every mouse move
* while dragging blocks.
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BlockDragSurfaceSvg');
import {Coordinate} from './utils/coordinate.js';
import * as deprecation from './utils/deprecation.js';
import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js';
import * as svgMath from './utils/svg_math.js';
/**
* Class for a drag surface for the currently dragged block. This is a separate
* SVG that contains only the currently moving block, or nothing.
*
* @alias Blockly.BlockDragSurfaceSvg
*/
export class BlockDragSurfaceSvg {
/**
* The root element of the drag surface.
*/
private svg: SVGElement;
/**
* This is where blocks live while they are being dragged if the drag
* surface is enabled.
*/
private dragGroup: SVGElement;
/**
* Cached value for the scale of the drag surface.
* Used to set/get the correct translation during and after a drag.
*/
private scale = 1;
/**
* Cached value for the translation of the drag surface.
* This translation is in pixel units, because the scale is applied to the
* drag group rather than the top-level SVG.
*/
private surfaceXY = new Coordinate(0, 0);
/**
* Cached value for the translation of the child drag surface in pixel
* units. Since the child drag surface tracks the translation of the
* workspace this is ultimately the translation of the workspace.
*/
private readonly childSurfaceXY = new Coordinate(0, 0);
/** @param container Containing element. */
constructor(private readonly container: Element) {
this.svg = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'class': 'blocklyBlockDragSurface',
},
this.container);
this.dragGroup = dom.createSvgElement(Svg.G, {}, this.svg);
}
/**
* Create the drag surface and inject it into the container.
*
* @deprecated The DOM is automatically created by the constructor.
*/
createDom() {
// No alternative provided, because now the dom is just automatically
// created in the constructor now.
deprecation.warn('BlockDragSurfaceSvg createDom', 'June 2022', 'June 2023');
}
/**
* Set the SVG blocks on the drag surface's group and show the surface.
* Only one block group should be on the drag surface at a time.
*
* @param blocks Block or group of blocks to place on the drag surface.
*/
setBlocksAndShow(blocks: SVGElement) {
if (this.dragGroup.childNodes.length) {
throw Error('Already dragging a block.');
}
// appendChild removes the blocks from the previous parent
this.dragGroup.appendChild(blocks);
this.svg.style.display = 'block';
this.surfaceXY = new Coordinate(0, 0);
}
/**
* Translate and scale the entire drag surface group to the given position, to
* keep in sync with the workspace.
*
* @param x X translation in pixel coordinates.
* @param y Y translation in pixel coordinates.
* @param scale Scale of the group.
*/
translateAndScaleGroup(x: number, y: number, scale: number) {
this.scale = scale;
// Make sure the svg exists on a pixel boundary so that it is not fuzzy.
const roundX = Math.round(x);
const roundY = Math.round(y);
this.childSurfaceXY.x = roundX;
this.childSurfaceXY.y = roundY;
this.dragGroup.setAttribute(
'transform',
'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')');
}
/**
* Translate the drag surface's SVG based on its internal state.
*
* @internal
*/
translateSurfaceInternal_() {
// Make sure the svg exists on a pixel boundary so that it is not fuzzy.
const x = Math.round(this.surfaceXY.x);
const y = Math.round(this.surfaceXY.y);
this.svg.style.display = 'block';
dom.setCssTransform(this.svg, 'translate3d(' + x + 'px, ' + y + 'px, 0)');
}
/**
* Translates the entire surface by a relative offset.
*
* @param deltaX Horizontal offset in pixel units.
* @param deltaY Vertical offset in pixel units.
*/
translateBy(deltaX: number, deltaY: number) {
const x = this.surfaceXY.x + deltaX;
const y = this.surfaceXY.y + deltaY;
this.surfaceXY = new Coordinate(x, y);
this.translateSurfaceInternal_();
}
/**
* 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 x X translation for the entire surface.
* @param y Y translation for the entire surface.
*/
translateSurface(x: number, y: number) {
this.surfaceXY = new Coordinate(x * this.scale, y * this.scale);
this.translateSurfaceInternal_();
}
/**
* Reports the surface translation in scaled workspace coordinates.
* Use this when finishing a drag to return blocks to the correct position.
*
* @returns Current translation of the surface.
*/
getSurfaceTranslation(): Coordinate {
const xy = svgMath.getRelativeXY(this.svg);
return new Coordinate(xy.x / this.scale, xy.y / this.scale);
}
/**
* Provide a reference to the drag group (primarily for
* BlockSvg.getRelativeToSurfaceXY).
*
* @returns Drag surface group element.
*/
getGroup(): SVGElement {
return this.dragGroup;
}
/**
* Returns the SVG drag surface.
*
* @returns The SVG drag surface.
*/
getSvgRoot(): SVGElement {
return this.svg;
}
/**
* Get the current blocks on the drag surface, if any (primarily
* for BlockSvg.getRelativeToSurfaceXY).
*
* @returns Drag surface block DOM element, or null if no blocks exist.
*/
getCurrentBlock(): Element|null {
return this.dragGroup.firstChild as Element;
}
/**
* Gets the translation of the child block surface
* This surface is in charge of keeping track of how much the workspace has
* moved.
*
* @returns The amount the workspace has been moved.
*/
getWsTranslation(): Coordinate {
// Returning a copy so the coordinate can not be changed outside this class.
return this.childSurfaceXY.clone();
}
/**
* Clear the group and hide the surface; move the blocks off onto the provided
* element.
* If the block is being deleted it doesn't need to go back to the original
* surface, since it would be removed immediately during dispose.
*
* @param opt_newSurface Surface the dragging blocks should be moved to, or
* null if the blocks should be removed from this surface without being
* moved to a different surface.
*/
clearAndHide(opt_newSurface?: Element) {
const currentBlockElement = this.getCurrentBlock();
if (currentBlockElement) {
if (opt_newSurface) {
// appendChild removes the node from this.dragGroup
opt_newSurface.appendChild(currentBlockElement);
} else {
this.dragGroup.removeChild(currentBlockElement);
}
}
this.svg.style.display = 'none';
if (this.dragGroup.childNodes.length) {
throw Error('Drag group was not cleared.');
}
this.surfaceXY = new Coordinate(0, 0);
}
}

View File

@@ -92,7 +92,7 @@ export class BlockDragger implements IBlockDragger {
}
/**
* Start dragging a block.
* Start dragging a block. This includes moving it to the drag surface.
*
* @param currentDragDeltaXY How far the pointer has moved from the position
* at mouse down, in pixel units.
@@ -122,6 +122,10 @@ export class BlockDragger implements IBlockDragger {
this.disconnectBlock_(healStack, currentDragDeltaXY);
}
this.draggingBlock_.setDragging(true);
// For future consideration: we may be able to put moveToDragSurface inside
// the block dragger, which would also let the block not track the block
// drag surface.
this.draggingBlock_.moveToDragSurface();
}
/**
@@ -215,11 +219,16 @@ export class BlockDragger implements IBlockDragger {
const preventMove = !!this.dragTarget_ &&
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
let newLoc: Coordinate;
let delta: Coordinate|null = null;
if (!preventMove) {
if (preventMove) {
newLoc = this.startXY_;
} else {
const newValues = this.getNewLocationAfterDrag_(currentDragDeltaXY);
delta = newValues.delta;
newLoc = newValues.newLocation;
}
this.draggingBlock_.moveOffDragSurface(newLoc);
if (this.dragTarget_) {
this.dragTarget_.onDrop(this.draggingBlock_);
@@ -427,8 +436,6 @@ function initIconData(block: BlockSvg): IconPositionData[] {
for (let i = 0, descendant; descendant = descendants[i]; i++) {
const icons = descendant.getIcons();
for (let j = 0; j < icons.length; j++) {
// Only bother to track icons whose bubble is visible.
if (!icons[j].isVisible()) continue;
const data = {
// Coordinate with x and y properties (workspace
// coordinates).

View File

@@ -140,6 +140,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
override nextConnection!: RenderedConnection;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
override previousConnection!: RenderedConnection;
private readonly useDragSurface_: boolean;
private translation = '';
@@ -178,6 +179,12 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
this.pathObject =
workspace.getRenderer().makePathObject(this.svgGroup_, this.style);
/**
* Whether to move the block to the drag surface when it is dragged.
* True if it should move, false if it should be translated directly.
*/
this.useDragSurface_ = !!workspace.getBlockDragSurface();
const svgPath = this.pathObject.svgPath;
(svgPath as any).tooltip = this;
Tooltip.bindMouseEvents(svgPath);
@@ -351,6 +358,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
let x = 0;
let y = 0;
const dragSurfaceGroup = this.useDragSurface_ ?
this.workspace.getBlockDragSurface()!.getGroup() :
null;
let element: SVGElement = this.getSvgRoot();
if (element) {
do {
@@ -358,8 +369,19 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
const xy = svgMath.getRelativeXY(element);
x += xy.x;
y += xy.y;
// If this element is the current element on the drag surface, include
// the translation of the drag surface itself.
if (this.useDragSurface_ &&
this.workspace.getBlockDragSurface()!.getCurrentBlock() ===
element) {
const surfaceTranslation =
this.workspace.getBlockDragSurface()!.getSurfaceTranslation();
x += surfaceTranslation.x;
y += surfaceTranslation.y;
}
element = element.parentNode as SVGElement;
} while (element && element !== this.workspace.getCanvas());
} while (element && element !== this.workspace.getCanvas() &&
element !== dragSurfaceGroup);
}
return new Coordinate(x, y);
}
@@ -411,6 +433,31 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
return this.translation;
}
/**
* Move this block to its workspace's drag surface, accounting for
* positioning. Generally should be called at the same time as
* setDragging_(true). Does nothing if useDragSurface_ is false.
*
* @internal
*/
moveToDragSurface() {
if (!this.useDragSurface_) {
return;
}
// The translation for drag surface blocks,
// is equal to the current relative-to-surface position,
// to keep the position in sync as it move on/off the surface.
// This is in workspace coordinates.
const xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.workspace.getBlockDragSurface()!.translateSurface(xy.x, xy.y);
// Execute the move on the top-level SVG component
const svg = this.getSvgRoot();
if (svg) {
this.workspace.getBlockDragSurface()!.setBlocksAndShow(svg);
}
}
/**
* Move a block to a position.
*
@@ -422,15 +469,40 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg,
}
/**
* Move this block during a drag.
* Move this block back to the workspace block canvas.
* Generally should be called at the same time as setDragging_(false).
* Does nothing if useDragSurface_ is false.
*
* @param newXY The position the block should take on on the workspace canvas,
* in workspace coordinates.
* @internal
*/
moveOffDragSurface(newXY: Coordinate) {
if (!this.useDragSurface_) {
return;
}
// Translate to current position, turning off 3d.
this.translate(newXY.x, newXY.y);
this.workspace.getBlockDragSurface()!.clearAndHide(
this.workspace.getCanvas());
}
/**
* Move this block during a drag, taking into account whether we are using a
* drag surface to translate blocks.
* This block must be a top-level block.
*
* @param newLoc The location to translate to, in workspace coordinates.
* @internal
*/
moveDuringDrag(newLoc: Coordinate) {
this.translate(newLoc.x, newLoc.y);
this.getSvgRoot().setAttribute('transform', this.getTranslation());
if (this.useDragSurface_) {
this.workspace.getBlockDragSurface()!.translateSurface(
newLoc.x, newLoc.y);
} else {
this.translate(newLoc.x, newLoc.y);
this.getSvgRoot().setAttribute('transform', this.getTranslation());
}
}
/**

View File

@@ -25,6 +25,7 @@ import './events/events_var_create.js';
import {Block} from './block.js';
import * as blockAnimations from './block_animations.js';
import {BlockDragSurfaceSvg} from './block_drag_surface.js';
import {BlockDragger} from './block_dragger.js';
import {BlockSvg} from './block_svg.js';
import {BlocklyOptions} from './blockly_options.js';
@@ -161,6 +162,7 @@ import {Workspace} from './workspace.js';
import {WorkspaceAudio} from './workspace_audio.js';
import {WorkspaceComment} from './workspace_comment.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js';
import {WorkspaceDragger} from './workspace_dragger.js';
import {resizeSvgContents as realResizeSvgContents, WorkspaceSvg} from './workspace_svg.js';
import * as Xml from './xml.js';
@@ -591,6 +593,7 @@ export {BasicCursor};
export {Block};
export {BlocklyOptions};
export {BlockDragger};
export {BlockDragSurfaceSvg};
export {BlockSvg};
export {Blocks};
export {Bubble};
@@ -732,6 +735,7 @@ export {Workspace};
export {WorkspaceAudio};
export {WorkspaceComment};
export {WorkspaceCommentSvg};
export {WorkspaceDragSurfaceSvg};
export {WorkspaceDragger};
export {WorkspaceSvg};
export {ZoomControls};

View File

@@ -12,6 +12,7 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Bubble');
import type {BlockDragSurfaceSvg} from './block_drag_surface.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import type {IBubble} from './interfaces/i_bubble.js';
@@ -782,13 +783,20 @@ export class Bubble implements IBubble {
}
/**
* Move this bubble during a drag.
* Move this bubble during a drag, taking into account whether or not there is
* a drag surface.
*
* @param dragSurface The surface that carries rendered items during a drag,
* or null if no drag surface is in use.
* @param newLoc The location to translate to, in workspace coordinates.
* @internal
*/
moveDuringDrag(newLoc: Coordinate) {
this.moveTo(newLoc.x, newLoc.y);
moveDuringDrag(dragSurface: BlockDragSurfaceSvg, newLoc: Coordinate) {
if (dragSurface) {
dragSurface.translateSurface(newLoc.x, newLoc.y);
} else {
this.moveTo(newLoc.x, newLoc.y);
}
if (this.workspace_.RTL) {
this.relativeLeft = this.anchorXY.x - newLoc.x - this.width;
} else {

View File

@@ -12,6 +12,7 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BubbleDragger');
import type {BlockDragSurfaceSvg} from './block_drag_surface.js';
import {ComponentManager} from './component_manager.js';
import type {CommentMove} from './events/events_comment_move.js';
import * as eventUtils from './events/utils.js';
@@ -35,6 +36,7 @@ export class BubbleDragger {
/** Whether the bubble would be deleted if dropped immediately. */
private wouldDeleteBubble_ = false;
private readonly startXY_: Coordinate;
private dragSurface_: BlockDragSurfaceSvg|null;
/**
* @param bubble The item on the bubble canvas to drag.
@@ -46,10 +48,16 @@ export class BubbleDragger {
* beginning of the drag, in workspace coordinates.
*/
this.startXY_ = this.bubble.getRelativeToSurfaceXY();
/**
* The drag surface to move bubbles to during a drag, or null if none should
* be used. Block dragging and bubble dragging use the same surface.
*/
this.dragSurface_ = workspace.getBlockDragSurface();
}
/**
* Start dragging a bubble.
* Start dragging a bubble. This includes moving it to the drag surface.
*
* @internal
*/
@@ -60,6 +68,12 @@ export class BubbleDragger {
this.workspace.setResizesEnabled(false);
this.bubble.setAutoLayout(false);
if (this.dragSurface_) {
this.bubble.moveTo(0, 0);
this.dragSurface_.translateSurface(this.startXY_.x, this.startXY_.y);
// Execute the move on the top-level SVG component.
this.dragSurface_.setBlocksAndShow(this.bubble.getSvgRoot());
}
this.bubble.setDragging && this.bubble.setDragging(true);
}
@@ -76,7 +90,7 @@ export class BubbleDragger {
dragBubble(e: PointerEvent, currentDragDeltaXY: Coordinate) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.bubble.moveDuringDrag(newLoc);
this.bubble.moveDuringDrag(this.dragSurface_, newLoc);
const oldDragTarget = this.dragTarget_;
this.dragTarget_ = this.workspace.getDragTarget(e);
@@ -156,6 +170,9 @@ export class BubbleDragger {
this.bubble.dispose();
} else {
// Put everything back onto the bubble canvas.
if (this.dragSurface_) {
this.dragSurface_.clearAndHide(this.workspace.getBubbleCanvas());
}
if (this.bubble.setDragging) {
this.bubble.setDragging(false);
}

View File

@@ -94,6 +94,31 @@ let content = `
-webkit-user-select: none;
}
.blocklyWsDragSurface {
display: none;
position: absolute;
top: 0;
left: 0;
}
/* Added as a separate rule with multiple classes to make it more specific
than a bootstrap rule that selects svg:root. See issue #1275 for context.
*/
.blocklyWsDragSurface.blocklyOverflowVisible {
overflow: visible;
}
.blocklyBlockDragSurface {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: visible !important;
z-index: 50; /* Display below toolbox, but above everything else. */
}
.blocklyBlockCanvas.blocklyCanvasTransitioning,
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
transition: transform .5s;
@@ -229,6 +254,16 @@ let content = `
cursor: -webkit-grabbing;
}
/* Change the cursor on the whole drag surface in case the mouse gets
ahead of block during a drag. This way the cursor is still a closed hand.
*/
.blocklyBlockDragSurface .blocklyDraggable {
/* backup for browsers (e.g. IE11) that don't support grabbing */
cursor: url("<<<PATH>>>/handclosed.cur"), auto;
cursor: grabbing;
cursor: -webkit-grabbing;
}
.blocklyDragging.blocklyDraggingDelete {
cursor: url("<<<PATH>>>/handdelete.cur"), auto;
}
@@ -281,7 +316,8 @@ let content = `
Don't allow users to select text. It gets annoying when trying to
drag a block and selected text moves instead.
*/
.blocklySvg text {
.blocklySvg text,
.blocklyBlockDragSurface text {
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;

View File

@@ -12,6 +12,7 @@
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.inject');
import {BlockDragSurfaceSvg} from './block_drag_surface.js';
import type {BlocklyOptions} from './blockly_options.js';
import * as browserEvents from './browser_events.js';
import * as bumpObjects from './bump_objects.js';
@@ -30,6 +31,7 @@ import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js';
import {WorkspaceSvg} from './workspace_svg.js';
@@ -66,7 +68,14 @@ export function inject(
(container as AnyDuringMigration).appendChild(subContainer);
const svg = createDom(subContainer, options);
const workspace = createMainWorkspace(svg, options);
// Create surfaces for dragging things. These are optimizations
// so that the browser does not repaint during the drag.
const blockDragSurface = new BlockDragSurfaceSvg(subContainer);
const workspaceDragSurface = new WorkspaceDragSurfaceSvg(subContainer);
const workspace =
createMainWorkspace(svg, options, blockDragSurface, workspaceDragSurface);
init(workspace);
@@ -144,11 +153,16 @@ function createDom(container: Element, options: Options): SVGElement {
*
* @param svg SVG element with pattern defined.
* @param options Dictionary of options.
* @param blockDragSurface Drag surface SVG for the blocks.
* @param workspaceDragSurface Drag surface SVG for the workspace.
* @returns Newly created main workspace.
*/
function createMainWorkspace(svg: SVGElement, options: Options): WorkspaceSvg {
function createMainWorkspace(
svg: SVGElement, options: Options, blockDragSurface: BlockDragSurfaceSvg,
workspaceDragSurface: WorkspaceDragSurfaceSvg): WorkspaceSvg {
options.parentWorkspace = null;
const mainWorkspace = new WorkspaceSvg(options);
const mainWorkspace =
new WorkspaceSvg(options, blockDragSurface, workspaceDragSurface);
const wsOptions = mainWorkspace.options;
mainWorkspace.scale = wsOptions.zoomOptions.startScale;
svg.appendChild(mainWorkspace.createDom('blocklyMainBackground'));

View File

@@ -19,7 +19,7 @@ goog.declareModuleId('Blockly.IBlockDragger');
*/
export interface IBlockDragger {
/**
* Start dragging a block.
* Start dragging a block. This includes moving it to the drag surface.
*
* @param currentDragDeltaXY How far the pointer has moved from the position
* at mouse down, in pixel units.

View File

@@ -11,6 +11,7 @@
*/
import * as goog from '../../closure/goog/goog.js';
import type {Coordinate} from '../utils/coordinate.js';
import type {BlockDragSurfaceSvg} from '../block_drag_surface.js';
goog.declareModuleId('Blockly.IBubble');
import type {IContextMenu} from './i_contextmenu.js';
@@ -53,11 +54,15 @@ export interface IBubble extends IDraggable, IContextMenu {
setDragging(dragging: boolean): void;
/**
* Move this bubble during a drag.
* Move this bubble during a drag, taking into account whether or not there is
* a drag surface.
*
* @param dragSurface The surface that carries rendered items during a drag,
* or null if no drag surface is in use.
* @param newLoc The location to translate to, in workspace coordinates.
*/
moveDuringDrag(newLoc: Coordinate): void;
moveDuringDrag(dragSurface: BlockDragSurfaceSvg|null, newLoc: Coordinate):
void;
/**
* Move the bubble to the specified location in workspace coordinates.

View File

@@ -713,6 +713,11 @@ export class Scrollbar {
// Look up the current translation and record it.
this.startDragHandle = this.handlePosition;
// Tell the workspace to setup its drag surface since it is about to move.
// onMouseMoveHandle will call onScroll which actually tells the workspace
// to move.
this.workspace.setupDragSurface();
// Record the current mouse position.
this.startDragMouse = this.horizontal ? e.clientX : e.clientY;
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
@@ -739,6 +744,8 @@ export class Scrollbar {
/** Release the scrollbar handle and reset state accordingly. */
private onMouseUpHandle() {
// Tell the workspace to clean up now that the workspace is done moving.
this.workspace.resetDragSurface();
Touch.clearTouchIdentifier();
this.cleanUp();
}

View File

@@ -15,6 +15,7 @@ goog.declareModuleId('Blockly.WorkspaceCommentSvg');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_selected.js';
import type {BlockDragSurfaceSvg} from './block_drag_surface.js';
import * as browserEvents from './browser_events.js';
import * as common from './common.js';
// import * as ContextMenu from './contextmenu.js';
@@ -90,6 +91,7 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
/** Whether the comment is rendered onscreen and is a part of the DOM. */
private rendered_ = false;
private readonly useDragSurface_: boolean;
/**
* @param workspace The block's workspace.
@@ -115,6 +117,12 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
});
this.svgGroup_.appendChild(this.svgRect_);
/**
* Whether to move the comment to the drag surface when it is dragged.
* True if it should move, false if it should be translated directly.
*/
this.useDragSurface_ = !!workspace.getBlockDragSurface();
this.render();
}
@@ -298,6 +306,10 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
let x = 0;
let y = 0;
const dragSurfaceGroup = this.useDragSurface_ ?
this.workspace.getBlockDragSurface()!.getGroup() :
null;
let element: Node|null = this.getSvgRoot();
if (element) {
do {
@@ -305,9 +317,20 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
const xy = svgMath.getRelativeXY(element as Element);
x += xy.x;
y += xy.y;
// If this element is the current element on the drag surface, include
// the translation of the drag surface itself.
if (this.useDragSurface_ &&
this.workspace.getBlockDragSurface()!.getCurrentBlock() ===
element) {
const surfaceTranslation =
this.workspace.getBlockDragSurface()!.getSurfaceTranslation();
x += surfaceTranslation.x;
y += surfaceTranslation.y;
}
element = element.parentNode;
} while (element && element !== this.workspace.getBubbleCanvas() &&
element !== null);
element !== dragSurfaceGroup);
}
this.xy_ = new Coordinate(x, y);
return this.xy_;
@@ -347,14 +370,43 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements
}
/**
* Move this comment during a drag.
* Move this comment to its workspace's drag surface, accounting for
* positioning. Generally should be called at the same time as
* setDragging(true). Does nothing if useDragSurface_ is false.
*
* @internal
*/
moveToDragSurface() {
if (!this.useDragSurface_) {
return;
}
// The translation for drag surface blocks,
// is equal to the current relative-to-surface position,
// to keep the position in sync as it move on/off the surface.
// This is in workspace coordinates.
const xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.workspace.getBlockDragSurface()!.translateSurface(xy.x, xy.y);
// Execute the move on the top-level SVG component
this.workspace.getBlockDragSurface()!.setBlocksAndShow(this.getSvgRoot());
}
/**
* Move this comment during a drag, taking into account whether we are using a
* drag surface to translate blocks.
*
* @param dragSurface The surface that carries rendered items during a drag,
* or null if no drag surface is in use.
* @param newLoc The location to translate to, in workspace coordinates.
* @internal
*/
moveDuringDrag(newLoc: Coordinate) {
const translation = `translate(${newLoc.x}, ${newLoc.y})`;
this.getSvgRoot().setAttribute('transform', translation);
moveDuringDrag(dragSurface: BlockDragSurfaceSvg, newLoc: Coordinate) {
if (dragSurface) {
dragSurface.translateSurface(newLoc.x, newLoc.y);
} else {
const translation = `translate(${newLoc.x}, ${newLoc.y})`;
this.getSvgRoot().setAttribute('transform', translation);
}
}
/**

View File

@@ -0,0 +1,176 @@
/**
* @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* 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.
*
* @class
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.WorkspaceDragSurfaceSvg');
import type {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js';
import * as svgMath from './utils/svg_math.js';
/**
* 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.
*
* @alias Blockly.WorkspaceDragSurfaceSvg
*/
export class WorkspaceDragSurfaceSvg {
/**
* The SVG drag surface. Set once by WorkspaceDragSurfaceSvg.createDom.
*/
private SVG!: SVGElement;
/**
* The element to insert the block canvas and bubble canvas after when it
* goes back in the DOM at the end of a drag.
*/
private previousSibling: Element|null = null;
/** @param container Containing element. */
constructor(private readonly container: Element) {
this.createDom();
}
/** Create the drag surface and inject it into the container. */
createDom() {
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.
* <svg class="blocklyWsDragSurface" style=transform:translate3d(...)>
* <g class="blocklyBlockCanvas"></g>
* <g class="blocklyBubbleCanvas">/g>
* </svg>
*/
this.SVG = dom.createSvgElement(Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'class': 'blocklyWsDragSurface blocklyOverflowVisible',
});
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 x X translation for the entire surface
* @param y Y translation for the entire surface
* @internal
*/
translateSurface(x: number, y: number) {
// Make sure the svg exists on a pixel boundary so that it is not fuzzy.
const fixedX = Math.round(x);
const fixedY = Math.round(y);
this.SVG.style.display = 'block';
dom.setCssTransform(
this.SVG, 'translate3d(' + fixedX + 'px, ' + fixedY + 'px, 0)');
}
/**
* Reports the surface translation in scaled workspace coordinates.
* Use this when finishing a drag to return blocks to the correct position.
*
* @returns Current translation of the surface
* @internal
*/
getSurfaceTranslation(): Coordinate {
return svgMath.getRelativeXY((this.SVG));
}
/**
* Move the blockCanvas and bubbleCanvas out of the surface SVG and on to
* newSurface.
*
* @param newSurface The element to put the drag surface contents into.
* @internal
*/
clearAndHide(newSurface: SVGElement) {
if (!newSurface) {
throw Error(
'Couldn\'t clear and hide the drag surface: missing new surface.');
}
const blockCanvas = this.SVG.childNodes[0] as Element;
const bubbleCanvas = this.SVG.childNodes[1] as Element;
if (!blockCanvas || !bubbleCanvas ||
!(blockCanvas.classList.contains('blocklyBlockCanvas') ||
!bubbleCanvas.classList.contains('blocklyBubbleCanvas'))) {
throw Error(
'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) {
dom.insertAfter(blockCanvas, this.previousSibling);
} else {
newSurface.insertBefore(blockCanvas, newSurface.firstChild);
}
// Reattach the bubble canvas after the blockCanvas.
dom.insertAfter(bubbleCanvas, blockCanvas);
// Hide the drag surface.
this.SVG.style.display = 'none';
if (this.SVG.childNodes.length) {
throw Error('Drag surface was not cleared.');
}
dom.setCssTransform(this.SVG, '');
this.previousSibling = null;
}
/**
* Set the SVG to have the block canvas and bubble canvas in it and then
* show the surface.
*
* @param blockCanvas The block canvas <g> element from the
* workspace.
* @param bubbleCanvas The <g> element that contains the
bubbles.
* @param previousSibling The element to insert the block canvas and
bubble canvas after when it goes back in the DOM at the end of a
drag.
* @param width The width of the workspace SVG element.
* @param height The height of the workspace SVG element.
* @param scale The scale of the workspace being dragged.
* @internal
*/
setContentsAndShow(
blockCanvas: SVGElement, bubbleCanvas: SVGElement,
previousSibling: Element, width: number, height: number, scale: number) {
if (this.SVG.childNodes.length) {
throw Error('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', String(width));
this.SVG.setAttribute('height', String(height));
this.SVG.appendChild(blockCanvas);
this.SVG.appendChild(bubbleCanvas);
this.SVG.style.display = 'block';
}
}

View File

@@ -20,6 +20,9 @@ import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Class for a workspace dragger. It moves the workspace around when it is
* being dragged by a mouse or touch.
* Note that the workspace itself manages whether or not it has a drag surface
* and how to do translations based on that. This simply passes the right
* commands based on events.
*/
export class WorkspaceDragger {
private readonly horizontalScrollEnabled_: boolean;
@@ -62,6 +65,7 @@ export class WorkspaceDragger {
if (common.getSelected()) {
common.getSelected()!.unselect();
}
this.workspace.setupDragSurface();
}
/**
@@ -74,6 +78,7 @@ export class WorkspaceDragger {
endDrag(currentDragDeltaXY: Coordinate) {
// Make sure everything is up to date.
this.drag(currentDragDeltaXY);
this.workspace.resetDragSurface();
}
/**

View File

@@ -20,6 +20,7 @@ import './events/events_theme_change.js';
import './events/events_viewport.js';
import type {Block} from './block.js';
import type {BlockDragSurfaceSvg} from './block_drag_surface.js';
import type {BlockSvg} from './block_svg.js';
import type {BlocklyOptions} from './blockly_options.js';
import * as browserEvents from './browser_events.js';
@@ -74,6 +75,7 @@ import {Workspace} from './workspace.js';
import {WorkspaceAudio} from './workspace_audio.js';
import {WorkspaceComment} from './workspace_comment.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import type {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js';
import * as Xml from './xml.js';
import {ZoomControls} from './zoom_controls.js';
import {ContextMenuOption} from './contextmenu_registry.js';
@@ -221,6 +223,26 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
*/
currentGesture_: Gesture|null = null;
/** This workspace's surface for dragging blocks, if it exists. */
private readonly blockDragSurface: BlockDragSurfaceSvg|null = null;
/** This workspace's drag surface, if it exists. */
private readonly workspaceDragSurface: WorkspaceDragSurfaceSvg|null = 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.
*/
private readonly useWorkspaceDragSurface;
/**
* 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.
*/
private isDragSurfaceActive = false;
/**
* The first parent div with 'injectionDiv' in the name, or null if not set.
* Access this with getInjectionDiv.
@@ -314,8 +336,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
/**
* @param options Dictionary of options.
* @param opt_blockDragSurface Drag surface for blocks.
* @param opt_wsDragSurface Drag surface for the workspace.
*/
constructor(options: Options) {
constructor(
options: Options, opt_blockDragSurface?: BlockDragSurfaceSvg,
opt_wsDragSurface?: WorkspaceDragSurfaceSvg) {
super(options);
const MetricsManagerClass = registry.getClassFromOptions(
@@ -335,6 +361,16 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
this.connectionDBList = ConnectionDB.init(this.connectionChecker);
if (opt_blockDragSurface) {
this.blockDragSurface = opt_blockDragSurface;
}
if (opt_wsDragSurface) {
this.workspaceDragSurface = opt_wsDragSurface;
}
this.useWorkspaceDragSurface = !!this.workspaceDragSurface;
/**
* Object in charge of loading, storing, and playing audio for a workspace.
*/
@@ -1114,10 +1150,18 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
* the Blockly div.
*/
translate(x: number, y: number) {
const translation = 'translate(' + x + ',' + y + ') ' +
'scale(' + this.scale + ')';
this.svgBlockCanvas_.setAttribute('transform', translation);
this.svgBubbleCanvas_.setAttribute('transform', translation);
if (this.useWorkspaceDragSurface && this.isDragSurfaceActive) {
this.workspaceDragSurface?.translateSurface(x, y);
} else {
const 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);
}
// And update the grid if we're using one.
if (this.grid) {
this.grid.moveTo(x, y);
@@ -1126,6 +1170,75 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
this.maybeFireViewportChangeEvent();
}
/**
* 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.
*
* @internal
*/
resetDragSurface() {
// Don't do anything if we aren't using a drag surface.
if (!this.useWorkspaceDragSurface) {
return;
}
this.isDragSurfaceActive = false;
const trans = this.workspaceDragSurface!.getSurfaceTranslation();
this.workspaceDragSurface!.clearAndHide(this.svgGroup_);
const 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.
*
* @internal
*/
setupDragSurface() {
// 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.
const previousElement = this.svgBlockCanvas_.previousSibling as Element;
const width = parseInt(this.getParentSvg().getAttribute('width') ?? '0');
const height = parseInt(this.getParentSvg().getAttribute('height') ?? '0');
const coord = svgMath.getRelativeXY(this.getCanvas());
this.workspaceDragSurface!.setContentsAndShow(
this.getCanvas(), this.getBubbleCanvas(), previousElement, width,
height, this.scale);
this.workspaceDragSurface!.translateSurface(coord.x, coord.y);
}
/**
* Gets the drag surface blocks are moved to when a drag is started.
*
* @returns This workspace's block drag surface, if one is in use.
* @internal
*/
getBlockDragSurface(): BlockDragSurfaceSvg|null {
return this.blockDragSurface;
}
/**
* Returns the horizontal offset of the workspace.
* Intended for LTR/RTL compatibility in XML.