mirror of
https://github.com/google/blockly.git
synced 2026-01-10 02:17:09 +01:00
Improve performance of block dragging. This is a backport of the block drag surface from scratch-blocks. At the beginning of a block drag, blocks get moved to a drag surface which then translates using translate3d to avoid repainting the entire svg on every mouse move. At the end of the drag, the blocks are dropped back in the svg in their new position.
450 lines
13 KiB
JavaScript
450 lines
13 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2011 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 Core JavaScript library for Blockly.
|
|
* @author fraser@google.com (Neil Fraser)
|
|
*/
|
|
'use strict';
|
|
|
|
/**
|
|
* The top level namespace used to access the Blockly library.
|
|
* @namespace Blockly
|
|
*/
|
|
goog.provide('Blockly');
|
|
|
|
goog.require('Blockly.BlockSvg.render');
|
|
goog.require('Blockly.Events');
|
|
goog.require('Blockly.FieldAngle');
|
|
goog.require('Blockly.FieldCheckbox');
|
|
goog.require('Blockly.FieldColour');
|
|
// Date picker commented out since it increases footprint by 60%.
|
|
// Add it only if you need it.
|
|
//goog.require('Blockly.FieldDate');
|
|
goog.require('Blockly.FieldDropdown');
|
|
goog.require('Blockly.FieldImage');
|
|
goog.require('Blockly.FieldTextInput');
|
|
goog.require('Blockly.FieldNumber');
|
|
goog.require('Blockly.FieldVariable');
|
|
goog.require('Blockly.Generator');
|
|
goog.require('Blockly.Msg');
|
|
goog.require('Blockly.Procedures');
|
|
goog.require('Blockly.Toolbox');
|
|
goog.require('Blockly.Touch');
|
|
goog.require('Blockly.WidgetDiv');
|
|
goog.require('Blockly.WorkspaceSvg');
|
|
goog.require('Blockly.constants');
|
|
goog.require('Blockly.inject');
|
|
goog.require('Blockly.utils');
|
|
goog.require('goog.color');
|
|
goog.require('goog.userAgent');
|
|
|
|
|
|
// Turn off debugging when compiled.
|
|
var CLOSURE_DEFINES = {'goog.DEBUG': false};
|
|
|
|
/**
|
|
* The main workspace most recently used.
|
|
* Set by Blockly.WorkspaceSvg.prototype.markFocused
|
|
* @type {Blockly.Workspace}
|
|
*/
|
|
Blockly.mainWorkspace = null;
|
|
|
|
/**
|
|
* Currently selected block.
|
|
* @type {Blockly.Block}
|
|
*/
|
|
Blockly.selected = null;
|
|
|
|
/**
|
|
* Currently highlighted connection (during a drag).
|
|
* @type {Blockly.Connection}
|
|
* @private
|
|
*/
|
|
Blockly.highlightedConnection_ = null;
|
|
|
|
/**
|
|
* Connection on dragged block that matches the highlighted connection.
|
|
* @type {Blockly.Connection}
|
|
* @private
|
|
*/
|
|
Blockly.localConnection_ = null;
|
|
|
|
/**
|
|
* All of the connections on blocks that are currently being dragged.
|
|
* @type {!Array.<!Blockly.Connection>}
|
|
* @private
|
|
*/
|
|
Blockly.draggingConnections_ = [];
|
|
|
|
/**
|
|
* Contents of the local clipboard.
|
|
* @type {Element}
|
|
* @private
|
|
*/
|
|
Blockly.clipboardXml_ = null;
|
|
|
|
/**
|
|
* Source of the local clipboard.
|
|
* @type {Blockly.WorkspaceSvg}
|
|
* @private
|
|
*/
|
|
Blockly.clipboardSource_ = null;
|
|
|
|
/**
|
|
* Is the mouse dragging a block?
|
|
* DRAG_NONE - No drag operation.
|
|
* DRAG_STICKY - Still inside the sticky DRAG_RADIUS.
|
|
* DRAG_FREE - Freely draggable.
|
|
* @private
|
|
*/
|
|
Blockly.dragMode_ = Blockly.DRAG_NONE;
|
|
|
|
/**
|
|
* Map from function names to callbacks, for deciding what to do when a button
|
|
* is clicked.
|
|
* @type {!Object<string, function(!Blockly.FlyoutButton)>}
|
|
*/
|
|
Blockly.flyoutButtonCallbacks_ = {};
|
|
|
|
/**
|
|
* Cached value for whether 3D is supported.
|
|
* @type {!boolean}
|
|
* @private
|
|
*/
|
|
Blockly.cache3dSupported_ = null;
|
|
|
|
/**
|
|
* 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.registerButtonCallback = function(key, func) {
|
|
Blockly.flyoutButtonCallbacks_[key] = func;
|
|
};
|
|
|
|
/**
|
|
* Convert a hue (HSV model) into an RGB hex triplet.
|
|
* @param {number} hue Hue on a colour wheel (0-360).
|
|
* @return {string} RGB code, e.g. '#5ba65b'.
|
|
*/
|
|
Blockly.hueToRgb = function(hue) {
|
|
return goog.color.hsvToHex(hue, Blockly.HSV_SATURATION,
|
|
Blockly.HSV_VALUE * 255);
|
|
};
|
|
|
|
/**
|
|
* Returns the dimensions of the specified SVG image.
|
|
* @param {!Element} svg SVG image.
|
|
* @return {!Object} Contains width and height properties.
|
|
*/
|
|
Blockly.svgSize = function(svg) {
|
|
return {width: svg.cachedWidth_,
|
|
height: svg.cachedHeight_};
|
|
};
|
|
|
|
/**
|
|
* Size the workspace when the contents change. This also updates
|
|
* scrollbars accordingly.
|
|
* @param {!Blockly.WorkspaceSvg} workspace The workspace to resize.
|
|
*/
|
|
Blockly.resizeSvgContents = function(workspace) {
|
|
workspace.resizeContents();
|
|
};
|
|
|
|
/**
|
|
* Size the SVG image to completely fill its container. Call this when the view
|
|
* actually changes sizes (e.g. on a window resize/device orientation change).
|
|
* See Blockly.resizeSvgContents to resize the workspace when the contents
|
|
* change (e.g. when a block is added or removed).
|
|
* Record the height/width of the SVG image.
|
|
* @param {!Blockly.WorkspaceSvg} workspace Any workspace in the SVG.
|
|
*/
|
|
Blockly.svgResize = function(workspace) {
|
|
var mainWorkspace = workspace;
|
|
while (mainWorkspace.options.parentWorkspace) {
|
|
mainWorkspace = mainWorkspace.options.parentWorkspace;
|
|
}
|
|
var svg = mainWorkspace.getParentSvg();
|
|
var div = svg.parentNode;
|
|
if (!div) {
|
|
// Workspace deleted, or something.
|
|
return;
|
|
}
|
|
var width = div.offsetWidth;
|
|
var height = div.offsetHeight;
|
|
if (svg.cachedWidth_ != width) {
|
|
svg.setAttribute('width', width + 'px');
|
|
svg.cachedWidth_ = width;
|
|
}
|
|
if (svg.cachedHeight_ != height) {
|
|
svg.setAttribute('height', height + 'px');
|
|
svg.cachedHeight_ = height;
|
|
}
|
|
mainWorkspace.resize();
|
|
};
|
|
|
|
/**
|
|
* Handle a key-down on SVG drawing surface.
|
|
* @param {!Event} e Key down event.
|
|
* @private
|
|
*/
|
|
Blockly.onKeyDown_ = function(e) {
|
|
if (Blockly.mainWorkspace.options.readOnly || Blockly.isTargetInput_(e)) {
|
|
// No key actions on readonly workspaces.
|
|
// When focused on an HTML text input widget, don't trap any keys.
|
|
return;
|
|
}
|
|
var deleteBlock = false;
|
|
if (e.keyCode == 27) {
|
|
// Pressing esc closes the context menu.
|
|
Blockly.hideChaff();
|
|
} else if (e.keyCode == 8 || e.keyCode == 46) {
|
|
// Delete or backspace.
|
|
// Stop the browser from going back to the previous page.
|
|
// Do this first to prevent an error in the delete code from resulting in
|
|
// data loss.
|
|
e.preventDefault();
|
|
if (Blockly.selected && Blockly.selected.isDeletable()) {
|
|
deleteBlock = true;
|
|
}
|
|
} else if (e.altKey || e.ctrlKey || e.metaKey) {
|
|
if (Blockly.selected &&
|
|
Blockly.selected.isDeletable() && Blockly.selected.isMovable()) {
|
|
if (e.keyCode == 67) {
|
|
// 'c' for copy.
|
|
Blockly.hideChaff();
|
|
Blockly.copy_(Blockly.selected);
|
|
} else if (e.keyCode == 88) {
|
|
// 'x' for cut.
|
|
Blockly.copy_(Blockly.selected);
|
|
deleteBlock = true;
|
|
}
|
|
}
|
|
if (e.keyCode == 86) {
|
|
// 'v' for paste.
|
|
if (Blockly.clipboardXml_) {
|
|
Blockly.Events.setGroup(true);
|
|
Blockly.clipboardSource_.paste(Blockly.clipboardXml_);
|
|
Blockly.Events.setGroup(false);
|
|
}
|
|
} else if (e.keyCode == 90) {
|
|
// 'z' for undo 'Z' is for redo.
|
|
Blockly.hideChaff();
|
|
Blockly.mainWorkspace.undo(e.shiftKey);
|
|
}
|
|
}
|
|
if (deleteBlock) {
|
|
// Common code for delete and cut.
|
|
Blockly.Events.setGroup(true);
|
|
Blockly.hideChaff();
|
|
var heal = Blockly.dragMode_ != Blockly.DRAG_FREE;
|
|
Blockly.selected.dispose(heal, true);
|
|
if (Blockly.highlightedConnection_) {
|
|
Blockly.highlightedConnection_.unhighlight();
|
|
Blockly.highlightedConnection_ = null;
|
|
}
|
|
Blockly.Events.setGroup(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Stop binding to the global mouseup and mousemove events.
|
|
* @private
|
|
*/
|
|
Blockly.terminateDrag_ = function() {
|
|
Blockly.BlockSvg.terminateDrag();
|
|
Blockly.Flyout.terminateDrag_();
|
|
};
|
|
|
|
/**
|
|
* Copy a block onto the local clipboard.
|
|
* @param {!Blockly.Block} block Block to be copied.
|
|
* @private
|
|
*/
|
|
Blockly.copy_ = function(block) {
|
|
var xmlBlock = Blockly.Xml.blockToDom(block);
|
|
if (Blockly.dragMode_ != Blockly.DRAG_FREE) {
|
|
Blockly.Xml.deleteNext(xmlBlock);
|
|
}
|
|
// Encode start position in XML.
|
|
var xy = block.getRelativeToSurfaceXY();
|
|
xmlBlock.setAttribute('x', block.RTL ? -xy.x : xy.x);
|
|
xmlBlock.setAttribute('y', xy.y);
|
|
Blockly.clipboardXml_ = xmlBlock;
|
|
Blockly.clipboardSource_ = block.workspace;
|
|
};
|
|
|
|
/**
|
|
* Duplicate this block and its children.
|
|
* @param {!Blockly.Block} block Block to be copied.
|
|
* @private
|
|
*/
|
|
Blockly.duplicate_ = function(block) {
|
|
// Save the clipboard.
|
|
var clipboardXml = Blockly.clipboardXml_;
|
|
var clipboardSource = Blockly.clipboardSource_;
|
|
|
|
// Create a duplicate via a copy/paste operation.
|
|
Blockly.copy_(block);
|
|
block.workspace.paste(Blockly.clipboardXml_);
|
|
|
|
// Restore the clipboard.
|
|
Blockly.clipboardXml_ = clipboardXml;
|
|
Blockly.clipboardSource_ = clipboardSource;
|
|
};
|
|
|
|
/**
|
|
* Cancel the native context menu, unless the focus is on an HTML input widget.
|
|
* @param {!Event} e Mouse down event.
|
|
* @private
|
|
*/
|
|
Blockly.onContextMenu_ = function(e) {
|
|
if (!Blockly.isTargetInput_(e)) {
|
|
// When focused on an HTML text input widget, don't cancel the context menu.
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Close tooltips, context menus, dropdown selections, etc.
|
|
* @param {boolean=} opt_allowToolbox If true, don't close the toolbox.
|
|
*/
|
|
Blockly.hideChaff = function(opt_allowToolbox) {
|
|
Blockly.Tooltip.hide();
|
|
Blockly.WidgetDiv.hide();
|
|
if (!opt_allowToolbox) {
|
|
var workspace = Blockly.getMainWorkspace();
|
|
if (workspace.toolbox_ &&
|
|
workspace.toolbox_.flyout_ &&
|
|
workspace.toolbox_.flyout_.autoClose) {
|
|
workspace.toolbox_.clearSelection();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* When something in Blockly's workspace changes, call a function.
|
|
* @param {!Function} func Function to call.
|
|
* @return {!Array.<!Array>} Opaque data that can be passed to
|
|
* removeChangeListener.
|
|
* @deprecated April 2015
|
|
*/
|
|
Blockly.addChangeListener = function(func) {
|
|
// Backwards compatability from before there could be multiple workspaces.
|
|
console.warn('Deprecated call to Blockly.addChangeListener, ' +
|
|
'use workspace.addChangeListener instead.');
|
|
return Blockly.getMainWorkspace().addChangeListener(func);
|
|
};
|
|
|
|
/**
|
|
* Returns the main workspace. Returns the last used main workspace (based on
|
|
* focus). Try not to use this function, particularly if there are multiple
|
|
* Blockly instances on a page.
|
|
* @return {!Blockly.Workspace} The main workspace.
|
|
*/
|
|
Blockly.getMainWorkspace = function() {
|
|
return Blockly.mainWorkspace;
|
|
};
|
|
|
|
/**
|
|
* Wrapper to window.alert() that app developers may override to
|
|
* provide alternatives to the modal browser window.
|
|
* @param {string} message The message to display to the user.
|
|
* @param {function()=} opt_callback The callback when the alert is dismissed.
|
|
*/
|
|
Blockly.alert = function(message, opt_callback) {
|
|
window.alert(message);
|
|
if (opt_callback) {
|
|
opt_callback();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Wrapper to window.confirm() that app developers may override to
|
|
* provide alternatives to the modal browser window.
|
|
* @param {string} message The message to display to the user.
|
|
* @param {!function(boolean)} callback The callback for handling user response.
|
|
*/
|
|
Blockly.confirm = function(message, callback) {
|
|
callback(window.confirm(message));
|
|
};
|
|
|
|
/**
|
|
* Wrapper to window.prompt() that app developers may override to provide
|
|
* alternatives to the modal browser window. Built-in browser prompts are
|
|
* often used for better text input experience on mobile device. We strongly
|
|
* recommend testing mobile when overriding this.
|
|
* @param {string} message The message to display to the user.
|
|
* @param {string} defaultValue The value to initialize the prompt with.
|
|
* @param {!function(string)} callback The callback for handling user reponse.
|
|
*/
|
|
Blockly.prompt = function(message, defaultValue, callback) {
|
|
callback(window.prompt(message, defaultValue));
|
|
};
|
|
|
|
/**
|
|
* Helper function for defining a block from JSON. The resulting function has
|
|
* the correct value of jsonDef at the point in code where jsonInit is called.
|
|
* @param {!Object} jsonDef The JSON definition of a block.
|
|
* @return {function} A function that calls jsonInit with the correct value
|
|
* of jsonDef.
|
|
* @private
|
|
*/
|
|
Blockly.jsonInitFactory_ = function(jsonDef) {
|
|
return function() {
|
|
this.jsonInit(jsonDef);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Define blocks from an array of JSON block definitions, as might be generated
|
|
* by the Blockly Developer Tools.
|
|
* @param {!Array.<!Object>} jsonArray An array of JSON block definitions.
|
|
*/
|
|
Blockly.defineBlocksWithJsonArray = function(jsonArray) {
|
|
for (var i = 0, elem; elem = jsonArray[i]; i++) {
|
|
Blockly.Blocks[elem.type] = {
|
|
init: Blockly.jsonInitFactory_(elem)
|
|
};
|
|
}
|
|
};
|
|
|
|
// IE9 does not have a console. Create a stub to stop errors.
|
|
if (!goog.global['console']) {
|
|
goog.global['console'] = {
|
|
'log': function() {},
|
|
'warn': function() {}
|
|
};
|
|
}
|
|
|
|
// Export symbols that would otherwise be renamed by Closure compiler.
|
|
if (!goog.global['Blockly']) {
|
|
goog.global['Blockly'] = {};
|
|
}
|
|
goog.global['Blockly']['getMainWorkspace'] = Blockly.getMainWorkspace;
|
|
goog.global['Blockly']['addChangeListener'] = Blockly.addChangeListener;
|