mirror of
https://github.com/google/blockly.git
synced 2026-01-09 18:10:08 +01:00
416 lines
12 KiB
JavaScript
416 lines
12 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_ = {};
|
|
|
|
/**
|
|
* 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));
|
|
};
|
|
|
|
// 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;
|