Initial commit of 001code-html Scratch frontend project.

Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 15:37:45 +08:00
commit 6e0a1fbcbb
11350 changed files with 965674 additions and 0 deletions

1837
scratch-blocks/core/block.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Methods animating a block on connection and disconnection.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.BlockAnimations');
/**
* Play some UI effects (sound, animation) when disposing of a block.
* @param {!Blockly.BlockSvg} block The block being disposed of.
* @package
*/
Blockly.BlockAnimations.disposeUiEffect = function(block) {
var workspace = block.workspace;
var svgGroup = block.getSvgRoot();
workspace.getAudioManager().play('delete');
var xy = workspace.getSvgXY(svgGroup);
// Deeply clone the current block.
var clone = svgGroup.cloneNode(true);
clone.translateX_ = xy.x;
clone.translateY_ = xy.y;
clone.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')');
workspace.getParentSvg().appendChild(clone);
clone.bBox_ = clone.getBBox();
// Start the animation.
Blockly.BlockAnimations.disposeUiStep_(clone, workspace.RTL, new Date,
workspace.scale);
};
/**
* Animate a cloned block and eventually dispose of it.
* This is a class method, not an instance method since the original block has
* been destroyed and is no longer accessible.
* @param {!Element} clone SVG element to animate and dispose of.
* @param {boolean} rtl True if RTL, false if LTR.
* @param {!Date} start Date of animation's start.
* @param {number} workspaceScale Scale of workspace.
* @private
*/
Blockly.BlockAnimations.disposeUiStep_ = function(clone, rtl, start,
workspaceScale) {
var ms = new Date - start;
var percent = ms / 150;
if (percent > 1) {
goog.dom.removeNode(clone);
} else {
var x = clone.translateX_ +
(rtl ? -1 : 1) * clone.bBox_.width * workspaceScale / 2 * percent;
var y = clone.translateY_ + clone.bBox_.height * workspaceScale * percent;
var scale = (1 - percent) * workspaceScale;
clone.setAttribute('transform', 'translate(' + x + ',' + y + ')' +
' scale(' + scale + ')');
setTimeout(Blockly.BlockAnimations.disposeUiStep_, 10, clone, rtl, start,
workspaceScale);
}
};
/**
* Play some UI effects (sound, ripple) after a connection has been established.
* @param {!Blockly.BlockSvg} block The block being connected.
* @package
*/
Blockly.BlockAnimations.connectionUiEffect = function(block) {
block.workspace.getAudioManager().play('click');
};
/**
* Play some UI effects (sound, animation) when disconnecting a block.
* No-op in scratch-blocks, which has no disconnect animation.
* @param {!Blockly.BlockSvg} _block The block being disconnected.
* @package
*/
Blockly.BlockAnimations.disconnectUiEffect = function(
/* eslint-disable no-unused-vars */ _block
/* eslint-enable no-unused-vars */) {
};
/**
* Stop the disconnect UI animation immediately.
* No-op in scratch-blocks, which has no disconnect animation.
* @package
*/
Blockly.BlockAnimations.disconnectUiStop = function() {
};

View File

@@ -0,0 +1,299 @@
/**
* @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 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.
* @author picklesrus
*/
'use strict';
goog.provide('Blockly.BlockDragSurfaceSvg');
goog.require('Blockly.utils');
goog.require('goog.asserts');
goog.require('goog.math.Coordinate');
/**
* Class for a drag surface for the currently dragged block. This is a separate
* SVG that contains only the currently moving block, or nothing.
* @param {!Element} container Containing element.
* @constructor
*/
Blockly.BlockDragSurfaceSvg = function(container) {
/**
* @type {!Element}
* @private
*/
this.container_ = container;
this.createDom();
};
/**
* The SVG drag surface. Set once by Blockly.BlockDragSurfaceSvg.createDom.
* @type {Element}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.SVG_ = null;
/**
* This is where blocks live while they are being dragged if the drag surface
* is enabled.
* @type {Element}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.dragGroup_ = null;
/**
* Containing HTML element; parent of the workspace and the drag surface.
* @type {Element}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.container_ = null;
/**
* Cached value for the scale of the drag surface.
* Used to set/get the correct translation during and after a drag.
* @type {number}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.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.
* @type {goog.math.Coordinate}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.surfaceXY_ = null;
/**
* ID for the drag shadow filter, set in createDom.
* Belongs in Scratch Blocks but not Blockly.
* @type {string}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.dragShadowFilterId_ = '';
/**
* Standard deviation for gaussian blur on drag shadow, in px.
* Belongs in Scratch Blocks but not Blockly.
* @type {number}
* @const
*/
Blockly.BlockDragSurfaceSvg.SHADOW_STD_DEVIATION = 6;
/**
* Create the drag surface and inject it into the container.
*/
Blockly.BlockDragSurfaceSvg.prototype.createDom = function() {
if (this.SVG_) {
return; // Already created.
}
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': 'blocklyBlockDragSurface'
}, this.container_);
this.dragGroup_ = Blockly.utils.createSvgElement('g', {}, this.SVG_);
// Belongs in Scratch Blocks, but not Blockly.
var defs = Blockly.utils.createSvgElement('defs', {}, this.SVG_);
this.dragShadowFilterId_ = this.createDropShadowDom_(defs);
this.dragGroup_.setAttribute(
'filter', 'url(#' + this.dragShadowFilterId_ + ')');
};
/**
* Scratch-specific: Create the SVG def for the drop shadow.
* @param {Element} defs Defs element to insert the shadow filter definition
* @return {string} ID for the filter element
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.createDropShadowDom_ = function(defs) {
var rnd = String(Math.random()).substring(2);
// Adjust these width/height, x/y properties to stop the shadow from clipping
var dragShadowFilter = Blockly.utils.createSvgElement('filter',
{
'id': 'blocklyDragShadowFilter' + rnd,
'height': '140%',
'width': '140%',
'y': '-20%',
'x': '-20%'
},
defs);
Blockly.utils.createSvgElement('feGaussianBlur',
{
'in': 'SourceAlpha',
'stdDeviation': Blockly.BlockDragSurfaceSvg.SHADOW_STD_DEVIATION
},
dragShadowFilter);
var componentTransfer = Blockly.utils.createSvgElement(
'feComponentTransfer', {'result': 'offsetBlur'}, dragShadowFilter);
// Shadow opacity is specified in the adjustable colour library,
// since the darkness of the shadow largely depends on the workspace colour.
Blockly.utils.createSvgElement('feFuncA',
{
'type': 'linear',
'slope': Blockly.Colours.dragShadowOpacity
},
componentTransfer);
Blockly.utils.createSvgElement('feComposite',
{
'in': 'SourceGraphic',
'in2': 'offsetBlur',
'operator': 'over'
},
dragShadowFilter);
return dragShadowFilter.id;
};
/**
* 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 {!Element} blocks Block or group of blocks to place on the drag
* surface.
*/
Blockly.BlockDragSurfaceSvg.prototype.setBlocksAndShow = function(blocks) {
goog.asserts.assert(
this.dragGroup_.childNodes.length == 0, 'Already dragging a block.');
// appendChild removes the blocks from the previous parent
this.dragGroup_.appendChild(blocks);
this.SVG_.style.display = 'block';
this.surfaceXY_ = new goog.math.Coordinate(0, 0);
// This allows blocks to be dragged outside of the blockly svg space.
// This should be reset to hidden at the end of the block drag.
// Note that this behavior is different from blockly where block disappear
// "under" the blockly area.
var injectionDiv = document.getElementsByClassName('injectionDiv')[0];
injectionDiv.style.overflow = 'visible';
};
/**
* Translate and scale the entire drag surface group to the given position, to
* keep in sync with the workspace.
* @param {number} x X translation in workspace coordinates.
* @param {number} y Y translation in workspace coordinates.
* @param {number} scale Scale of the group.
*/
Blockly.BlockDragSurfaceSvg.prototype.translateAndScaleGroup = function(x, y, scale) {
this.scale_ = scale;
// This is a work-around to prevent a the blocks from rendering
// fuzzy while they are being dragged on the drag surface.
var fixedX = x.toFixed(0);
var fixedY = y.toFixed(0);
this.dragGroup_.setAttribute('transform',
'translate(' + fixedX + ',' + fixedY + ') scale(' + scale + ')');
};
/**
* Translate the drag surface's SVG based on its internal state.
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.translateSurfaceInternal_ = function() {
var x = this.surfaceXY_.x;
var y = this.surfaceXY_.y;
// This is a work-around to prevent a the blocks from rendering
// fuzzy while they are being dragged on the drag surface.
x = x.toFixed(0);
y = y.toFixed(0);
this.SVG_.style.display = 'block';
Blockly.utils.setCssTransform(this.SVG_,
'translate(' + x + 'px, ' + y + 'px)');
};
/**
* 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.
*/
Blockly.BlockDragSurfaceSvg.prototype.translateSurface = function(x, y) {
this.surfaceXY_ = new goog.math.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.
* @return {!goog.math.Coordinate} Current translation of the surface.
*/
Blockly.BlockDragSurfaceSvg.prototype.getSurfaceTranslation = function() {
var xy = Blockly.utils.getRelativeXY(this.SVG_);
return new goog.math.Coordinate(xy.x / this.scale_, xy.y / this.scale_);
};
/**
* Provide a reference to the drag group (primarily for
* BlockSvg.getRelativeToSurfaceXY).
* @return {Element} Drag surface group element.
*/
Blockly.BlockDragSurfaceSvg.prototype.getGroup = function() {
return this.dragGroup_;
};
/**
* Get the current blocks on the drag surface, if any (primarily
* for BlockSvg.getRelativeToSurfaceXY).
* @return {!Element|undefined} Drag surface block DOM element, or undefined
* if no blocks exist.
*/
Blockly.BlockDragSurfaceSvg.prototype.getCurrentBlock = function() {
return this.dragGroup_.firstChild;
};
/**
* 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 {Element=} 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.
*/
Blockly.BlockDragSurfaceSvg.prototype.clearAndHide = function(opt_newSurface) {
if (opt_newSurface) {
// appendChild removes the node from this.dragGroup_
opt_newSurface.appendChild(this.getCurrentBlock());
} else {
this.dragGroup_.removeChild(this.getCurrentBlock());
}
this.SVG_.style.display = 'none';
goog.asserts.assert(
this.dragGroup_.childNodes.length == 0, 'Drag group was not cleared.');
this.surfaceXY_ = null;
// Reset the overflow property back to hidden so that nothing appears outside
// of the blockly area.
// Note that this behavior is different from blockly. See note in
// setBlocksAndShow.
var injectionDiv = document.getElementsByClassName('injectionDiv')[0];
injectionDiv.style.overflow = 'hidden';
};

View File

@@ -0,0 +1,424 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Methods for dragging a block visually.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.BlockDragger');
goog.require('Blockly.BlockAnimations');
goog.require('Blockly.Events.BlockMove');
goog.require('Blockly.Events.DragBlockOutside');
goog.require('Blockly.Events.EndBlockDrag');
goog.require('Blockly.InsertionMarkerManager');
goog.require('goog.math.Coordinate');
goog.require('goog.asserts');
/**
* Class for a block dragger. It moves blocks around the workspace when they
* are being dragged by a mouse or touch.
* @param {!Blockly.BlockSvg} block The block to drag.
* @param {!Blockly.WorkspaceSvg} workspace The workspace to drag on.
* @constructor
*/
Blockly.BlockDragger = function(block, workspace) {
/**
* The top block in the stack that is being dragged.
* @type {!Blockly.BlockSvg}
* @private
*/
this.draggingBlock_ = block;
/**
* The workspace on which the block is being dragged.
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* Object that keeps track of connections on dragged blocks.
* @type {!Blockly.InsertionMarkerManager}
* @private
*/
this.draggedConnectionManager_ = new Blockly.InsertionMarkerManager(
this.draggingBlock_);
/**
* Which delete area the mouse pointer is over, if any.
* One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @type {?number}
* @private
*/
this.deleteArea_ = null;
/**
* Whether the block would be deleted if dropped immediately.
* @type {boolean}
* @private
*/
this.wouldDeleteBlock_ = false;
/**
* Whether the currently dragged block is outside of the workspace. Keep
* track so that we can fire events only when this changes.
* @type {boolean}
* @private
*/
this.wasOutside_ = false;
/**
* The location of the top left corner of the dragging block at the beginning
* of the drag in workspace coordinates.
* @type {!goog.math.Coordinate}
* @private
*/
this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY();
/**
* A list of all of the icons (comment, warning, and mutator) that are
* on this block and its descendants. Moving an icon moves the bubble that
* extends from it if that bubble is open.
* @type {Array.<!Object>}
* @private
*/
this.dragIconData_ = Blockly.BlockDragger.initIconData_(block);
};
/**
* Sever all links from this object.
* @package
*/
Blockly.BlockDragger.prototype.dispose = function() {
this.draggingBlock_ = null;
this.workspace_ = null;
this.startWorkspace_ = null;
this.dragIconData_.length = 0;
if (this.draggedConnectionManager_) {
this.draggedConnectionManager_.dispose();
this.draggedConnectionManager_ = null;
}
};
/**
* Make a list of all of the icons (comment, warning, and mutator) that are
* on this block and its descendants. Moving an icon moves the bubble that
* extends from it if that bubble is open.
* @param {!Blockly.BlockSvg} block The root block that is being dragged.
* @return {!Array.<!Object>} The list of all icons and their locations.
* @private
*/
Blockly.BlockDragger.initIconData_ = function(block) {
// Build a list of icons that need to be moved and where they started.
var dragIconData = [];
var descendants = block.getDescendants(false);
for (var i = 0, descendant; descendant = descendants[i]; i++) {
var icons = descendant.getIcons();
for (var j = 0; j < icons.length; j++) {
var data = {
// goog.math.Coordinate with x and y properties (workspace coordinates).
location: icons[j].getIconLocation(),
// Blockly.Icon
icon: icons[j]
};
dragIconData.push(data);
}
}
return dragIconData;
};
/**
* Start dragging a block. This includes moving it to the drag surface.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @package
*/
Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY) {
if (!Blockly.Events.getGroup()) {
Blockly.Events.setGroup(true);
}
this.workspace_.setResizesEnabled(false);
Blockly.BlockAnimations.disconnectUiStop();
if (this.draggingBlock_.getParent()) {
this.draggingBlock_.unplug();
var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
var newLoc = goog.math.Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.translate(newLoc.x, newLoc.y);
Blockly.BlockAnimations.disconnectUiEffect(this.draggingBlock_);
}
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_();
var toolbox = this.workspace_.getToolbox();
if (toolbox) {
var style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' :
'blocklyToolboxGrab';
toolbox.addStyle(style);
}
};
/**
* Execute a step of block dragging, based on the given event. Update the
* display accordingly.
* @param {!Event} e The most recent move event.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
* @return {boolean} True if the event should be propagated, false if not.
*/
Blockly.BlockDragger.prototype.dragBlock = function(e, currentDragDeltaXY) {
var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
var newLoc = goog.math.Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveDuringDrag(newLoc);
this.dragIcons_(delta);
this.deleteArea_ = this.workspace_.isDeleteArea(e);
var isOutside = !this.workspace_.isInsideBlocksArea(e);
this.draggedConnectionManager_.update(delta, this.deleteArea_, isOutside);
if (isOutside !== this.wasOutside_) {
this.fireDragOutsideEvent_(isOutside);
this.wasOutside_ = isOutside;
}
this.updateCursorDuringBlockDrag_(isOutside);
return isOutside;
};
/**
* Finish a block drag and put the block back on the workspace.
* @param {!Event} e The mouseup/touchend event.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
*/
Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) {
// Make sure internal state is fresh.
this.dragBlock(e, currentDragDeltaXY);
this.dragIconData_ = [];
var isOutside = this.wasOutside_;
this.fireEndDragEvent_(isOutside);
this.draggingBlock_.setMouseThroughStyle(false);
Blockly.BlockAnimations.disconnectUiStop();
var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
var newLoc = goog.math.Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveOffDragSurface_(newLoc);
// Scratch-specific: note possible illegal definition deletion for rollback below.
var isDeletingProcDef = this.wouldDeleteBlock_ &&
(this.draggingBlock_.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE);
if (isDeletingProcDef) {
var procCodeBeingDeleted = this.draggingBlock_.getInput('custom_block').connection.targetBlock().getProcCode();
}
var deleted = this.maybeDeleteBlock_();
if (!deleted) {
// These are expensive and don't need to be done if we're deleting.
this.draggingBlock_.moveConnections_(delta.x, delta.y);
this.draggingBlock_.setDragging(false);
this.fireMoveEvent_();
if (this.draggedConnectionManager_.wouldConnectBlock()) {
// Applying connections also rerenders the relevant blocks.
this.draggedConnectionManager_.applyConnections();
} else {
this.draggingBlock_.render();
}
this.draggingBlock_.scheduleSnapAndBump();
}
this.workspace_.setResizesEnabled(true);
var toolbox = this.workspace_.getToolbox();
if (toolbox) {
var style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' :
'blocklyToolboxGrab';
toolbox.removeStyle(style);
}
Blockly.Events.setGroup(false);
if (isOutside) {
var ws = this.workspace_;
// Reset a drag to outside of scratch-blocks
setTimeout(function() {
ws.undo();
});
}
// Scratch-specific: roll back deletes that create call blocks with defines.
// Have to wait for connections to be re-established, so put in setTimeout.
// Only do this if we deleted a proc def.
if (isDeletingProcDef) {
var ws = this.workspace_;
setTimeout(function() {
var allBlocks = ws.getAllBlocks();
for (var i = 0; i < allBlocks.length; i++) {
var block = allBlocks[i];
if (block.type == Blockly.PROCEDURES_CALL_BLOCK_TYPE) {
var procCode = block.getProcCode();
// Check for call blocks with no associated define block.
if (procCode === procCodeBeingDeleted) {
alert(Blockly.Msg.PROCEDURE_USED);
ws.undo();
return; // There can only be one define deletion at a time.
}
}
}
// The proc deletion was valid, update the toolbox.
ws.refreshToolboxSelection_();
});
}
};
/**
* Fire an event when the dragged blocks move outside or back into the blocks workspace
* @param {?boolean} isOutside True if the drag is going outside the visible area.
* @private
*/
Blockly.BlockDragger.prototype.fireDragOutsideEvent_ = function(isOutside) {
var event = new Blockly.Events.DragBlockOutside(this.draggingBlock_);
event.isOutside = isOutside;
Blockly.Events.fire(event);
};
/**
* Fire an end drag event at the end of a block drag.
* @param {?boolean} isOutside True if the drag is going outside the visible area.
* @private
*/
Blockly.BlockDragger.prototype.fireEndDragEvent_ = function(isOutside) {
var event = new Blockly.Events.EndBlockDrag(this.draggingBlock_, isOutside);
Blockly.Events.fire(event);
};
/**
* Fire a move event at the end of a block drag.
* @private
*/
Blockly.BlockDragger.prototype.fireMoveEvent_ = function() {
var event = new Blockly.Events.BlockMove(this.draggingBlock_);
event.oldCoordinate = this.startXY_;
event.recordNew();
Blockly.Events.fire(event);
};
/**
* Shut the trash can and, if necessary, delete the dragging block.
* Should be called at the end of a block drag.
* @return {boolean} whether the block was deleted.
* @private
*/
Blockly.BlockDragger.prototype.maybeDeleteBlock_ = function() {
var trashcan = this.workspace_.trashcan;
if (this.wouldDeleteBlock_) {
if (trashcan) {
goog.Timer.callOnce(trashcan.close, 100, trashcan);
}
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
this.draggingBlock_.dispose(false, true);
} else if (trashcan) {
// Make sure the trash can is closed.
trashcan.close();
}
return this.wouldDeleteBlock_;
};
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging block would be deleted if released immediately.
* @param {boolean} isOutside True if the cursor is outside of the blocks workspace
* @private
*/
Blockly.BlockDragger.prototype.updateCursorDuringBlockDrag_ = function(isOutside) {
this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock();
var trashcan = this.workspace_.trashcan;
if (this.wouldDeleteBlock_) {
this.draggingBlock_.setDeleteStyle(true);
if (this.deleteArea_ == Blockly.DELETE_AREA_TRASH && trashcan) {
trashcan.setOpen_(true);
}
} else {
this.draggingBlock_.setDeleteStyle(false);
if (trashcan) {
trashcan.setOpen_(false);
}
}
if (isOutside) {
// Let mouse events through to GUI
this.draggingBlock_.setMouseThroughStyle(true);
} else {
this.draggingBlock_.setMouseThroughStyle(false);
}
};
/**
* Convert a coordinate object from pixels to workspace units, including a
* correction for mutator workspaces.
* This function does not consider differing origins. It simply scales the
* input's x and y values.
* @param {!goog.math.Coordinate} pixelCoord A coordinate with x and y values
* in css pixel units.
* @return {!goog.math.Coordinate} The input coordinate divided by the workspace
* scale.
* @private
*/
Blockly.BlockDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) {
var result = new goog.math.Coordinate(pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale);
if (this.workspace_.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same as
// the scale on the parent workspace.
// Fix that for dragging.
var mainScale = this.workspace_.options.parentWorkspace.scale;
result = result.scale(1 / mainScale);
}
return result;
};
/**
* Move all of the icons connected to this drag.
* @param {!goog.math.Coordinate} dxy How far to move the icons from their
* original positions, in workspace units.
* @private
*/
Blockly.BlockDragger.prototype.dragIcons_ = function(dxy) {
// Moving icons moves their associated bubbles.
for (var i = 0; i < this.dragIconData_.length; i++) {
var data = this.dragIconData_[i];
data.icon.setIconLocation(goog.math.Coordinate.sum(data.location, dxy));
}
};

View File

@@ -0,0 +1,531 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Classes for all types of block events.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.Events.BlockBase');
goog.provide('Blockly.Events.BlockChange');
goog.provide('Blockly.Events.BlockCreate');
goog.provide('Blockly.Events.BlockDelete');
goog.provide('Blockly.Events.BlockMove');
goog.provide('Blockly.Events.Change'); // Deprecated.
goog.provide('Blockly.Events.Create'); // Deprecated.
goog.provide('Blockly.Events.Delete'); // Deprecated.
goog.provide('Blockly.Events.Move'); // Deprecated.
goog.require('Blockly.Events');
goog.require('Blockly.Events.Abstract');
goog.require('goog.array');
goog.require('goog.math.Coordinate');
/**
* Abstract class for a block event.
* @param {Blockly.Block} block The block this event corresponds to.
* @extends {Blockly.Events.Abstract}
* @constructor
*/
Blockly.Events.BlockBase = function(block) {
Blockly.Events.BlockBase.superClass_.constructor.call(this);
/**
* The block id for the block this event pertains to
* @type {string}
*/
this.blockId = block.id;
this.workspaceId = block.workspace.id;
};
goog.inherits(Blockly.Events.BlockBase, Blockly.Events.Abstract);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.BlockBase.prototype.toJson = function() {
var json = Blockly.Events.BlockBase.superClass_.toJson.call(this);
json['blockId'] = this.blockId;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.BlockBase.prototype.fromJson = function(json) {
Blockly.Events.BlockBase.superClass_.toJson.call(this);
this.blockId = json['blockId'];
};
/**
* Class for a block change event.
* @param {Blockly.Block} block The changed block. Null for a blank event.
* @param {string} element One of 'field', 'comment', 'disabled', etc.
* @param {?string} name Name of input or field affected, or null.
* @param {*} oldValue Previous value of element.
* @param {*} newValue New value of element.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.Change = function(block, element, name, oldValue, newValue) {
if (!block) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.Change.superClass_.constructor.call(this, block);
this.element = element;
this.name = name;
this.oldValue = oldValue;
this.newValue = newValue;
};
goog.inherits(Blockly.Events.Change, Blockly.Events.BlockBase);
/**
* Class for a block change event.
* @param {Blockly.Block} block The changed block. Null for a blank event.
* @param {string} element One of 'field', 'comment', 'disabled', etc.
* @param {?string} name Name of input or field affected, or null.
* @param {*} oldValue Previous value of element.
* @param {*} newValue New value of element.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.BlockChange = Blockly.Events.Change;
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.Change.prototype.type = Blockly.Events.CHANGE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.Change.prototype.toJson = function() {
var json = Blockly.Events.Change.superClass_.toJson.call(this);
json['element'] = this.element;
if (this.name) {
json['name'] = this.name;
}
json['newValue'] = this.newValue;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.Change.prototype.fromJson = function(json) {
Blockly.Events.Change.superClass_.fromJson.call(this, json);
this.element = json['element'];
this.name = json['name'];
this.newValue = json['newValue'];
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
Blockly.Events.Change.prototype.isNull = function() {
return this.oldValue == this.newValue;
};
/**
* Run a change event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.Change.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
var block = workspace.getBlockById(this.blockId);
if (!block) {
console.warn("Can't change non-existent block: " + this.blockId);
return;
}
if (block.mutator) {
// Close the mutator (if open) since we don't want to update it.
block.mutator.setVisible(false);
}
var value = forward ? this.newValue : this.oldValue;
switch (this.element) {
case 'field':
var field = block.getField(this.name);
if (field) {
// Run the validator for any side-effects it may have.
// The validator's opinion on validity is ignored.
field.callValidator(value);
field.setValue(value);
} else {
console.warn("Can't set non-existent field: " + this.name);
}
break;
case 'comment':
block.setCommentText(value || null);
break;
case 'collapsed':
block.setCollapsed(value);
break;
case 'disabled':
block.setDisabled(value);
break;
case 'inline':
block.setInputsInline(value);
break;
case 'mutation':
var oldMutation = '';
if (block.mutationToDom) {
var oldMutationDom = block.mutationToDom();
oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
}
if (block.domToMutation) {
value = value || '<mutation></mutation>';
var dom = Blockly.Xml.textToDom('<xml>' + value + '</xml>');
block.domToMutation(dom.firstChild);
}
Blockly.Events.fire(new Blockly.Events.Change(
block, 'mutation', null, oldMutation, value));
break;
default:
console.warn('Unknown change type: ' + this.element);
}
};
/**
* Class for a block creation event.
* @param {Blockly.Block} block The created block. Null for a blank event.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.Create = function(block) {
if (!block) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.Create.superClass_.constructor.call(this, block);
if (block.workspace.rendered) {
this.xml = Blockly.Xml.blockToDomWithXY(block);
} else {
this.xml = Blockly.Xml.blockToDom(block);
}
this.ids = Blockly.Events.getDescendantIds_(block);
};
goog.inherits(Blockly.Events.Create, Blockly.Events.BlockBase);
/**
* Class for a block creation event.
* @param {Blockly.Block} block The created block. Null for a blank event.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.BlockCreate = Blockly.Events.Create;
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.Create.prototype.type = Blockly.Events.CREATE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.Create.prototype.toJson = function() {
var json = Blockly.Events.Create.superClass_.toJson.call(this);
json['xml'] = Blockly.Xml.domToText(this.xml);
json['ids'] = this.ids;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.Create.prototype.fromJson = function(json) {
Blockly.Events.Create.superClass_.fromJson.call(this, json);
this.xml = Blockly.Xml.textToDom('<xml>' + json['xml'] + '</xml>').firstChild;
this.ids = json['ids'];
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.Create.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
if (forward) {
var xml = goog.dom.createDom('xml');
xml.appendChild(this.xml);
Blockly.Xml.domToWorkspace(xml, workspace);
} else {
for (var i = 0, id; id = this.ids[i]; i++) {
var block = workspace.getBlockById(id);
if (block) {
block.dispose(false, false);
} else if (id == this.blockId) {
// Only complain about root-level block.
console.warn("Can't uncreate non-existent block: " + id);
}
}
}
};
/**
* Class for a block deletion event.
* @param {Blockly.Block} block The deleted block. Null for a blank event.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.Delete = function(block) {
if (!block) {
return; // Blank event to be populated by fromJson.
}
if (block.getParent()) {
throw 'Connected blocks cannot be deleted.';
}
Blockly.Events.Delete.superClass_.constructor.call(this, block);
if (block.workspace.rendered) {
this.oldXml = Blockly.Xml.blockToDomWithXY(block);
} else {
this.oldXml = Blockly.Xml.blockToDom(block);
}
this.ids = Blockly.Events.getDescendantIds_(block);
};
goog.inherits(Blockly.Events.Delete, Blockly.Events.BlockBase);
/**
* Class for a block deletion event.
* @param {Blockly.Block} block The deleted block. Null for a blank event.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.BlockDelete = Blockly.Events.Delete;
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.Delete.prototype.type = Blockly.Events.DELETE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.Delete.prototype.toJson = function() {
var json = Blockly.Events.Delete.superClass_.toJson.call(this);
json['ids'] = this.ids;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.Delete.prototype.fromJson = function(json) {
Blockly.Events.Delete.superClass_.fromJson.call(this, json);
this.ids = json['ids'];
};
/**
* Run a deletion event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.Delete.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
if (forward) {
for (var i = 0, id; id = this.ids[i]; i++) {
var block = workspace.getBlockById(id);
if (block) {
block.dispose(false, false);
} else if (id == this.blockId) {
// Only complain about root-level block.
console.warn("Can't delete non-existent block: " + id);
}
}
} else {
var xml = goog.dom.createDom('xml');
xml.appendChild(this.oldXml);
Blockly.Xml.domToWorkspace(xml, workspace);
}
};
/**
* Class for a block move event. Created before the move.
* @param {Blockly.Block} block The moved block. Null for a blank event.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.Move = function(block) {
if (!block) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.Move.superClass_.constructor.call(this, block);
var location = this.currentLocation_();
this.oldParentId = location.parentId;
this.oldInputName = location.inputName;
this.oldCoordinate = location.coordinate;
};
goog.inherits(Blockly.Events.Move, Blockly.Events.BlockBase);
/**
* Class for a block move event. Created before the move.
* @param {Blockly.Block} block The moved block. Null for a blank event.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.BlockMove = Blockly.Events.Move;
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.Move.prototype.type = Blockly.Events.MOVE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.Move.prototype.toJson = function() {
var json = Blockly.Events.Move.superClass_.toJson.call(this);
if (this.newParentId) {
json['newParentId'] = this.newParentId;
}
if (this.newInputName) {
json['newInputName'] = this.newInputName;
}
if (this.newCoordinate) {
json['newCoordinate'] = Math.round(this.newCoordinate.x) + ',' +
Math.round(this.newCoordinate.y);
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.Move.prototype.fromJson = function(json) {
Blockly.Events.Move.superClass_.fromJson.call(this, json);
this.newParentId = json['newParentId'];
this.newInputName = json['newInputName'];
if (json['newCoordinate']) {
var xy = json['newCoordinate'].split(',');
this.newCoordinate =
new goog.math.Coordinate(parseFloat(xy[0]), parseFloat(xy[1]));
}
};
/**
* Record the block's new location. Called after the move.
*/
Blockly.Events.Move.prototype.recordNew = function() {
var location = this.currentLocation_();
this.newParentId = location.parentId;
this.newInputName = location.inputName;
this.newCoordinate = location.coordinate;
};
/**
* Returns the parentId and input if the block is connected,
* or the XY location if disconnected.
* @return {!Object} Collection of location info.
* @private
*/
Blockly.Events.Move.prototype.currentLocation_ = function() {
var workspace = Blockly.Workspace.getById(this.workspaceId);
var block = workspace.getBlockById(this.blockId);
var location = {};
var parent = block.getParent();
if (parent) {
location.parentId = parent.id;
var input = parent.getInputWithBlock(block);
if (input) {
location.inputName = input.name;
}
} else {
var blockXY = block.getRelativeToSurfaceXY();
// The X position in the block move event should be the language agnostic
// position of the block. I.e. it should not be different in LTR vs. RTL.
var rtlAwareX = workspace.RTL ? workspace.getWidth() - blockXY.x : blockXY.x;
location.coordinate = new goog.math.Coordinate(rtlAwareX, blockXY.y);
}
return location;
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
Blockly.Events.Move.prototype.isNull = function() {
return this.oldParentId == this.newParentId &&
this.oldInputName == this.newInputName &&
goog.math.Coordinate.equals(this.oldCoordinate, this.newCoordinate);
};
/**
* Run a move event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.Move.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
var block = workspace.getBlockById(this.blockId);
if (!block) {
console.warn("Can't move non-existent block: " + this.blockId);
return;
}
var parentId = forward ? this.newParentId : this.oldParentId;
var inputName = forward ? this.newInputName : this.oldInputName;
var coordinate = forward ? this.newCoordinate : this.oldCoordinate;
var parentBlock = null;
if (parentId) {
parentBlock = workspace.getBlockById(parentId);
if (!parentBlock) {
console.warn("Can't connect to non-existent block: " + parentId);
return;
}
}
if (block.getParent()) {
block.unplug();
}
if (coordinate) {
var xy = block.getRelativeToSurfaceXY();
var rtlAwareX = workspace.RTL ? workspace.getWidth() - coordinate.x : coordinate.x;
block.moveBy(rtlAwareX - xy.x, coordinate.y - xy.y);
} else {
var blockConnection = block.outputConnection || block.previousConnection;
var parentConnection;
if (inputName) {
var input = parentBlock.getInput(inputName);
if (input) {
parentConnection = input.connection;
}
} else if (blockConnection.type == Blockly.PREVIOUS_STATEMENT) {
parentConnection = parentBlock.nextConnection;
}
if (parentConnection) {
blockConnection.connect(parentConnection);
} else {
console.warn("Can't connect to non-existent input: " + inputName);
}
}
};

View File

@@ -0,0 +1,892 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Methods for graphically rendering a block as SVG.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.BlockSvg.render');
goog.require('Blockly.BlockSvg');
// UI constants for rendering blocks.
/**
* Grid unit to pixels conversion
* @const
*/
Blockly.BlockSvg.GRID_UNIT = 4;
/**
* Horizontal space between elements.
* @const
*/
Blockly.BlockSvg.SEP_SPACE_X = 3 * Blockly.BlockSvg.GRID_UNIT;
/**
* Vertical space between elements.
* @const
*/
Blockly.BlockSvg.SEP_SPACE_Y = 3 * Blockly.BlockSvg.GRID_UNIT;
/**
* Vertical space above blocks with statements.
* @const
*/
Blockly.BlockSvg.STATEMENT_BLOCK_SPACE = 3 * Blockly.BlockSvg.GRID_UNIT;
/**
* Height of user inputs
* @const
*/
Blockly.BlockSvg.FIELD_HEIGHT = 8 * Blockly.BlockSvg.GRID_UNIT;
/**
* Width of user inputs
* @const
*/
Blockly.BlockSvg.FIELD_WIDTH = 12 * Blockly.BlockSvg.GRID_UNIT;
/**
* Editable field padding (left/right of the text).
* @const
*/
Blockly.BlockSvg.EDITABLE_FIELD_PADDING = 0;
/**
* Minimum width of user inputs during editing
* @const
*/
Blockly.BlockSvg.FIELD_WIDTH_MIN_EDIT = 13 * Blockly.BlockSvg.GRID_UNIT;
/**
* Maximum width of user inputs during editing
* @const
*/
Blockly.BlockSvg.FIELD_WIDTH_MAX_EDIT = 24 * Blockly.BlockSvg.GRID_UNIT;
/**
* Maximum height of user inputs during editing
* @const
*/
Blockly.BlockSvg.FIELD_HEIGHT_MAX_EDIT = 10 * Blockly.BlockSvg.GRID_UNIT;
/**
* Top padding of user inputs
* @const
*/
Blockly.BlockSvg.FIELD_TOP_PADDING = 0.25 * Blockly.BlockSvg.GRID_UNIT;
/**
* Corner radius of number inputs
* @const
*/
Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS = 4 * Blockly.BlockSvg.GRID_UNIT;
/**
* Corner radius of text inputs
* @const
*/
Blockly.BlockSvg.TEXT_FIELD_CORNER_RADIUS = 1 * Blockly.BlockSvg.GRID_UNIT;
/**
* Default radius for a field, in px.
* @const
*/
Blockly.BlockSvg.FIELD_DEFAULT_CORNER_RADIUS = 4 * Blockly.BlockSvg.GRID_UNIT;
/**
* Minimum width of a block.
* @const
*/
Blockly.BlockSvg.MIN_BLOCK_X = 1 / 2 * 16 * Blockly.BlockSvg.GRID_UNIT;
/**
* Minimum height of a block.
* @const
*/
Blockly.BlockSvg.MIN_BLOCK_Y = 16 * Blockly.BlockSvg.GRID_UNIT;
/**
* Width of horizontal puzzle tab.
* @const
*/
Blockly.BlockSvg.TAB_WIDTH = 2 * Blockly.BlockSvg.GRID_UNIT;
/**
* Rounded corner radius.
* @const
*/
Blockly.BlockSvg.CORNER_RADIUS = 1 * Blockly.BlockSvg.GRID_UNIT;
/**
* Rounded corner radius.
* @const
*/
Blockly.BlockSvg.HAT_CORNER_RADIUS = 8 * Blockly.BlockSvg.GRID_UNIT;
/**
* Full height of connector notch including rounded corner.
* @const
*/
Blockly.BlockSvg.NOTCH_HEIGHT = 8 * Blockly.BlockSvg.GRID_UNIT + 2;
/**
* Width of connector notch
* @const
*/
Blockly.BlockSvg.NOTCH_WIDTH = 2 * Blockly.BlockSvg.GRID_UNIT;
/**
* SVG path for drawing next/previous notch from top to bottom.
* Drawn in pixel units since Bezier control points are off the grid.
* @const
*/
Blockly.BlockSvg.NOTCH_PATH_DOWN =
'c 0,2 1,3 2,4 ' +
'l 4,4 ' +
'c 1,1 2,2 2,4 ' +
'v 12 ' +
'c 0,2 -1,3 -2,4 ' +
'l -4,4 ' +
'c -1,1 -2,2 -2,4';
/**
* SVG path for drawing next/previous notch from bottom to top.
* Drawn in pixel units since Bezier control points are off the grid.
* @const
*/
Blockly.BlockSvg.NOTCH_PATH_UP =
'c 0,-2 1,-3 2,-4 ' +
'l 4,-4 ' +
'c 1,-1 2,-2 2,-4 ' +
'v -12 ' +
'c 0,-2 -1,-3 -2,-4 ' +
'l -4,-4 ' +
'c -1,-1 -2,-2 -2,-4';
/**
* Width of rendered image field in px
* @const
*/
Blockly.BlockSvg.IMAGE_FIELD_WIDTH = 10 * Blockly.BlockSvg.GRID_UNIT;
/**
* Height of rendered image field in px
* @const
*/
Blockly.BlockSvg.IMAGE_FIELD_HEIGHT = 10 * Blockly.BlockSvg.GRID_UNIT;
/**
* y-offset of the top of the field shadow block from the bottom of the block.
* @const
*/
Blockly.BlockSvg.FIELD_Y_OFFSET = -2 * Blockly.BlockSvg.GRID_UNIT;
/**
* SVG start point for drawing the top-left corner.
* @const
*/
Blockly.BlockSvg.TOP_LEFT_CORNER_START =
'm ' + Blockly.BlockSvg.CORNER_RADIUS + ',0';
/**
* SVG path for drawing the rounded top-left corner.
* @const
*/
Blockly.BlockSvg.TOP_LEFT_CORNER =
'A ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
'0,' + Blockly.BlockSvg.CORNER_RADIUS;
/**
* SVG start point for drawing the top-left corner.
* @const
*/
Blockly.BlockSvg.HAT_TOP_LEFT_CORNER_START =
'm ' + Blockly.BlockSvg.HAT_CORNER_RADIUS + ',0';
/**
* SVG path for drawing the rounded top-left corner.
* @const
*/
Blockly.BlockSvg.HAT_TOP_LEFT_CORNER =
'A ' + Blockly.BlockSvg.HAT_CORNER_RADIUS + ',' +
Blockly.BlockSvg.HAT_CORNER_RADIUS + ' 0 0,0 ' +
'0,' + Blockly.BlockSvg.HAT_CORNER_RADIUS;
/**
* @type {Object} An object containing computed measurements of this block.
* @private
*/
Blockly.BlockSvg.renderingMetrics_ = null;
/**
* Max text display length for a field (per-horizontal/vertical)
* @const
*/
Blockly.BlockSvg.MAX_DISPLAY_LENGTH = 4;
/**
* Point size of text field before animation. Must match size in CSS.
* See implementation in field_textinput.
*/
Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_INITIAL = 12;
/**
* Point size of text field after animation.
* See implementation in field_textinput.
*/
Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_FINAL = 14;
/**
* Whether text fields are allowed to expand past their truncated block size.
* @const{boolean}
*/
Blockly.BlockSvg.FIELD_TEXTINPUT_EXPAND_PAST_TRUNCATION = true;
/**
* Whether text fields should animate their positioning.
* @const{boolean}
*/
Blockly.BlockSvg.FIELD_TEXTINPUT_ANIMATE_POSITIONING = true;
/**
* @param {!Object} first An object containing computed measurements of a
* block.
* @param {!Object} second Another object containing computed measurements of a
* block.
* @return {boolean} Whether the two sets of metrics are equivalent.
* @private
*/
Blockly.BlockSvg.metricsAreEquivalent_ = function(first, second) {
if (first.statement != second.statement) {
return false;
}
if (first.imageField != second.imageField) {
return false;
}
if ((first.height != second.height) ||
(first.width != second.width) ||
(first.bayHeight != second.bayHeight) ||
(first.bayWidth != second.bayWidth) ||
(first.fieldRadius != second.fieldRadius) ||
(first.startHat != second.startHat)) {
return false;
}
return true;
};
/**
* Play some UI effects (sound) after a connection has been established.
*/
Blockly.BlockSvg.prototype.connectionUiEffect = function() {
this.workspace.getAudioManager().play('click');
};
/**
* Change the colour of a block.
*/
Blockly.BlockSvg.prototype.updateColour = function() {
var fillColour = (this.isGlowing_) ? this.getColourSecondary() : this.getColour();
var strokeColour = this.getColourTertiary();
// Render block stroke
this.svgPath_.setAttribute('stroke', strokeColour);
// Render block fill
var fillColour = (this.isGlowingBlock_) ? this.getColourSecondary() : this.getColour();
this.svgPath_.setAttribute('fill', fillColour);
// Render opacity
this.svgPath_.setAttribute('fill-opacity', this.getOpacity());
// Bump every dropdown to change its colour.
for (var x = 0, input; input = this.inputList[x]; x++) {
for (var y = 0, field; field = input.fieldRow[y]; y++) {
field.setText(null);
}
}
};
/**
* Visual effect to show that if the dragging block is dropped, this block will
* be replaced. If a shadow block it will disappear. Otherwise it will bump.
* @param {boolean} add True if highlighting should be added.
*/
Blockly.BlockSvg.prototype.highlightForReplacement = function(add) {
if (add) {
var replacementGlowFilterId = this.workspace.options.replacementGlowFilterId
|| 'blocklyReplacementGlowFilter';
this.svgPath_.setAttribute('filter', 'url(#' + replacementGlowFilterId + ')');
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
} else {
this.svgPath_.removeAttribute('filter');
Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyReplaceable');
}
};
/**
* Returns a bounding box describing the dimensions of this block
* and any blocks stacked below it.
* @param {boolean=} opt_ignoreFields True if we should ignore fields in the
* size calculation, and just give the size of the base block(s).
* @return {!{height: number, width: number}} Object with height and width properties.
*/
Blockly.BlockSvg.prototype.getHeightWidth = function(opt_ignoreFields) {
var height = this.height;
var width = this.width;
// Add the size of the field shadow block.
if (!opt_ignoreFields && this.getFieldShadowBlock_()) {
height += Blockly.BlockSvg.FIELD_Y_OFFSET;
height += Blockly.BlockSvg.FIELD_HEIGHT;
}
// Recursively add size of subsequent blocks.
var nextBlock = this.getNextBlock();
if (nextBlock) {
var nextHeightWidth = nextBlock.getHeightWidth(opt_ignoreFields);
width += nextHeightWidth.width;
width -= Blockly.BlockSvg.NOTCH_WIDTH; // Exclude width of connected notch.
height = Math.max(height, nextHeightWidth.height);
}
return {height: height, width: width};
};
/**
* Render the block.
* Lays out and reflows a block based on its contents and settings.
* @param {boolean=} opt_bubble If false, just render this block.
* If true, also render block's parent, grandparent, etc. Defaults to true.
*/
Blockly.BlockSvg.prototype.render = function(opt_bubble) {
Blockly.Field.startCache();
this.rendered = true;
var oldMetrics = this.renderingMetrics_;
var metrics = this.renderCompute_();
// Don't redraw if we don't need to.
if (oldMetrics &&
Blockly.BlockSvg.metricsAreEquivalent_(oldMetrics, metrics)) {
// Skipping the redraw is fine, but we may still have to tighten up our
// connections with child blocks.
if (metrics.statement && metrics.statement.connection &&
metrics.statement.targetConnection) {
metrics.statement.connection.tighten_();
}
if (this.nextConnection && this.nextConnection.targetConnection) {
this.nextConnection.tighten_();
}
} else {
this.height = metrics.height;
this.width = metrics.width;
this.renderDraw_(metrics);
this.renderClassify_(metrics);
this.renderingMetrics_ = metrics;
}
if (opt_bubble !== false) {
// Render all blocks above this one (propagate a reflow).
var parentBlock = this.getParent();
if (parentBlock) {
parentBlock.render(true);
} else {
// Top-most block. Fire an event to allow scrollbars to resize.
Blockly.resizeSvgContents(this.workspace);
}
}
Blockly.Field.stopCache();
this.updateIntersectionObserver();
};
/**
* Computes the height and widths for each row and field.
* @return {!Array.<!Array.<!Object>>} 2D array of objects, each containing
* position information.
* @private
*/
Blockly.BlockSvg.prototype.renderCompute_ = function() {
var metrics = {
statement: null,
imageField: null,
iconMenu: null,
width: 0,
height: 0,
bayHeight: 0,
bayWidth: 0,
bayNotchAtRight: true,
fieldRadius: 0,
startHat: false,
endCap: false
};
// Does block have a statement?
for (var i = 0, input; input = this.inputList[i]; i++) {
if (input.type == Blockly.NEXT_STATEMENT) {
metrics.statement = input;
// Compute minimum input size.
metrics.bayHeight = Blockly.BlockSvg.MIN_BLOCK_Y;
metrics.bayWidth = Blockly.BlockSvg.MIN_BLOCK_X;
// Expand input size if there is a connection.
if (input.connection && input.connection.targetConnection) {
var linkedBlock = input.connection.targetBlock();
var bBox = linkedBlock.getHeightWidth(true);
metrics.bayHeight = Math.max(metrics.bayHeight, bBox.height);
metrics.bayWidth = Math.max(metrics.bayWidth, bBox.width);
}
var linkedBlock = input.connection.targetBlock();
if (linkedBlock && !linkedBlock.lastConnectionInStack()) {
metrics.bayNotchAtRight = false;
} else {
metrics.bayWidth -= Blockly.BlockSvg.NOTCH_WIDTH;
}
}
// Find image field, input fields
for (var j = 0, field; field = input.fieldRow[j]; j++) {
if (field instanceof Blockly.FieldImage) {
metrics.imageField = field;
}
if (field instanceof Blockly.FieldIconMenu) {
metrics.iconMenu = field;
}
if (field instanceof Blockly.FieldTextInput) {
metrics.fieldRadius = field.getBorderRadius();
} else {
metrics.fieldRadius = Blockly.BlockSvg.FIELD_DEFAULT_CORNER_RADIUS;
}
}
}
// Determine whether a block is a start hat or end cap by checking connections.
if (this.nextConnection && !this.previousConnection) {
metrics.startHat = true;
}
// End caps have no bay, a previous, no output, and no next.
if (!this.nextConnection && this.previousConnection &&
!this.outputConnection && !metrics.statement) {
metrics.endCap = true;
}
// If this block is an icon menu shadow, attempt to set the parent's
// ImageField src to the one that represents the current value of the field.
if (metrics.iconMenu) {
var currentSrc = metrics.iconMenu.getSrcForValue(metrics.iconMenu.getValue());
if (currentSrc) {
metrics.iconMenu.setParentFieldImage(currentSrc);
}
}
// Always render image field at 40x40 px
// Normal block sizing
metrics.width = Blockly.BlockSvg.SEP_SPACE_X * 2 + Blockly.BlockSvg.IMAGE_FIELD_WIDTH;
metrics.height = Blockly.BlockSvg.SEP_SPACE_Y * 2 + Blockly.BlockSvg.IMAGE_FIELD_HEIGHT;
if (this.outputConnection) {
// Field shadow block
metrics.height = Blockly.BlockSvg.FIELD_HEIGHT;
metrics.width = Blockly.BlockSvg.FIELD_WIDTH;
}
if (metrics.statement) {
// Block with statement (e.g., repeat, forever)
metrics.width += metrics.bayWidth + 4 * Blockly.BlockSvg.CORNER_RADIUS + 2 * Blockly.BlockSvg.GRID_UNIT;
metrics.height = metrics.bayHeight + Blockly.BlockSvg.STATEMENT_BLOCK_SPACE;
}
if (metrics.startHat) {
// Start hats are 1 unit wider to account for optical effect of curve.
metrics.width += 1 * Blockly.BlockSvg.GRID_UNIT;
}
if (metrics.endCap) {
// End caps are 1 unit wider to account for optical effect of no notch.
metrics.width += 1 * Blockly.BlockSvg.GRID_UNIT;
}
return metrics;
};
/**
* Draw the path of the block.
* Move the fields to the correct locations.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) {
// Fetch the block's coordinates on the surface for use in anchoring
// the connections.
var connectionsXY = this.getRelativeToSurfaceXY();
// Assemble the block's path.
var steps = [];
this.renderDrawLeft_(steps, connectionsXY, metrics);
this.renderDrawBottom_(steps, connectionsXY, metrics);
this.renderDrawRight_(steps, connectionsXY, metrics);
this.renderDrawTop_(steps, connectionsXY, metrics);
var pathString = steps.join(' ');
this.svgPath_.setAttribute('d', pathString);
if (this.RTL) {
// Mirror the block's path.
// This is awesome.
this.svgPath_.setAttribute('transform', 'scale(-1 1)');
}
// Horizontal blocks have a single Image Field that is specially positioned
if (metrics.imageField) {
var imageField = metrics.imageField.getSvgRoot();
var imageFieldSize = metrics.imageField.getSize();
// Image field's position is calculated relative to the "end" edge of the
// block.
var imageFieldX = metrics.width - imageFieldSize.width -
Blockly.BlockSvg.SEP_SPACE_X / 1.5;
var imageFieldY = metrics.height - imageFieldSize.height -
Blockly.BlockSvg.SEP_SPACE_Y;
if (metrics.endCap) {
// End-cap image is offset by a grid unit to account for optical effect of no notch.
imageFieldX -= Blockly.BlockSvg.GRID_UNIT;
}
var imageFieldScale = "scale(1 1)";
if (this.RTL) {
// Do we want to mirror the Image Field left-to-right?
if (metrics.imageField.getFlipRTL()) {
imageFieldScale = "scale(-1 1)";
imageFieldX = -metrics.width + imageFieldSize.width +
Blockly.BlockSvg.SEP_SPACE_X / 1.5;
} else {
// If not, don't offset by imageFieldSize.width
imageFieldX = -metrics.width + Blockly.BlockSvg.SEP_SPACE_X / 1.5;
}
}
if (imageField) {
// Fields are invisible on insertion marker.
if (this.isInsertionMarker()) {
imageField.setAttribute('display', 'none');
}
imageField.setAttribute('transform',
'translate(' + imageFieldX + ',' + imageFieldY + ') ' +
imageFieldScale);
}
}
// Position value input
if (this.getFieldShadowBlock_()) {
var input = this.getFieldShadowBlock_().getSvgRoot();
var valueX = (Blockly.BlockSvg.NOTCH_WIDTH +
(metrics.bayWidth ? 2 * Blockly.BlockSvg.GRID_UNIT +
Blockly.BlockSvg.NOTCH_WIDTH * 2 : 0) + metrics.bayWidth);
if (metrics.startHat) {
// Start hats add some left margin to field for visual balance
valueX += Blockly.BlockSvg.GRID_UNIT * 2;
}
if (this.RTL) {
valueX = -valueX;
}
var valueY = (metrics.height + Blockly.BlockSvg.FIELD_Y_OFFSET);
var transformation = 'translate(' + valueX + ',' + valueY + ')';
input.setAttribute('transform', transformation);
}
};
/**
* Give the block an attribute 'data-shapes' that lists its shape[s], and an
* attribute 'data-category' with its category.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderClassify_ = function(metrics) {
var shapes = [];
if (this.isShadow_) {
shapes.push('argument');
} else {
if(metrics.statement) {
shapes.push('c-block');
}
if (metrics.startHat) {
shapes.push('hat'); // c-block+hats are possible (e.x. reprter procedures)
} else if (!metrics.statement) {
shapes.push('stack'); //only call it "stack" if it's not a c-block
}
if (!this.nextConnection) {
shapes.push('end');
}
}
this.svgGroup_.setAttribute('data-shapes', shapes.join(' '));
if (this.getCategory()) {
this.svgGroup_.setAttribute('data-category', this.getCategory());
}
};
/**
* Render the left edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Object} connectionsXY Location of block.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawLeft_ = function(steps, connectionsXY, metrics) {
// Top edge.
if (metrics.startHat) {
// Hat block
// Position the cursor at the top-left starting point.
steps.push(Blockly.BlockSvg.HAT_TOP_LEFT_CORNER_START);
// Top-left rounded corner.
steps.push(Blockly.BlockSvg.HAT_TOP_LEFT_CORNER);
} else if (this.previousConnection) {
// Regular block
// Position the cursor at the top-left starting point.
steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START);
// Top-left rounded corner.
steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER);
var cursorY = metrics.height - Blockly.BlockSvg.CORNER_RADIUS -
Blockly.BlockSvg.SEP_SPACE_Y - Blockly.BlockSvg.NOTCH_HEIGHT;
steps.push('V', cursorY);
steps.push(Blockly.BlockSvg.NOTCH_PATH_DOWN);
// Create previous block connection.
var connectionX = connectionsXY.x;
var connectionY = connectionsXY.y + metrics.height -
Blockly.BlockSvg.CORNER_RADIUS * 2;
this.previousConnection.moveTo(connectionX, connectionY);
// This connection will be tightened when the parent renders.
steps.push('V', metrics.height - Blockly.BlockSvg.CORNER_RADIUS);
} else {
// Input
// Position the cursor at the top-left starting point.
steps.push('m', metrics.fieldRadius + ',0');
// Top-left rounded corner.
steps.push(
'A', metrics.fieldRadius + ',' + metrics.fieldRadius,
'0', '0,0', '0,' + metrics.fieldRadius);
steps.push(
'V', metrics.height - metrics.fieldRadius);
}
};
/**
* Render the bottom edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Object} connectionsXY Location of block.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawBottom_ = function(steps,
connectionsXY, metrics) {
if (metrics.startHat) {
steps.push('a', Blockly.BlockSvg.HAT_CORNER_RADIUS + ',' +
Blockly.BlockSvg.HAT_CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.HAT_CORNER_RADIUS + ',' +
Blockly.BlockSvg.HAT_CORNER_RADIUS);
} else if (this.previousConnection) {
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS);
} else {
// Input
steps.push(
'a', metrics.fieldRadius + ',' + metrics.fieldRadius,
'0', '0,0', metrics.fieldRadius + ',' + metrics.fieldRadius);
}
// Has statement
if (metrics.statement) {
steps.push('h', 4 * Blockly.BlockSvg.GRID_UNIT);
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',-' +
Blockly.BlockSvg.CORNER_RADIUS);
steps.push('v', -2.5 * Blockly.BlockSvg.GRID_UNIT);
steps.push(Blockly.BlockSvg.NOTCH_PATH_UP);
// @todo Why 3?
steps.push('v', -metrics.bayHeight + (Blockly.BlockSvg.CORNER_RADIUS * 3) +
Blockly.BlockSvg.NOTCH_HEIGHT + 2 * Blockly.BlockSvg.GRID_UNIT);
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',-' +
Blockly.BlockSvg.CORNER_RADIUS);
steps.push('h', metrics.bayWidth - (Blockly.BlockSvg.CORNER_RADIUS * 2));
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS);
if (metrics.bayNotchAtRight) {
steps.push('v', metrics.bayHeight - (Blockly.BlockSvg.CORNER_RADIUS * 3) -
Blockly.BlockSvg.NOTCH_HEIGHT - 2 * Blockly.BlockSvg.GRID_UNIT);
steps.push(Blockly.BlockSvg.NOTCH_PATH_DOWN);
}
steps.push('V', metrics.bayHeight + 2 * Blockly.BlockSvg.GRID_UNIT);
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS);
// Create statement connection.
var connectionX = connectionsXY.x + Blockly.BlockSvg.CORNER_RADIUS * 2 +
4 * Blockly.BlockSvg.GRID_UNIT;
if (this.RTL) {
connectionX = connectionsXY.x - Blockly.BlockSvg.CORNER_RADIUS * 2 -
4 * Blockly.BlockSvg.GRID_UNIT;
}
var connectionY = connectionsXY.y + metrics.height -
Blockly.BlockSvg.CORNER_RADIUS * 2;
metrics.statement.connection.moveTo(connectionX, connectionY);
if (metrics.statement.connection.targetConnection) {
metrics.statement.connection.tighten_();
}
}
if (!this.isShadow()) {
steps.push('H', metrics.width - Blockly.BlockSvg.CORNER_RADIUS);
} else {
// input
steps.push('H', metrics.width - metrics.fieldRadius);
}
};
/**
* Render the right edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Object} connectionsXY Location of block.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawRight_ = function(steps, connectionsXY, metrics) {
if (!this.isShadow()) {
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',-' +
Blockly.BlockSvg.CORNER_RADIUS);
steps.push('v', -2.5 * Blockly.BlockSvg.GRID_UNIT);
} else {
// Input
steps.push(
'a', metrics.fieldRadius + ',' + metrics.fieldRadius,
'0', '0,0', metrics.fieldRadius + ',' + -1 * metrics.fieldRadius);
steps.push('v', -1 * (metrics.height - metrics.fieldRadius * 2));
}
if (this.nextConnection) {
steps.push(Blockly.BlockSvg.NOTCH_PATH_UP);
// Include width of notch in block width.
this.width += Blockly.BlockSvg.NOTCH_WIDTH;
// Create next block connection.
var connectionX;
if (this.RTL) {
connectionX = connectionsXY.x - metrics.width;
} else {
connectionX = connectionsXY.x + metrics.width;
}
var connectionY = connectionsXY.y + metrics.height -
Blockly.BlockSvg.CORNER_RADIUS * 2;
this.nextConnection.moveTo(connectionX, connectionY);
if (this.nextConnection.targetConnection) {
this.nextConnection.tighten_();
}
steps.push('V', Blockly.BlockSvg.CORNER_RADIUS);
} else if (!this.isShadow()) {
steps.push('V', Blockly.BlockSvg.CORNER_RADIUS);
}
};
/**
* Render the top edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Object} connectionsXY Location of block.
* @param {!Object} metrics An object containing computed measurements of the
* block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawTop_ = function(steps, connectionsXY, metrics) {
if (!this.isShadow()) {
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 -' +
Blockly.BlockSvg.CORNER_RADIUS + ',-' +
Blockly.BlockSvg.CORNER_RADIUS);
} else {
steps.push(
'a', metrics.fieldRadius + ',' + metrics.fieldRadius,
'0', '0,0', '-' + metrics.fieldRadius + ',-' + metrics.fieldRadius);
}
steps.push('z');
};
/**
* Get the field shadow block, if this block has one.
* <p>This is horizontal Scratch-specific, as "fields" are implemented as inputs
* with shadow blocks, and there is only one per block.
* @return {Blockly.BlockSvg} The field shadow block, or null if not found.
* @private
*/
Blockly.BlockSvg.prototype.getFieldShadowBlock_ = function() {
for (var i = 0, child; child = this.childBlocks_[i]; i++) {
if (child.isShadow()) {
return child;
}
}
return null;
};
/**
* Position an new block correctly, so that it doesn't move the existing block
* when connected to it.
* @param {!Blockly.Block} newBlock The block to position - either the first
* block in a dragged stack or an insertion marker.
* @param {!Blockly.Connection} newConnection The connection on the new block's
* stack - either a connection on newBlock, or the last NEXT_STATEMENT
* connection on the stack if the stack's being dropped before another
* block.
* @param {!Blockly.Connection} existingConnection The connection on the
* existing block, which newBlock should line up with.
*/
Blockly.BlockSvg.prototype.positionNewBlock = function(newBlock, newConnection, existingConnection) {
// We only need to position the new block if it's before the existing one,
// otherwise its position is set by the previous block.
if (newConnection.type == Blockly.NEXT_STATEMENT) {
var dx = existingConnection.x_ - newConnection.x_;
var dy = existingConnection.y_ - newConnection.y_;
// When putting a c-block around another c-block, the outer block must
// positioned above the inner block, as its connection point will stretch
// downwards when connected.
if (newConnection == newBlock.getFirstStatementConnection()) {
dy -= existingConnection.sourceBlock_.getHeightWidth(true).height -
Blockly.BlockSvg.MIN_BLOCK_Y;
}
newBlock.moveBy(dx, dy);
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,618 @@
/**
* @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.DropDownDiv');
goog.require('Blockly.Events');
goog.require('Blockly.FieldAngle');
goog.require('Blockly.FieldCheckbox');
goog.require('Blockly.FieldColour');
goog.require('Blockly.FieldColourSlider');
// 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.FieldIconMenu');
goog.require('Blockly.FieldImage');
goog.require('Blockly.FieldNote');
goog.require('Blockly.FieldTextInput');
goog.require('Blockly.FieldTextInputRemovable');
goog.require('Blockly.FieldTextDropdown');
goog.require('Blockly.FieldNumber');
goog.require('Blockly.FieldNumberDropdown');
goog.require('Blockly.FieldMatrix');
goog.require('Blockly.FieldVariable');
goog.require('Blockly.FieldVerticalSeparator');
goog.require('Blockly.Generator');
goog.require('Blockly.Msg');
goog.require('Blockly.Procedures');
goog.require('Blockly.ScratchMsgs');
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');
// Turn off debugging when compiled.
/* eslint-disable no-unused-vars */
var CLOSURE_DEFINES = {'goog.DEBUG': false};
/* eslint-enable no-unused-vars */
/**
* 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;
/**
* 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;
/**
* Cached value for whether 3D is supported.
* @type {!boolean}
* @private
*/
Blockly.cache3dSupported_ = null;
/**
* 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. Does nothing if the main workspace is not visible.
* @param {!Event} e Key down event.
* @private
*/
// TODO (https://github.com/google/blockly/issues/1998) handle cases where there are multiple workspaces
// and non-main workspaces are able to accept input.
Blockly.onKeyDown_ = function(e) {
if (Blockly.mainWorkspace.options.readOnly || Blockly.utils.isTargetInput(e)
|| (Blockly.mainWorkspace.rendered && !Blockly.mainWorkspace.isVisible())) {
// No key actions on readonly workspaces.
// When focused on an HTML text input widget, don't trap any keys.
// Ignore keypresses on rendered workspaces that have been explicitly
// hidden.
return;
}
var deleteBlock = false;
if (e.keyCode == 27) {
// Pressing esc closes the context menu and any drop-down
Blockly.hideChaff();
Blockly.DropDownDiv.hide();
} 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();
// Don't delete while dragging. Jeez.
if (Blockly.mainWorkspace.isDragging()) {
return;
}
if (Blockly.selected && Blockly.selected.isDeletable()) {
deleteBlock = true;
}
} else if (e.altKey || e.ctrlKey || e.metaKey) {
// Don't use meta keys during drags.
if (Blockly.mainWorkspace.isDragging()) {
return;
}
if (Blockly.selected &&
Blockly.selected.isDeletable() && Blockly.selected.isMovable()) {
// Don't allow copying immovable or undeletable blocks. The next step
// would be to paste, which would create additional undeletable/immovable
// blocks on the workspace.
if (e.keyCode == 67) {
// 'c' for copy.
Blockly.hideChaff();
Blockly.copy_(Blockly.selected);
} else if (e.keyCode == 88 && !Blockly.selected.workspace.isFlyout) {
// 'x' for cut, but not in a flyout.
// Don't even copy the selected item in the flyout.
Blockly.copy_(Blockly.selected);
deleteBlock = true;
}
}
if (e.keyCode == 86) {
// 'v' for paste.
if (Blockly.clipboardXml_) {
Blockly.Events.setGroup(true);
// Pasting always pastes to the main workspace, even if the copy started
// in a flyout workspace.
var workspace = Blockly.clipboardSource_;
if (workspace.isFlyout) {
workspace = workspace.targetWorkspace;
}
workspace.paste(Blockly.clipboardXml_);
Blockly.Events.setGroup(false);
}
} else if (e.keyCode == 90 || e.keyCode === 89) {
// 'z' for undo 'Z' is for redo. 'y' is always redo.
e.preventDefault();
Blockly.hideChaff();
Blockly.mainWorkspace.undo(e.shiftKey || e.keyCode === 89);
}
}
// Common code for delete and cut.
// Don't delete in the flyout.
if (deleteBlock && !Blockly.selected.workspace.isFlyout) {
Blockly.Events.setGroup(true);
Blockly.hideChaff();
Blockly.selected.dispose(/* heal */ true, true);
Blockly.Events.setGroup(false);
}
};
/**
* Copy a block or workspace comment onto the local clipboard.
* @param {!Blockly.Block | !Blockly.WorkspaceComment} toCopy Block or Workspace Comment
* to be copied.
* @private
*/
Blockly.copy_ = function(toCopy) {
if (toCopy.isComment) {
var xml = toCopy.toXmlWithXY();
} else {
var xml = Blockly.Xml.blockToDom(toCopy);
// Encode start position in XML.
var xy = toCopy.getRelativeToSurfaceXY();
xml.setAttribute('x', toCopy.RTL ? -xy.x : xy.x);
xml.setAttribute('y', xy.y);
}
Blockly.clipboardXml_ = xml;
Blockly.clipboardSource_ = toCopy.workspace;
};
/**
* Duplicate this block and its children, or a workspace comment.
* @param {!Blockly.Block | !Blockly.WorkspaceComment} toDuplicate Block or
* Workspace Comment to be copied.
* @private
*/
Blockly.duplicate_ = function(toDuplicate) {
// Save the clipboard.
var clipboardXml = Blockly.clipboardXml_;
var clipboardSource = Blockly.clipboardSource_;
// Create a duplicate via a copy/paste operation.
Blockly.copy_(toDuplicate);
toDuplicate.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.utils.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.hideChaffInternal_(opt_allowToolbox);
Blockly.WidgetDiv.hide(true);
};
/**
* Close tooltips, context menus, dropdown selections, etc.
* For some elements (e.g. field text inputs), rather than hiding, it will
* move them.
* @param {boolean=} opt_allowToolbox If true, don't close the toolbox.
*/
Blockly.hideChaffOnResize = function(opt_allowToolbox) {
Blockly.hideChaffInternal_(opt_allowToolbox);
Blockly.WidgetDiv.repositionForWindowResize();
};
/**
* Does a majority of the work for hideChaff including tooltips, dropdowns,
* toolbox, etc. It does not deal with the WidgetDiv.
* @param {boolean=} opt_allowToolbox If true, don't close the toolbox.
* @private
*/
Blockly.hideChaffInternal_ = function(opt_allowToolbox) {
Blockly.Tooltip.hide();
Blockly.DropDownDiv.hideWithoutAnimation();
if (!opt_allowToolbox) {
var workspace = Blockly.getMainWorkspace();
if (workspace.toolbox_ &&
workspace.toolbox_.flyout_ &&
workspace.toolbox_.flyout_.autoClose) {
workspace.toolbox_.clearSelection();
}
}
};
/**
* 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 response.
* @param {?string} _opt_title An optional title for the prompt.
* @param {?string} _opt_varType An optional variable type for variable specific
* prompt behavior.
*/
Blockly.prompt = function(message, defaultValue, callback, _opt_title,
_opt_varType) {
// opt_title and opt_varType are unused because we only need them to pass
// information to the scratch-gui, which overwrites this function
callback(window.prompt(message, defaultValue));
};
/**
* A callback for status buttons. The window.alert is here for testing and
* should be overridden.
* @param {string} id An identifier.
*/
Blockly.statusButtonCallback = function(id) {
window.alert('status button was pressed for ' + id);
};
/**
* Refresh the visual state of a status button in all extension category headers.
* @param {Blockly.Workspace} workspace A workspace.
*/
Blockly.refreshStatusButtons = function(workspace) {
var buttons = workspace.getFlyout().buttons_;
for (var i = 0; i < buttons.length; i++) {
if (buttons[i] instanceof Blockly.FlyoutExtensionCategoryHeader) {
buttons[i].refreshStatus();
}
}
};
/**
* 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; i < jsonArray.length; i++) {
var elem = jsonArray[i];
if (!elem) {
console.warn(
'Block definition #' + i + ' in JSON array is ' + elem + '. ' +
'Skipping.');
} else {
var typename = elem.type;
if (typename == null || typename === '') {
console.warn(
'Block definition #' + i +
' in JSON array is missing a type attribute. Skipping.');
} else {
Blockly.Blocks[typename] = {
init: Blockly.jsonInitFactory_(elem)
};
}
}
}
};
/**
* Bind an event to a function call. When calling the function, verifies that
* it belongs to the touch stream that is currently being processed, and splits
* multitouch events into multiple events as needed.
* @param {!EventTarget} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @param {boolean=} opt_noCaptureIdentifier True if triggering on this event
* should not block execution of other event handlers on this touch or other
* simultaneous touches.
* @param {boolean=} opt_noPreventDefault True if triggering on this event
* should prevent the default handler. False by default. If
* opt_noPreventDefault is provided, opt_noCaptureIdentifier must also be
* provided.
* @return {!Array.<!Array>} Opaque data that can be passed to unbindEvent_.
*/
Blockly.bindEventWithChecks_ = function(node, name, thisObject, func,
opt_noCaptureIdentifier, opt_noPreventDefault) {
var handled = false;
var wrapFunc = function(e) {
var captureIdentifier = !opt_noCaptureIdentifier;
// Handle each touch point separately. If the event was a mouse event, this
// will hand back an array with one element, which we're fine handling.
var events = Blockly.Touch.splitEventByTouches(e);
for (var i = 0, event; event = events[i]; i++) {
if (captureIdentifier && !Blockly.Touch.shouldHandleEvent(event)) {
continue;
}
Blockly.Touch.setClientFromTouch(event);
if (thisObject) {
func.call(thisObject, event);
} else {
func(event);
}
handled = true;
}
};
node.addEventListener(name, wrapFunc, false);
var bindData = [[node, name, wrapFunc]];
// Add equivalent touch event.
if (name in Blockly.Touch.TOUCH_MAP) {
var touchWrapFunc = function(e) {
wrapFunc(e);
// Calling preventDefault stops the browser from scrolling/zooming the
// page.
var preventDef = !opt_noPreventDefault;
if (handled && preventDef) {
e.preventDefault();
}
};
for (var i = 0, type; type = Blockly.Touch.TOUCH_MAP[name][i]; i++) {
node.addEventListener(type, touchWrapFunc, false);
bindData.push([node, type, touchWrapFunc]);
}
}
return bindData;
};
/**
* Bind an event to a function call. Handles multitouch events by using the
* coordinates of the first changed touch, and doesn't do any safety checks for
* simultaneous event processing.
* @deprecated in favor of bindEventWithChecks_, but preserved for external
* users.
* @param {!EventTarget} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @return {!Array.<!Array>} Opaque data that can be passed to unbindEvent_.
* @private
*/
Blockly.bindEvent_ = function(node, name, thisObject, func) {
var wrapFunc = function(e) {
if (thisObject) {
func.call(thisObject, e);
} else {
func(e);
}
};
node.addEventListener(name, wrapFunc, false);
var bindData = [[node, name, wrapFunc]];
// Add equivalent touch event.
if (name in Blockly.Touch.TOUCH_MAP) {
var touchWrapFunc = function(e) {
// Punt on multitouch events.
if (e.changedTouches.length == 1) {
// Map the touch event's properties to the event.
var touchPoint = e.changedTouches[0];
e.clientX = touchPoint.clientX;
e.clientY = touchPoint.clientY;
}
wrapFunc(e);
// Stop the browser from scrolling/zooming the page.
e.preventDefault();
};
for (var i = 0, type; type = Blockly.Touch.TOUCH_MAP[name][i]; i++) {
node.addEventListener(type, touchWrapFunc, false);
bindData.push([node, type, touchWrapFunc]);
}
}
return bindData;
};
/**
* Unbind one or more events event from a function call.
* @param {!Array.<!Array>} bindData Opaque data from bindEvent_.
* This list is emptied during the course of calling this function.
* @return {!Function} The function call.
* @private
*/
Blockly.unbindEvent_ = function(bindData) {
while (bindData.length) {
var bindDatum = bindData.pop();
var node = bindDatum[0];
var name = bindDatum[1];
var func = bindDatum[2];
node.removeEventListener(name, func, false);
}
return func;
};
/**
* Is the given string a number (includes negative and decimals).
* @param {string} str Input string.
* @return {boolean} True if number, false otherwise.
*/
Blockly.isNumber = function(str) {
return !!str.match(/^\s*-?\d+(\.\d+)?\s*$/);
};
// 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;

View File

@@ -0,0 +1,37 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2013 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 A mapping of block type names to block prototype objects.
* @author spertus@google.com (Ellen Spertus)
*/
'use strict';
/**
* A mapping of block type names to block prototype objects.
* @name Blockly.Blocks
*/
goog.provide('Blockly.Blocks');
/*
* A mapping of block type names to block prototype objects.
* @type {!Object.<string,Object>}
*/
Blockly.Blocks = new Object(null);

View File

@@ -0,0 +1,664 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Object representing a UI bubble.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Bubble');
goog.require('Blockly.Touch');
goog.require('Blockly.Workspace');
goog.require('goog.dom');
goog.require('goog.math.Coordinate');
goog.require('goog.userAgent');
/**
* Class for UI bubble.
* @param {!Blockly.WorkspaceSvg} workspace The workspace on which to draw the
* bubble.
* @param {!Element} content SVG content for the bubble.
* @param {Element} shape SVG element to avoid eclipsing.
* @param {!goog.math.Coordinate} anchorXY Absolute position of bubble's anchor
* point.
* @param {?number} bubbleWidth Width of bubble, or null if not resizable.
* @param {?number} bubbleHeight Height of bubble, or null if not resizable.
* @constructor
*/
Blockly.Bubble = function(workspace, content, shape, anchorXY,
bubbleWidth, bubbleHeight) {
this.workspace_ = workspace;
this.content_ = content;
this.shape_ = shape;
var angle = Blockly.Bubble.ARROW_ANGLE;
if (this.workspace_.RTL) {
angle = -angle;
}
this.arrow_radians_ = Blockly.utils.toRadians(angle);
var canvas = workspace.getBubbleCanvas();
canvas.appendChild(this.createDom_(content, !!(bubbleWidth && bubbleHeight)));
this.setAnchorLocation(anchorXY);
if (!bubbleWidth || !bubbleHeight) {
var bBox = /** @type {SVGLocatable} */ (this.content_).getBBox();
bubbleWidth = bBox.width + 2 * Blockly.Bubble.BORDER_WIDTH;
bubbleHeight = bBox.height + 2 * Blockly.Bubble.BORDER_WIDTH;
}
this.setBubbleSize(bubbleWidth, bubbleHeight);
// Render the bubble.
this.positionBubble_();
this.renderArrow_();
this.rendered_ = true;
if (!workspace.options.readOnly) {
Blockly.bindEventWithChecks_(
this.bubbleBack_, 'mousedown', this, this.bubbleMouseDown_);
if (this.resizeGroup_) {
Blockly.bindEventWithChecks_(
this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_);
}
}
};
/**
* Width of the border around the bubble.
*/
Blockly.Bubble.BORDER_WIDTH = 6;
/**
* Determines the thickness of the base of the arrow in relation to the size
* of the bubble. Higher numbers result in thinner arrows.
*/
Blockly.Bubble.ARROW_THICKNESS = 5;
/**
* The number of degrees that the arrow bends counter-clockwise.
*/
Blockly.Bubble.ARROW_ANGLE = 20;
/**
* The sharpness of the arrow's bend. Higher numbers result in smoother arrows.
*/
Blockly.Bubble.ARROW_BEND = 4;
/**
* Distance between arrow point and anchor point.
*/
Blockly.Bubble.ANCHOR_RADIUS = 8;
/**
* Wrapper function called when a mouseUp occurs during a drag operation.
* @type {Array.<!Array>}
* @private
*/
Blockly.Bubble.onMouseUpWrapper_ = null;
/**
* Wrapper function called when a mouseMove occurs during a drag operation.
* @type {Array.<!Array>}
* @private
*/
Blockly.Bubble.onMouseMoveWrapper_ = null;
/**
* Function to call on resize of bubble.
* @type {Function}
*/
Blockly.Bubble.prototype.resizeCallback_ = null;
/**
* Stop binding to the global mouseup and mousemove events.
* @private
*/
Blockly.Bubble.unbindDragEvents_ = function() {
if (Blockly.Bubble.onMouseUpWrapper_) {
Blockly.unbindEvent_(Blockly.Bubble.onMouseUpWrapper_);
Blockly.Bubble.onMouseUpWrapper_ = null;
}
if (Blockly.Bubble.onMouseMoveWrapper_) {
Blockly.unbindEvent_(Blockly.Bubble.onMouseMoveWrapper_);
Blockly.Bubble.onMouseMoveWrapper_ = null;
}
};
/*
* Handle a mouse-up event while dragging a bubble's border or resize handle.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.Bubble.bubbleMouseUp_ = function(/*e*/) {
Blockly.Touch.clearTouchIdentifier();
Blockly.Bubble.unbindDragEvents_();
};
/**
* Flag to stop incremental rendering during construction.
* @private
*/
Blockly.Bubble.prototype.rendered_ = false;
/**
* Absolute coordinate of anchor point, in workspace coordinates.
* @type {goog.math.Coordinate}
* @private
*/
Blockly.Bubble.prototype.anchorXY_ = null;
/**
* Relative X coordinate of bubble with respect to the anchor's centre,
* in workspace units.
* In RTL mode the initial value is negated.
* @private
*/
Blockly.Bubble.prototype.relativeLeft_ = 0;
/**
* Relative Y coordinate of bubble with respect to the anchor's centre.
* @private
*/
Blockly.Bubble.prototype.relativeTop_ = 0;
/**
* Width of bubble.
* @private
*/
Blockly.Bubble.prototype.width_ = 0;
/**
* Height of bubble.
* @private
*/
Blockly.Bubble.prototype.height_ = 0;
/**
* Automatically position and reposition the bubble.
* @private
*/
Blockly.Bubble.prototype.autoLayout_ = true;
/**
* Create the bubble's DOM.
* @param {!Element} content SVG content for the bubble.
* @param {boolean} hasResize Add diagonal resize gripper if true.
* @return {!Element} The bubble's SVG group.
* @private
*/
Blockly.Bubble.prototype.createDom_ = function(content, hasResize) {
/* Create the bubble. Here's the markup that will be generated:
<g>
<g filter="url(#blocklyEmbossFilter837493)">
<path d="... Z" />
<rect class="blocklyDraggable" rx="8" ry="8" width="180" height="180"/>
</g>
<g transform="translate(165, 165)" class="blocklyResizeSE">
<polygon points="0,15 15,15 15,0"/>
<line class="blocklyResizeLine" x1="5" y1="14" x2="14" y2="5"/>
<line class="blocklyResizeLine" x1="10" y1="14" x2="14" y2="10"/>
</g>
[...content goes here...]
</g>
*/
this.bubbleGroup_ = Blockly.utils.createSvgElement('g', {}, null);
var filter =
{'filter': 'url(#' + this.workspace_.options.embossFilterId + ')'};
if (goog.userAgent.getUserAgentString().indexOf('JavaFX') != -1) {
// Multiple reports that JavaFX can't handle filters. UserAgent:
// Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.44
// (KHTML, like Gecko) JavaFX/8.0 Safari/537.44
// https://github.com/google/blockly/issues/99
filter = {};
}
var bubbleEmboss = Blockly.utils.createSvgElement('g',
filter, this.bubbleGroup_);
this.bubbleArrow_ = Blockly.utils.createSvgElement('path', {}, bubbleEmboss);
this.bubbleBack_ = Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyDraggable',
'x': 0,
'y': 0,
'rx': Blockly.Bubble.BORDER_WIDTH,
'ry': Blockly.Bubble.BORDER_WIDTH
},
bubbleEmboss);
if (hasResize) {
this.resizeGroup_ = Blockly.utils.createSvgElement('g',
{'class': this.workspace_.RTL ?
'blocklyResizeSW' : 'blocklyResizeSE'},
this.bubbleGroup_);
var resizeSize = 2 * Blockly.Bubble.BORDER_WIDTH;
Blockly.utils.createSvgElement('polygon',
{'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())},
this.resizeGroup_);
Blockly.utils.createSvgElement('line',
{
'class': 'blocklyResizeLine',
'x1': resizeSize / 3, 'y1': resizeSize - 1,
'x2': resizeSize - 1, 'y2': resizeSize / 3
}, this.resizeGroup_);
Blockly.utils.createSvgElement('line',
{
'class': 'blocklyResizeLine',
'x1': resizeSize * 2 / 3,
'y1': resizeSize - 1,
'x2': resizeSize - 1,
'y2': resizeSize * 2 / 3
}, this.resizeGroup_);
} else {
this.resizeGroup_ = null;
}
this.bubbleGroup_.appendChild(content);
return this.bubbleGroup_;
};
/**
* Return the root node of the bubble's SVG group.
* @return {Element} The root SVG node of the bubble's group.
*/
Blockly.Bubble.prototype.getSvgRoot = function() {
return this.bubbleGroup_;
};
/**
* Expose the block's ID on the bubble's top-level SVG group.
* @param {string} id ID of block.
*/
Blockly.Bubble.prototype.setSvgId = function(id) {
if (this.bubbleGroup_.dataset) {
this.bubbleGroup_.dataset.blockId = id;
}
};
/**
* Handle a mouse-down on bubble's border.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Bubble.prototype.bubbleMouseDown_ = function(e) {
var gesture = this.workspace_.getGesture(e);
if (gesture) {
gesture.handleBubbleStart(e, this);
}
};
/**
* Show the context menu for this bubble.
* @param {!Event} _e Mouse event.
* @private
*/
Blockly.Bubble.prototype.showContextMenu_ = function(_e) {
// NOP on bubbles, but used by the bubble dragger to pass events to
// workspace comments.
};
/**
* Get whether this bubble is deletable or not.
* @return {boolean} True if deletable.
* @package
*/
Blockly.Bubble.prototype.isDeletable = function() {
return false;
};
/**
* Handle a mouse-down on bubble's resize corner.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Bubble.prototype.resizeMouseDown_ = function(e) {
this.promote_();
Blockly.Bubble.unbindDragEvents_();
if (Blockly.utils.isRightButton(e)) {
// No right-click.
e.stopPropagation();
return;
}
// Left-click (or middle click)
this.workspace_.startDrag(e, new goog.math.Coordinate(
this.workspace_.RTL ? -this.width_ : this.width_, this.height_));
Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(document,
'mouseup', this, Blockly.Bubble.bubbleMouseUp_);
Blockly.Bubble.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(document,
'mousemove', this, this.resizeMouseMove_);
Blockly.hideChaff();
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
};
/**
* Resize this bubble to follow the mouse.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.Bubble.prototype.resizeMouseMove_ = function(e) {
this.autoLayout_ = false;
var newXY = this.workspace_.moveDrag(e);
this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y);
if (this.workspace_.RTL) {
// RTL requires the bubble to move its left edge.
this.positionBubble_();
}
};
/**
* Register a function as a callback event for when the bubble is resized.
* @param {!Function} callback The function to call on resize.
*/
Blockly.Bubble.prototype.registerResizeEvent = function(callback) {
this.resizeCallback_ = callback;
};
/**
* Move this bubble to the top of the stack.
* @return {!boolean} Whether or not the bubble has been moved.
* @private
*/
Blockly.Bubble.prototype.promote_ = function() {
var svgGroup = this.bubbleGroup_.parentNode;
if (svgGroup.lastChild !== this.bubbleGroup_) {
svgGroup.appendChild(this.bubbleGroup_);
return true;
}
return false;
};
/**
* Notification that the anchor has moved.
* Update the arrow and bubble accordingly.
* @param {!goog.math.Coordinate} xy Absolute location.
*/
Blockly.Bubble.prototype.setAnchorLocation = function(xy) {
this.anchorXY_ = xy;
if (this.rendered_) {
this.positionBubble_();
}
};
/**
* Position the bubble so that it does not fall off-screen.
* @private
*/
Blockly.Bubble.prototype.layoutBubble_ = function() {
// Compute the preferred bubble location.
var relativeLeft = -this.width_ / 4;
var relativeTop = -this.height_ - Blockly.BlockSvg.MIN_BLOCK_Y;
// Prevent the bubble from being off-screen.
var metrics = this.workspace_.getMetrics();
metrics.viewWidth /= this.workspace_.scale;
metrics.viewLeft /= this.workspace_.scale;
var anchorX = this.anchorXY_.x;
if (this.workspace_.RTL) {
if (anchorX - metrics.viewLeft - relativeLeft - this.width_ <
Blockly.Scrollbar.scrollbarThickness) {
// Slide the bubble right until it is onscreen.
relativeLeft = anchorX - metrics.viewLeft - this.width_ -
Blockly.Scrollbar.scrollbarThickness;
} else if (anchorX - metrics.viewLeft - relativeLeft >
metrics.viewWidth) {
// Slide the bubble left until it is onscreen.
relativeLeft = anchorX - metrics.viewLeft - metrics.viewWidth;
}
} else {
if (anchorX + relativeLeft < metrics.viewLeft) {
// Slide the bubble right until it is onscreen.
relativeLeft = metrics.viewLeft - anchorX;
} else if (metrics.viewLeft + metrics.viewWidth <
anchorX + relativeLeft + this.width_ +
Blockly.BlockSvg.SEP_SPACE_X +
Blockly.Scrollbar.scrollbarThickness) {
// Slide the bubble left until it is onscreen.
relativeLeft = metrics.viewLeft + metrics.viewWidth - anchorX -
this.width_ - Blockly.Scrollbar.scrollbarThickness;
}
}
if (this.anchorXY_.y + relativeTop < metrics.viewTop) {
// Slide the bubble below the block.
var bBox = /** @type {SVGLocatable} */ (this.shape_).getBBox();
relativeTop = bBox.height;
}
this.relativeLeft_ = relativeLeft;
this.relativeTop_ = relativeTop;
};
/**
* Move the bubble to a location relative to the anchor's centre.
* @private
*/
Blockly.Bubble.prototype.positionBubble_ = function() {
var left = this.anchorXY_.x;
if (this.workspace_.RTL) {
left -= this.relativeLeft_ ;
} else {
left += this.relativeLeft_;
}
var top = this.relativeTop_ + this.anchorXY_.y;
this.moveTo(left, top);
};
/**
* Move the bubble group to the specified location in workspace coordinates.
* @param {number} x The x position to move to.
* @param {number} y The y position to move to.
* @package
*/
Blockly.Bubble.prototype.moveTo = function(x, y) {
this.bubbleGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')');
};
/**
* Get the dimensions of this bubble.
* @return {!Object} Object with width and height properties.
*/
Blockly.Bubble.prototype.getBubbleSize = function() {
return {width: this.width_, height: this.height_};
};
/**
* Size this bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
*/
Blockly.Bubble.prototype.setBubbleSize = function(width, height) {
var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
// Minimum size of a bubble.
width = Math.max(width, doubleBorderWidth + 45);
height = Math.max(height, doubleBorderWidth + 20);
this.width_ = width;
this.height_ = height;
this.bubbleBack_.setAttribute('width', width);
this.bubbleBack_.setAttribute('height', height);
if (this.resizeGroup_) {
if (this.workspace_.RTL) {
// Mirror the resize group.
var resizeSize = 2 * Blockly.Bubble.BORDER_WIDTH;
this.resizeGroup_.setAttribute('transform', 'translate(' +
resizeSize + ',' + (height - doubleBorderWidth) + ') scale(-1 1)');
} else {
this.resizeGroup_.setAttribute('transform', 'translate(' +
(width - doubleBorderWidth) + ',' +
(height - doubleBorderWidth) + ')');
}
}
if (this.rendered_) {
if (this.autoLayout_) {
this.layoutBubble_();
}
this.positionBubble_();
this.renderArrow_();
}
// Allow the contents to resize.
if (this.resizeCallback_) {
this.resizeCallback_();
}
};
/**
* Draw the arrow between the bubble and the origin.
* @private
*/
Blockly.Bubble.prototype.renderArrow_ = function() {
var steps = [];
// Find the relative coordinates of the center of the bubble.
var relBubbleX = this.width_ / 2;
var relBubbleY = this.height_ / 2;
// Find the relative coordinates of the center of the anchor.
var relAnchorX = -this.relativeLeft_;
var relAnchorY = -this.relativeTop_;
if (relBubbleX == relAnchorX && relBubbleY == relAnchorY) {
// Null case. Bubble is directly on top of the anchor.
// Short circuit this rather than wade through divide by zeros.
steps.push('M ' + relBubbleX + ',' + relBubbleY);
} else {
// Compute the angle of the arrow's line.
var rise = relAnchorY - relBubbleY;
var run = relAnchorX - relBubbleX;
if (this.workspace_.RTL) {
run *= -1;
}
var hypotenuse = Math.sqrt(rise * rise + run * run);
var angle = Math.acos(run / hypotenuse);
if (rise < 0) {
angle = 2 * Math.PI - angle;
}
// Compute a line perpendicular to the arrow.
var rightAngle = angle + Math.PI / 2;
if (rightAngle > Math.PI * 2) {
rightAngle -= Math.PI * 2;
}
var rightRise = Math.sin(rightAngle);
var rightRun = Math.cos(rightAngle);
// Calculate the thickness of the base of the arrow.
var bubbleSize = this.getBubbleSize();
var thickness = (bubbleSize.width + bubbleSize.height) /
Blockly.Bubble.ARROW_THICKNESS;
thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 4;
// Back the tip of the arrow off of the anchor.
var backoffRatio = 1 - Blockly.Bubble.ANCHOR_RADIUS / hypotenuse;
relAnchorX = relBubbleX + backoffRatio * run;
relAnchorY = relBubbleY + backoffRatio * rise;
// Coordinates for the base of the arrow.
var baseX1 = relBubbleX + thickness * rightRun;
var baseY1 = relBubbleY + thickness * rightRise;
var baseX2 = relBubbleX - thickness * rightRun;
var baseY2 = relBubbleY - thickness * rightRise;
// Distortion to curve the arrow.
var swirlAngle = angle + this.arrow_radians_;
if (swirlAngle > Math.PI * 2) {
swirlAngle -= Math.PI * 2;
}
var swirlRise = Math.sin(swirlAngle) *
hypotenuse / Blockly.Bubble.ARROW_BEND;
var swirlRun = Math.cos(swirlAngle) *
hypotenuse / Blockly.Bubble.ARROW_BEND;
steps.push('M' + baseX1 + ',' + baseY1);
steps.push('C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) +
' ' + relAnchorX + ',' + relAnchorY +
' ' + relAnchorX + ',' + relAnchorY);
steps.push('C' + relAnchorX + ',' + relAnchorY +
' ' + (baseX2 + swirlRun) + ',' + (baseY2 + swirlRise) +
' ' + baseX2 + ',' + baseY2);
}
steps.push('z');
this.bubbleArrow_.setAttribute('d', steps.join(' '));
};
/**
* Change the colour of a bubble.
* @param {string} hexColour Hex code of colour.
*/
Blockly.Bubble.prototype.setColour = function(hexColour) {
this.bubbleBack_.setAttribute('fill', hexColour);
this.bubbleArrow_.setAttribute('fill', hexColour);
};
/**
* Dispose of this bubble.
*/
Blockly.Bubble.prototype.dispose = function() {
Blockly.Bubble.unbindDragEvents_();
// Dispose of and unlink the bubble.
goog.dom.removeNode(this.bubbleGroup_);
this.bubbleGroup_ = null;
this.bubbleArrow_ = null;
this.bubbleBack_ = null;
this.resizeGroup_ = null;
this.workspace_ = null;
this.content_ = null;
this.shape_ = null;
};
/**
* Move this bubble during a drag, taking into account whether or not there is
* a drag surface.
* @param {?Blockly.BlockDragSurfaceSvg} dragSurface The surface that carries
* rendered items during a drag, or null if no drag surface is in use.
* @param {!goog.math.Coordinate} newLoc The location to translate to, in
* workspace coordinates.
* @package
*/
Blockly.Bubble.prototype.moveDuringDrag = function(dragSurface, newLoc) {
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 {
this.relativeLeft_ = newLoc.x - this.anchorXY_.x;
}
this.relativeTop_ = newLoc.y - this.anchorXY_.y;
this.renderArrow_();
};
/**
* Return the coordinates of the top corner of this bubble's starting edge (e.g.
* top left corner in LTR and top right corner in RTL) relative
* to the drawing surface's origin (0,0), in workspace units.
* @return {!goog.math.Coordinate} Object with .x and .y properties.
*/
Blockly.Bubble.prototype.getRelativeToSurfaceXY = function() {
return new goog.math.Coordinate(
this.workspace_.RTL ? this.anchorXY_.x - this.relativeLeft_ : this.anchorXY_.x + this.relativeLeft_,
this.anchorXY_.y + this.relativeTop_);
};
/**
* Set whether auto-layout of this bubble is enabled. The first time a bubble
* is shown it positions itself to not cover any blocks. Once a user has
* dragged it to reposition, it renders where the user put it.
* @param {boolean} enable True if auto-layout should be enabled, false
* otherwise.
* @package
*/
Blockly.Bubble.prototype.setAutoLayout = function(enable) {
this.autoLayout_ = enable;
};

View File

@@ -0,0 +1,285 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Methods for dragging a bubble visually.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.BubbleDragger');
goog.require('Blockly.Bubble');
goog.require('Blockly.Events.CommentMove');
goog.require('Blockly.WorkspaceCommentSvg');
goog.require('goog.math.Coordinate');
goog.require('goog.asserts');
/**
* Class for a bubble dragger. It moves things on the bubble canvas around the
* workspace when they are being dragged by a mouse or touch. These can be
* block comments, mutators, warnings, or workspace comments.
* @param {!Blockly.Bubble|!Blockly.WorkspaceCommentSvg} bubble The item on the
* bubble canvas to drag.
* @param {!Blockly.WorkspaceSvg} workspace The workspace to drag on.
* @constructor
*/
Blockly.BubbleDragger = function(bubble, workspace) {
/**
* The item on the bubble canvas that is being dragged.
* @type {!Blockly.Bubble|!Blockly.WorkspaceCommentSvg}
* @private
*/
this.draggingBubble_ = bubble;
/**
* The workspace on which the bubble is being dragged.
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* Which delete area the mouse pointer is over, if any.
* One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @type {?number}
* @private
*/
this.deleteArea_ = null;
/**
* Whether the bubble would be deleted if dropped immediately.
* @type {boolean}
* @private
*/
this.wouldDeleteBubble_ = false;
/**
* The location of the top left corner of the dragging bubble's body at the
* beginning of the drag, in workspace coordinates.
* @type {!goog.math.Coordinate}
* @private
*/
this.startXY_ = this.draggingBubble_.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.
* @type {?Blockly.BlockDragSurfaceSvg}
* @private
*/
this.dragSurface_ =
Blockly.utils.is3dSupported() && !!workspace.getBlockDragSurface() ?
workspace.getBlockDragSurface() : null;
};
/**
* Sever all links from this object.
* @package
*/
Blockly.BubbleDragger.prototype.dispose = function() {
this.draggingBubble_ = null;
this.workspace_ = null;
this.dragSurface_ = null;
};
/**
* Start dragging a bubble. This includes moving it to the drag surface.
* @package
*/
Blockly.BubbleDragger.prototype.startBubbleDrag = function() {
if (!Blockly.Events.getGroup()) {
Blockly.Events.setGroup(true);
}
this.workspace_.setResizesEnabled(false);
this.draggingBubble_.setAutoLayout(false);
if (this.dragSurface_) {
this.moveToDragSurface_();
}
this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(true);
var toolbox = this.workspace_.getToolbox();
if (toolbox) {
var style = this.draggingBubble_.isDeletable() ? 'blocklyToolboxDelete' :
'blocklyToolboxGrab';
toolbox.addStyle(style);
}
};
/**
* Execute a step of bubble dragging, based on the given event. Update the
* display accordingly.
* @param {!Event} e The most recent move event.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
*/
Blockly.BubbleDragger.prototype.dragBubble = function(e, currentDragDeltaXY) {
var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
var newLoc = goog.math.Coordinate.sum(this.startXY_, delta);
this.draggingBubble_.moveDuringDrag(this.dragSurface_, newLoc);
if (this.draggingBubble_.isDeletable()) {
this.deleteArea_ = this.workspace_.isDeleteArea(e);
this.updateCursorDuringBubbleDrag_();
}
};
/**
* Shut the trash can and, if necessary, delete the dragging bubble.
* Should be called at the end of a bubble drag.
* @return {boolean} whether the bubble was deleted.
* @private
*/
Blockly.BubbleDragger.prototype.maybeDeleteBubble_ = function() {
var trashcan = this.workspace_.trashcan;
if (this.wouldDeleteBubble_) {
if (trashcan) {
setTimeout(trashcan.close.bind(trashcan), 100);
}
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
this.draggingBubble_.dispose(false, true);
} else if (trashcan) {
// Make sure the trash can is closed.
trashcan.close();
}
return this.wouldDeleteBubble_;
};
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging bubble would be deleted if released immediately.
* @private
*/
Blockly.BubbleDragger.prototype.updateCursorDuringBubbleDrag_ = function() {
this.wouldDeleteBubble_ = this.deleteArea_ != Blockly.DELETE_AREA_NONE;
var trashcan = this.workspace_.trashcan;
if (this.wouldDeleteBubble_) {
this.draggingBubble_.setDeleteStyle(true);
if (this.deleteArea_ == Blockly.DELETE_AREA_TRASH && trashcan) {
trashcan.setOpen_(true);
}
} else {
this.draggingBubble_.setDeleteStyle(false);
if (trashcan) {
trashcan.setOpen_(false);
}
}
};
/**
* Finish a bubble drag and put the bubble back on the workspace.
* @param {!Event} e The mouseup/touchend event.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
*/
Blockly.BubbleDragger.prototype.endBubbleDrag = function(
e, currentDragDeltaXY) {
// Make sure internal state is fresh.
this.dragBubble(e, currentDragDeltaXY);
var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
var newLoc = goog.math.Coordinate.sum(this.startXY_, delta);
// Move the bubble to its final location.
this.draggingBubble_.moveTo(newLoc.x, newLoc.y);
var deleted = this.maybeDeleteBubble_();
if (!deleted) {
// Put everything back onto the bubble canvas.
if (this.dragSurface_) {
this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas());
}
this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(false);
this.fireMoveEvent_();
}
this.workspace_.setResizesEnabled(true);
if (this.workspace_.toolbox_) {
var style = this.draggingBubble_.isDeletable() ? 'blocklyToolboxDelete' :
'blocklyToolboxGrab';
this.workspace_.toolbox_.removeStyle(style);
}
Blockly.Events.setGroup(false);
};
/**
* Fire a move event at the end of a bubble drag.
* @private
*/
Blockly.BubbleDragger.prototype.fireMoveEvent_ = function() {
var event = null;
if (this.draggingBubble_.isComment) {
event = new Blockly.Events.CommentMove(this.draggingBubble_);
} else if (this.draggingBubble_ instanceof Blockly.ScratchBubble) {
event = new Blockly.Events.CommentMove(this.draggingBubble_.comment);
} else {
return;
}
event.setOldCoordinate(this.startXY_);
event.recordNew();
Blockly.Events.fire(event);
};
/**
* Convert a coordinate object from pixels to workspace units, including a
* correction for mutator workspaces.
* This function does not consider differing origins. It simply scales the
* input's x and y values.
* @param {!goog.math.Coordinate} pixelCoord A coordinate with x and y values
* in css pixel units.
* @return {!goog.math.Coordinate} The input coordinate divided by the workspace
* scale.
* @private
*/
Blockly.BubbleDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) {
var result = new goog.math.Coordinate(pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale);
if (this.workspace_.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same as
// the scale on the parent workspace.
// Fix that for dragging.
var mainScale = this.workspace_.options.parentWorkspace.scale;
result = result.scale(1 / mainScale);
}
return result;
};
/**
* Move the bubble onto the drag surface at the beginning of a drag. Move the
* drag surface to preserve the apparent location of the bubble.
* @private
*/
Blockly.BubbleDragger.prototype.moveToDragSurface_ = function() {
this.draggingBubble_.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.draggingBubble_.getSvgRoot());
};

View File

@@ -0,0 +1,177 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Massachusetts Institute of Technology
* All rights reserved.
*
* 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.
*/
'use strict';
goog.provide('Blockly.Colours');
Blockly.Colours = {
// SVG colours: these must be specificed in #RRGGBB style
// To add an opacity, this must be specified as a separate property (for SVG fill-opacity)
"motion": {
"primary": "#4C97FF",
"secondary": "#4280D7",
"tertiary": "#3373CC",
"quaternary": "#3373CC"
},
"looks": {
"primary": "#9966FF",
"secondary": "#855CD6",
"tertiary": "#774DCB",
"quaternary": "#774DCB"
},
"sounds": {
"primary": "#CF63CF",
"secondary": "#C94FC9",
"tertiary": "#BD42BD",
"quaternary": "#BD42BD"
},
"control": {
"primary": "#FFAB19",
"secondary": "#EC9C13",
"tertiary": "#CF8B17",
"quaternary": "#CF8B17"
},
"event": {
"primary": "#FFBF00",
"secondary": "#E6AC00",
"tertiary": "#CC9900",
"quaternary": "#CC9900"
},
"sensing": {
"primary": "#5CB1D6",
"secondary": "#47A8D1",
"tertiary": "#2E8EB8",
"quaternary": "#2E8EB8"
},
"pen": {
"primary": "#0fBD8C",
"secondary": "#0DA57A",
"tertiary": "#0B8E69",
"quaternary": "#0B8E69"
},
"operators": {
"primary": "#59C059",
"secondary": "#46B946",
"tertiary": "#389438",
"quaternary": "#389438"
},
"data": {
"primary": "#FF8C1A",
"secondary": "#FF8000",
"tertiary": "#DB6E00",
"quaternary": "#DB6E00"
},
// This is not a new category, but rather for differentiation
// between lists and scalar variables.
"data_lists": {
"primary": "#FF661A",
"secondary": "#FF5500",
"tertiary": "#E64D00",
"quaternary": "#E64D00"
},
"more": {
"primary": "#FF6680",
"secondary": "#FF4D6A",
"tertiary": "#FF3355",
"quaternary": "#FF3355"
},
"text": "#FFFFFF",
"workspace": "#F9F9F9",
"toolboxHover": "#4C97FF",
"toolboxSelected": "#e9eef2",
"toolboxText": "#575E75",
"blackText": "#575E75",
"toolbox": "#FFFFFF",
"flyout": "#F9F9F9",
"scrollbar": "#CECDCE",
"scrollbarHover": '#CECDCE',
"textField": "#FFFFFF",
"textFieldText": "#575E75",
"insertionMarker": "#000000",
"insertionMarkerOpacity": 0.2,
"dragShadowOpacity": 0.3,
"stackGlow": "#FFF200",
"stackGlowSize": 4,
"stackGlowOpacity": 1,
"replacementGlow": "#FFFFFF",
"replacementGlowSize": 2,
"replacementGlowOpacity": 1,
"colourPickerStroke": "#FFFFFF",
// CSS colours: support RGBA
"fieldShadow": "rgba(0,0,0,0.1)",
"dropDownShadow": "rgba(0, 0, 0, .3)",
"numPadBackground": "#547AB2",
"numPadBorder": "#435F91",
"numPadActiveBackground": "#435F91",
"numPadText": "white", // Do not use hex here, it cannot be inlined with data-uri SVG
"valueReportBackground": "#FFFFFF",
"valueReportBorder": "#AAAAAA",
"valueReportForeground": "#000000",
"menuHover": "rgba(0, 0, 0, 0.2)",
"contextMenuBackground": "#ffffff",
"contextMenuBorder": "#cccccc",
"contextMenuForeground": "#000000",
"contextMenuActiveBackground": "#d6e9f8",
"contextMenuDisabledForeground": "#cccccc",
"flyoutLabelColor": "#575E75",
"checkboxInactiveBackground": "#ffffff",
"checkboxInactiveBorder": "#c8c8c8",
"checkboxActiveBackground": "#4C97FF",
"checkboxActiveBorder": "#3373CC",
"checkboxCheck": "#ffffff",
"buttonActiveBackground": "#ffffff",
"buttonForeground": "#575E75",
"buttonBorder": "#c6c6c6",
"zoomIconFilter": "none"
};
/**
* Override the colours in Blockly.Colours with new values basded on the
* given dictionary.
* @param {!Object} colours Dictionary of colour properties and new values.
* @package
*/
Blockly.Colours.overrideColours = function(colours) {
// Colour overrides provided by the injection
if (colours) {
for (var colourProperty in colours) {
if (colours.hasOwnProperty(colourProperty) &&
Blockly.Colours.hasOwnProperty(colourProperty)) {
// If a property is in both colours option and Blockly.Colours,
// set the Blockly.Colours value to the override.
// Override Blockly category color object properties with those
// provided.
var colourPropertyValue = colours[colourProperty];
if (goog.isObject(colourPropertyValue)) {
for (var colourSequence in colourPropertyValue) {
if (colourPropertyValue.hasOwnProperty(colourSequence) &&
Blockly.Colours[colourProperty].hasOwnProperty(colourSequence)) {
Blockly.Colours[colourProperty][colourSequence] =
colourPropertyValue[colourSequence];
}
}
} else {
Blockly.Colours[colourProperty] = colourPropertyValue;
}
}
}
}
};

View File

@@ -0,0 +1,293 @@
/**
* @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 Object representing a code comment.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Comment');
goog.require('Blockly.Bubble');
goog.require('Blockly.Events.BlockChange');
goog.require('Blockly.Events.Ui');
goog.require('Blockly.Icon');
goog.require('goog.userAgent');
/**
* Class for a comment.
* @param {!Blockly.Block} block The block associated with this comment.
* @extends {Blockly.Icon}
* @constructor
*/
Blockly.Comment = function(block) {
Blockly.Comment.superClass_.constructor.call(this, block);
this.createIcon();
};
goog.inherits(Blockly.Comment, Blockly.Icon);
/**
* Comment text (if bubble is not visible).
* @private
*/
Blockly.Comment.prototype.text_ = '';
/**
* Width of bubble.
* @private
*/
Blockly.Comment.prototype.width_ = 160;
/**
* Height of bubble.
* @private
*/
Blockly.Comment.prototype.height_ = 80;
/**
* Draw the comment icon.
* @param {!Element} group The icon group.
* @private
*/
Blockly.Comment.prototype.drawIcon_ = function(group) {
// Circle.
Blockly.utils.createSvgElement('circle',
{'class': 'blocklyIconShape', 'r': '8', 'cx': '8', 'cy': '8'},
group);
// Can't use a real '?' text character since different browsers and operating
// systems render it differently.
// Body of question mark.
Blockly.utils.createSvgElement('path',
{
'class': 'blocklyIconSymbol',
'd': 'm6.8,10h2c0.003,-0.617 0.271,-0.962 0.633,-1.266 2.875,-2.405' +
'0.607,-5.534 -3.765,-3.874v1.7c3.12,-1.657 3.698,0.118 2.336,1.25' +
'-1.201,0.998 -1.201,1.528 -1.204,2.19z'
},
group);
// Dot of question mark.
Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyIconSymbol',
'x': '6.8',
'y': '10.78',
'height': '2',
'width': '2'
},
group);
};
/**
* Create the editor for the comment's bubble.
* @return {!Element} The top-level node of the editor.
* @private
*/
Blockly.Comment.prototype.createEditor_ = function() {
/* Create the editor. Here's the markup that will be generated:
<foreignObject x="8" y="8" width="164" height="164">
<body xmlns="http://www.w3.org/1999/xhtml" class="blocklyMinimalBody">
<textarea xmlns="http://www.w3.org/1999/xhtml"
class="blocklyCommentTextarea"
style="height: 164px; width: 164px;"></textarea>
</body>
</foreignObject>
*/
this.foreignObject_ = Blockly.utils.createSvgElement('foreignObject',
{'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH},
null);
var body = document.createElementNS(Blockly.HTML_NS, 'body');
body.setAttribute('xmlns', Blockly.HTML_NS);
body.className = 'blocklyMinimalBody';
var textarea = document.createElementNS(Blockly.HTML_NS, 'textarea');
textarea.className = 'blocklyCommentTextarea';
textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
body.appendChild(textarea);
this.textarea_ = textarea;
this.foreignObject_.appendChild(body);
Blockly.bindEventWithChecks_(textarea, 'mouseup', this, this.textareaFocus_);
// Don't zoom with mousewheel.
Blockly.bindEventWithChecks_(textarea, 'wheel', this, function(e) {
e.stopPropagation();
});
Blockly.bindEventWithChecks_(textarea, 'change', this, function(_e) {
if (this.text_ != textarea.value) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.block_, 'comment', null, this.text_, textarea.value));
this.text_ = textarea.value;
}
});
setTimeout(function() {
textarea.focus();
}, 0);
return this.foreignObject_;
};
/**
* Add or remove editability of the comment.
* @override
*/
Blockly.Comment.prototype.updateEditable = function() {
if (this.isVisible()) {
// Toggling visibility will force a rerendering.
this.setVisible(false);
this.setVisible(true);
}
// Allow the icon to update.
Blockly.Icon.prototype.updateEditable.call(this);
};
/**
* Callback function triggered when the bubble has resized.
* Resize the text area accordingly.
* @private
*/
Blockly.Comment.prototype.resizeBubble_ = function() {
if (this.isVisible()) {
var size = this.bubble_.getBubbleSize();
var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
this.foreignObject_.setAttribute('width', size.width - doubleBorderWidth);
this.foreignObject_.setAttribute('height', size.height - doubleBorderWidth);
this.textarea_.style.width = (size.width - doubleBorderWidth - 4) + 'px';
this.textarea_.style.height = (size.height - doubleBorderWidth - 4) + 'px';
}
};
/**
* Show or hide the comment bubble.
* @param {boolean} visible True if the bubble should be visible.
*/
Blockly.Comment.prototype.setVisible = function(visible) {
if (visible == this.isVisible()) {
// No change.
return;
}
Blockly.Events.fire(
new Blockly.Events.Ui(this.block_, 'commentOpen', !visible, visible));
if ((!this.block_.isEditable() && !this.textarea_) || goog.userAgent.IE) {
// Steal the code from warnings to make an uneditable text bubble.
// MSIE does not support foreignobject; textareas are impossible.
// http://msdn.microsoft.com/en-us/library/hh834675%28v=vs.85%29.aspx
// Always treat comments in IE as uneditable.
Blockly.Warning.prototype.setVisible.call(this, visible);
return;
}
// Save the bubble stats before the visibility switch.
var text = this.getText();
var size = this.getBubbleSize();
if (visible) {
// Create the bubble.
this.bubble_ = new Blockly.Bubble(
/** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
this.createEditor_(), this.block_.svgPath_,
this.iconXY_, this.width_, this.height_);
// Expose this comment's block's ID on its top-level SVG group.
this.bubble_.setSvgId(this.block_.id);
this.bubble_.registerResizeEvent(this.resizeBubble_.bind(this));
this.updateColour();
} else {
// Dispose of the bubble.
this.bubble_.dispose();
this.bubble_ = null;
this.textarea_ = null;
this.foreignObject_ = null;
}
// Restore the bubble stats after the visibility switch.
this.setText(text);
this.setBubbleSize(size.width, size.height);
};
/**
* Bring the comment to the top of the stack when clicked on.
* @param {!Event} _e Mouse up event.
* @private
*/
Blockly.Comment.prototype.textareaFocus_ = function(_e) {
// Ideally this would be hooked to the focus event for the comment.
// This is tied to mousedown, however doing so in Firefox swallows the cursor
// for unknown reasons.
// See https://github.com/LLK/scratch-blocks/issues/1631 for more history.
if (this.bubble_.promote_()) {
// Since the act of moving this node within the DOM causes a loss of focus,
// we need to reapply the focus.
this.textarea_.focus();
}
};
/**
* Get the dimensions of this comment's bubble.
* @return {!Object} Object with width and height properties.
*/
Blockly.Comment.prototype.getBubbleSize = function() {
if (this.isVisible()) {
return this.bubble_.getBubbleSize();
} else {
return {width: this.width_, height: this.height_};
}
};
/**
* Size this comment's bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
*/
Blockly.Comment.prototype.setBubbleSize = function(width, height) {
if (this.textarea_) {
this.bubble_.setBubbleSize(width, height);
} else {
this.width_ = width;
this.height_ = height;
}
};
/**
* Returns this comment's text.
* @return {string} Comment text.
*/
Blockly.Comment.prototype.getText = function() {
return this.textarea_ ? this.textarea_.value : this.text_;
};
/**
* Set this comment's text.
* @param {string} text Comment text.
*/
Blockly.Comment.prototype.setText = function(text) {
if (this.text_ != text) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.block_, 'comment', null, this.text_, text));
this.text_ = text;
}
if (this.textarea_) {
this.textarea_.value = text;
}
};
/**
* Dispose of this comment.
*/
Blockly.Comment.prototype.dispose = function() {
if (Blockly.Events.isEnabled()) {
this.setText(''); // Fire event to delete comment.
}
this.block_.comment = null;
Blockly.Icon.prototype.dispose.call(this);
};

View File

@@ -0,0 +1,539 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Classes for all comment events.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.Events.CommentBase');
goog.provide('Blockly.Events.CommentChange');
goog.provide('Blockly.Events.CommentCreate');
goog.provide('Blockly.Events.CommentDelete');
goog.provide('Blockly.Events.CommentMove');
goog.require('Blockly.Events');
goog.require('Blockly.Events.Abstract');
goog.require('goog.math.Coordinate');
/**
* Abstract class for a comment event.
* @param {Blockly.WorkspaceComment | Blockly.ScratchBlockComment} comment
* The comment this event corresponds to.
* @extends {Blockly.Events.Abstract}
* @constructor
*/
Blockly.Events.CommentBase = function(comment) {
/**
* The ID of the comment this event pertains to.
* @type {string}
*/
this.commentId = comment.id;
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = comment.workspace.id;
/**
* The ID of the block this comment belongs to or null if it is not a block
* comment.
* @type {string}
*/
this.blockId = comment.blockId || null;
/**
* The event group id for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = Blockly.Events.group_;
/**
* Sets whether the event should be added to the undo stack.
* @type {boolean}
*/
this.recordUndo = Blockly.Events.recordUndo;
};
goog.inherits(Blockly.Events.CommentBase, Blockly.Events.Abstract);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentBase.prototype.toJson = function() {
var json = {
'type': this.type
};
if (this.group) {
json['group'] = this.group;
}
if (this.commentId) {
json['commentId'] = this.commentId;
}
if (this.blockId) {
json['blockId'] = this.blockId;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentBase.prototype.fromJson = function(json) {
this.commentId = json['commentId'];
this.group = json['group'];
this.blockId = json['blockId'];
};
/**
* Helper function for finding the comment this event pertains to.
* @return {?(Blockly.WorkspaceComment | Blockly.ScratchBlockComment)}
* The comment this event pertains to, or null if it no longer exists.
* @private
*/
Blockly.Events.CommentBase.prototype.getComment_ = function() {
var workspace = this.getEventWorkspace_();
return workspace.getCommentById(this.commentId);
};
/**
* Class for a comment change event.
* @param {Blockly.WorkspaceComment | Blockly.ScratchBlockComment} comment
* The comment that is being changed. Null for a blank event.
* @param {!object} oldContents Object containing previous state of a comment's
* properties. The possible properties can be: 'minimized', 'text', or
* 'width' and 'height' together. Must contain the same property (or in the
* case of 'width' and 'height' properties) as the 'newContents' param.
* @param {!object} newContents Object containing the new state of a comment's
* properties. The possible properties can be: 'minimized', 'text', or
* 'width' and 'height' together. Must contain the same property (or in the
* case of 'width' and 'height' properties) as the 'oldContents' param.
* @extends {Blockly.Events.CommentBase}
* @constructor
*/
Blockly.Events.CommentChange = function(comment, oldContents, newContents) {
if (!comment) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.CommentChange.superClass_.constructor.call(this, comment);
this.oldContents_ = oldContents;
this.newContents_ = newContents;
};
goog.inherits(Blockly.Events.CommentChange, Blockly.Events.CommentBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.CommentChange.prototype.type = Blockly.Events.COMMENT_CHANGE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentChange.prototype.toJson = function() {
var json = Blockly.Events.CommentChange.superClass_.toJson.call(this);
json['newContents'] = this.newContents_;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentChange.prototype.fromJson = function(json) {
Blockly.Events.CommentChange.superClass_.fromJson.call(this, json);
this.newContents_ = json['newValue'];
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
Blockly.Events.CommentChange.prototype.isNull = function() {
return this.oldContents_ == this.newContents_;
};
/**
* Run a change event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.CommentChange.prototype.run = function(forward) {
var comment = this.getComment_();
if (!comment) {
console.warn('Can\'t change non-existent comment: ' + this.commentId);
return;
}
var contents = forward ? this.newContents_ : this.oldContents_;
if (contents.hasOwnProperty('minimized')) {
comment.setMinimized(contents.minimized);
}
if (contents.hasOwnProperty('width') && contents.hasOwnProperty('height')) {
comment.setSize(contents.width, contents.height);
}
if (contents.hasOwnProperty('text')) {
comment.setText(contents.text);
}
};
/**
* Class for a comment creation event.
* @param {Blockly.WorkspaceComment | Blockly.ScratchBlockComment} comment
* The created comment. Null for a blank event.
* @param {string=} opt_blockId Optional id for the block this comment belongs
* to, if it is a block comment.
* @extends {Blockly.Events.CommentBase}
* @constructor
*/
Blockly.Events.CommentCreate = function(comment) {
if (!comment) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.CommentCreate.superClass_.constructor.call(this, comment);
/**
* The text content of this comment.
* @type {string}
*/
this.text = comment.getText();
/**
* The XY position of this comment on the workspace.
* @type {goog.math.Coordinate}
*/
this.xy = comment.getXY();
var hw = comment.getHeightWidth();
/**
* The width of this comment when it is full size.
* @type {number}
*/
this.width = hw.width;
/**
* The height of this comment when it is full size.
* @type {number}
*/
this.height = hw.height;
/**
* Whether or not this comment is minimized.
* @type {boolean}
*/
this.minimized = comment.isMinimized() || false;
this.xml = comment.toXmlWithXY();
};
goog.inherits(Blockly.Events.CommentCreate, Blockly.Events.CommentBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.CommentCreate.prototype.type = Blockly.Events.COMMENT_CREATE;
/**
* Encode the event as JSON.
* TODO (github.com/google/blockly/issues/1266): "Full" and "minimal"
* serialization.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentCreate.prototype.toJson = function() {
var json = Blockly.Events.CommentCreate.superClass_.toJson.call(this);
json['xml'] = Blockly.Xml.domToText(this.xml);
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentCreate.prototype.fromJson = function(json) {
Blockly.Events.CommentCreate.superClass_.fromJson.call(this, json);
this.xml = Blockly.Xml.textToDom('<xml>' + json['xml'] + '</xml>').firstChild;
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.CommentCreate.prototype.run = function(forward) {
if (forward) {
var workspace = this.getEventWorkspace_();
if (this.blockId) {
var block = workspace.getBlockById(this.blockId);
if (block) {
block.setCommentText('', this.commentId, this.xy.x, this.xy.y, this.minimized);
}
} else {
var xml = goog.dom.createDom('xml');
xml.appendChild(this.xml);
Blockly.Xml.domToWorkspace(xml, workspace);
}
} else {
var comment = this.getComment_();
if (comment) {
comment.dispose(false, false);
} else {
// Only complain about root-level block.
console.warn("Can't uncreate non-existent comment: " + this.commentId);
}
}
};
/**
* Class for a comment deletion event.
* @param {Blockly.WorkspaceComment | Blockly.ScratchBlockComment} comment
* The deleted comment. Null for a blank event.
* @extends {Blockly.Events.CommentBase}
* @constructor
*/
Blockly.Events.CommentDelete = function(comment) {
if (!comment) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.CommentDelete.superClass_.constructor.call(this, comment);
this.xy = comment.getXY();
this.minimized = comment.isMinimized() || false;
this.text = comment.getText();
var hw = comment.getHeightWidth();
this.height = hw.height;
this.width = hw.width;
this.xml = comment.toXmlWithXY();
};
goog.inherits(Blockly.Events.CommentDelete, Blockly.Events.CommentBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.CommentDelete.prototype.type = Blockly.Events.COMMENT_DELETE;
/**
* Encode the event as JSON.
* TODO (github.com/google/blockly/issues/1266): "Full" and "minimal"
* serialization.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentDelete.prototype.toJson = function() {
var json = Blockly.Events.CommentDelete.superClass_.toJson.call(this);
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentDelete.prototype.fromJson = function(json) {
Blockly.Events.CommentDelete.superClass_.fromJson.call(this, json);
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.CommentDelete.prototype.run = function(forward) {
if (forward) {
var comment = this.getComment_();
if (comment) {
comment.dispose(false, false);
} else {
// Only complain about root-level block.
console.warn("Can't delete non-existent comment: " + this.commentId);
}
} else {
var workspace = this.getEventWorkspace_();
if (this.blockId) {
var block = workspace.getBlockById(this.blockId);
block.setCommentText(this.text, this.commentId, this.xy.x, this.xy.y, this.minimized);
block.comment.setSize(this.width, this.height);
} else {
var xml = goog.dom.createDom('xml');
xml.appendChild(this.xml);
Blockly.Xml.domToWorkspace(xml, workspace);
}
}
};
/**
* Class for a comment move event. Created before the move.
* @param {Blockly.WorkspaceComment | Blockly.ScratchBlockComment} comment
* The comment that is being moved. Null for a blank event.
* @extends {Blockly.Events.CommentBase}
* @constructor
*/
Blockly.Events.CommentMove = function(comment) {
if (!comment) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.CommentMove.superClass_.constructor.call(this, comment);
/**
* The comment that is being moved. Will be cleared after recording the new
* location.
* @type {?Blockly.WorkspaceComment | Blockly.ScratchBlockComment}
*/
this.comment_ = comment;
this.workspaceWidth_ = comment.workspace.getWidth();
/**
* The location before the move, in workspace coordinates.
* @type {!goog.math.Coordinate}
*/
this.oldCoordinate_ = this.currentLocation_();
/**
* The location after the move, in workspace coordinates.
* @type {!goog.math.Coordinate}
*/
this.newCoordinate_ = null;
};
goog.inherits(Blockly.Events.CommentMove, Blockly.Events.CommentBase);
/**
* Calculate the current, language agnostic location of the comment.
* This value should not report different numbers in LTR vs. RTL.
* @return {goog.math.Coordinate} The location of the comment.
* @private
*/
Blockly.Events.CommentMove.prototype.currentLocation_ = function() {
var xy = this.comment_.getXY();
if (!this.comment_.workspace.RTL) {
return xy;
}
var rtlAwareX;
if (this.comment_ instanceof Blockly.ScratchBlockComment) {
var commentWidth = this.comment_.getBubbleSize().width;
rtlAwareX = this.workspaceWidth_ - xy.x - commentWidth;
} else {
rtlAwareX = this.workspaceWidth_ - xy.x;
}
return new goog.math.Coordinate(rtlAwareX, xy.y);
};
/**
* Record the comment's new location. Called after the move. Can only be
* called once.
*/
Blockly.Events.CommentMove.prototype.recordNew = function() {
if (!this.comment_) {
throw new Error('Tried to record the new position of a comment on the ' +
'same event twice.');
}
this.newCoordinate_ = this.currentLocation_();
this.comment_ = null;
};
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.CommentMove.prototype.type = Blockly.Events.COMMENT_MOVE;
/**
* Override the location before the move. Use this if you don't create the
* event until the end of the move, but you know the original location.
* @param {!goog.math.Coordinate} xy The location before the move, in workspace
* coordinates.
*/
Blockly.Events.CommentMove.prototype.setOldCoordinate = function(xy) {
this.oldCoordinate_ = new goog.math.Coordinate(this.comment_.workspace.RTL ?
this.workspaceWidth_ - xy.x : xy.x, xy.y);
};
/**
* Encode the event as JSON.
* TODO (github.com/google/blockly/issues/1266): "Full" and "minimal"
* serialization.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentMove.prototype.toJson = function() {
var json = Blockly.Events.CommentMove.superClass_.toJson.call(this);
if (this.newCoordinate_) {
json['newCoordinate'] = Math.round(this.newCoordinate_.x) + ',' +
Math.round(this.newCoordinate_.y);
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentMove.prototype.fromJson = function(json) {
Blockly.Events.CommentMove.superClass_.fromJson.call(this, json);
if (json['newCoordinate']) {
var xy = json['newCoordinate'].split(',');
this.newCoordinate_ =
new goog.math.Coordinate(parseFloat(xy[0]), parseFloat(xy[1]));
}
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
Blockly.Events.CommentMove.prototype.isNull = function() {
return goog.math.Coordinate.equals(this.oldCoordinate_, this.newCoordinate_);
};
/**
* Run a move event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.CommentMove.prototype.run = function(forward) {
var comment = this.getComment_();
if (!comment) {
console.warn('Can\'t move non-existent comment: ' + this.commentId);
return;
}
var target = forward ? this.newCoordinate_ : this.oldCoordinate_;
if (comment instanceof Blockly.ScratchBlockComment) {
if (comment.workspace.RTL) {
comment.moveTo(this.workspaceWidth_ - target.x, target.y);
} else {
comment.moveTo(target.x, target.y);
}
} else {
// TODO: Check if the comment is being dragged, and give up if so.
var current = comment.getXY();
if (comment.workspace.RTL) {
var deltaX = target.x - (this.workspaceWidth_ - current.x);
comment.moveBy(-deltaX, target.y - current.y);
} else {
comment.moveBy(target.x - current.x, target.y - current.y);
}
}
};

View File

@@ -0,0 +1,776 @@
/**
* @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 Components for creating connections between blocks.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Connection');
goog.require('Blockly.Events.BlockMove');
goog.require('goog.asserts');
goog.require('goog.dom');
/**
* Class for a connection between blocks.
* @param {!Blockly.Block} source The block establishing this connection.
* @param {number} type The type of the connection.
* @constructor
*/
Blockly.Connection = function(source, type) {
/**
* @type {!Blockly.Block}
* @protected
*/
this.sourceBlock_ = source;
/** @type {number} */
this.type = type;
// Shortcut for the databases for this connection's workspace.
if (source.workspace.connectionDBList) {
this.db_ = source.workspace.connectionDBList[type];
this.dbOpposite_ =
source.workspace.connectionDBList[Blockly.OPPOSITE_TYPE[type]];
this.hidden_ = !this.db_;
}
};
/**
* Constants for checking whether two connections are compatible.
*/
Blockly.Connection.CAN_CONNECT = 0;
Blockly.Connection.REASON_SELF_CONNECTION = 1;
Blockly.Connection.REASON_WRONG_TYPE = 2;
Blockly.Connection.REASON_TARGET_NULL = 3;
Blockly.Connection.REASON_CHECKS_FAILED = 4;
Blockly.Connection.REASON_DIFFERENT_WORKSPACES = 5;
Blockly.Connection.REASON_SHADOW_PARENT = 6;
// Fixes #1127, but may be the wrong solution.
Blockly.Connection.REASON_CUSTOM_PROCEDURE = 7;
/**
* Connection this connection connects to. Null if not connected.
* @type {Blockly.Connection}
*/
Blockly.Connection.prototype.targetConnection = null;
/**
* List of compatible value types. Null if all types are compatible.
* @type {Array}
* @private
*/
Blockly.Connection.prototype.check_ = null;
/**
* DOM representation of a shadow block, or null if none.
* @type {Element}
* @private
*/
Blockly.Connection.prototype.shadowDom_ = null;
/**
* Horizontal location of this connection.
* @type {number}
* @protected
*/
Blockly.Connection.prototype.x_ = 0;
/**
* Vertical location of this connection.
* @type {number}
* @protected
*/
Blockly.Connection.prototype.y_ = 0;
/**
* Has this connection been added to the connection database?
* @type {boolean}
* @protected
*/
Blockly.Connection.prototype.inDB_ = false;
/**
* Connection database for connections of this type on the current workspace.
* @type {Blockly.ConnectionDB}
* @protected
*/
Blockly.Connection.prototype.db_ = null;
/**
* Connection database for connections compatible with this type on the
* current workspace.
* @type {Blockly.ConnectionDB}
* @protected
*/
Blockly.Connection.prototype.dbOpposite_ = null;
/**
* Whether this connections is hidden (not tracked in a database) or not.
* @type {boolean}
* @protected
*/
Blockly.Connection.prototype.hidden_ = null;
/**
* Connect two connections together. This is the connection on the superior
* block.
* @param {!Blockly.Connection} childConnection Connection on inferior block.
* @protected
*/
Blockly.Connection.prototype.connect_ = function(childConnection) {
var parentConnection = this;
var parentBlock = parentConnection.getSourceBlock();
var childBlock = childConnection.getSourceBlock();
var isSurroundingC = false;
if (parentConnection == parentBlock.getFirstStatementConnection()) {
isSurroundingC = true;
}
if (Blockly.Events.isEnabled() && !childBlock.isInsertionMarker()) {
childBlock.workspace.procedureReturnsWillChange();
}
// Disconnect any existing parent on the child connection.
if (childConnection.isConnected()) {
// Scratch-specific behaviour:
// If we're using a c-shaped block to surround a stack, remember where the
// stack used to be connected.
if (isSurroundingC) {
var previousParentConnection = childConnection.targetConnection;
}
childConnection.disconnect();
}
if (parentConnection.isConnected()) {
// Other connection is already connected to something.
// Disconnect it and reattach it or bump it as needed.
var orphanBlock = parentConnection.targetBlock();
var shadowDom = parentConnection.getShadowDom();
// Temporarily set the shadow DOM to null so it does not respawn.
parentConnection.setShadowDom(null);
// Displaced shadow blocks dissolve rather than reattaching or bumping.
if (orphanBlock.isShadow()) {
// Save the shadow block so that field values are preserved.
shadowDom = Blockly.Xml.blockToDom(orphanBlock);
orphanBlock.dispose();
orphanBlock = null;
} else if (parentConnection.type == Blockly.NEXT_STATEMENT) {
// Statement connections.
// Statement blocks may be inserted into the middle of a stack.
// Split the stack.
if (!orphanBlock.previousConnection) {
throw 'Orphan block does not have a previous connection.';
}
// Attempt to reattach the orphan at the bottom of the newly inserted
// block. Since this block may be a stack, walk down to the end.
var newBlock = childBlock;
while (newBlock.nextConnection) {
var nextBlock = newBlock.getNextBlock();
if (nextBlock && !nextBlock.isShadow()) {
newBlock = nextBlock;
} else {
if (orphanBlock.previousConnection.checkType_(
newBlock.nextConnection)) {
newBlock.nextConnection.connect(orphanBlock.previousConnection);
orphanBlock = null;
}
break;
}
}
}
if (orphanBlock) {
// Unable to reattach orphan.
parentConnection.disconnect();
if (Blockly.Events.recordUndo) {
// Bump it off to the side after a moment.
var group = Blockly.Events.getGroup();
setTimeout(function() {
// Verify orphan hasn't been deleted or reconnected (user on meth).
if (orphanBlock.workspace && !orphanBlock.getParent()) {
Blockly.Events.setGroup(group);
if (orphanBlock.outputConnection) {
orphanBlock.outputConnection.bumpAwayFrom_(parentConnection);
} else if (orphanBlock.previousConnection) {
orphanBlock.previousConnection.bumpAwayFrom_(parentConnection);
}
Blockly.Events.setGroup(false);
}
}, Blockly.BUMP_DELAY);
}
}
// Restore the shadow DOM.
parentConnection.setShadowDom(shadowDom);
}
if (isSurroundingC && previousParentConnection) {
previousParentConnection.connect(parentBlock.previousConnection);
}
var event;
if (Blockly.Events.isEnabled()) {
event = new Blockly.Events.BlockMove(childBlock);
}
// Establish the connections.
Blockly.Connection.connectReciprocally_(parentConnection, childConnection);
// Demote the inferior block so that one is a child of the superior one.
childBlock.setParent(parentBlock);
if (event) {
event.recordNew();
Blockly.Events.fire(event);
}
};
/**
* Sever all links to this connection (not including from the source object).
*/
Blockly.Connection.prototype.dispose = function() {
if (this.isConnected()) {
throw 'Disconnect connection before disposing of it.';
}
if (this.inDB_) {
this.db_.removeConnection_(this);
}
this.db_ = null;
this.dbOpposite_ = null;
};
/**
* @return {boolean} true if the connection is not connected or is connected to
* an insertion marker, false otherwise.
*/
Blockly.Connection.prototype.isConnectedToNonInsertionMarker = function() {
return this.targetConnection && !this.targetBlock().isInsertionMarker();
};
/**
* Get the source block for this connection.
* @return {Blockly.Block} The source block, or null if there is none.
*/
Blockly.Connection.prototype.getSourceBlock = function() {
return this.sourceBlock_;
};
/**
* Does the connection belong to a superior block (higher in the source stack)?
* @return {boolean} True if connection faces down or right.
*/
Blockly.Connection.prototype.isSuperior = function() {
return this.type == Blockly.INPUT_VALUE ||
this.type == Blockly.NEXT_STATEMENT;
};
/**
* Is the connection connected?
* @return {boolean} True if connection is connected to another connection.
*/
Blockly.Connection.prototype.isConnected = function() {
return !!this.targetConnection;
};
/**
* Checks whether the current connection can connect with the target
* connection.
* @param {Blockly.Connection} target Connection to check compatibility with.
* @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal,
* an error code otherwise.
* @private
*/
Blockly.Connection.prototype.canConnectWithReason_ = function(target) {
if (!target) {
return Blockly.Connection.REASON_TARGET_NULL;
}
if (this.isSuperior()) {
var blockA = this.sourceBlock_;
var blockB = target.getSourceBlock();
var superiorConn = this;
} else {
var blockB = this.sourceBlock_;
var blockA = target.getSourceBlock();
var superiorConn = target;
}
if (blockA && blockA == blockB) {
return Blockly.Connection.REASON_SELF_CONNECTION;
} else if (target.type != Blockly.OPPOSITE_TYPE[this.type]) {
return Blockly.Connection.REASON_WRONG_TYPE;
} else if (blockA && blockB && blockA.workspace !== blockB.workspace) {
return Blockly.Connection.REASON_DIFFERENT_WORKSPACES;
} else if (!this.checkType_(target)) {
return Blockly.Connection.REASON_CHECKS_FAILED;
} else if (blockA.isShadow() && !blockB.isShadow()) {
return Blockly.Connection.REASON_SHADOW_PARENT;
} else if ((blockA.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE &&
blockB.type != Blockly.PROCEDURES_PROTOTYPE_BLOCK_TYPE &&
superiorConn == blockA.getInput('custom_block').connection) ||
(blockB.type == Blockly.PROCEDURES_PROTOTYPE_BLOCK_TYPE &&
blockA.type != Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE)) {
// Hack to fix #1127: Fail attempts to connect to the custom_block input
// on a defnoreturn block, unless the connecting block is a specific type.
// And hack to fix #1534: Fail attempts to connect anything but a
// defnoreturn block to a prototype block.
return Blockly.Connection.REASON_CUSTOM_PROCEDURE;
}
return Blockly.Connection.CAN_CONNECT;
};
/**
* Checks whether the current connection and target connection are compatible
* and throws an exception if they are not.
* @param {Blockly.Connection} target The connection to check compatibility
* with.
* @private
*/
Blockly.Connection.prototype.checkConnection_ = function(target) {
switch (this.canConnectWithReason_(target)) {
case Blockly.Connection.CAN_CONNECT:
break;
case Blockly.Connection.REASON_SELF_CONNECTION:
throw 'Attempted to connect a block to itself.';
case Blockly.Connection.REASON_DIFFERENT_WORKSPACES:
// Usually this means one block has been deleted.
throw 'Blocks not on same workspace.';
case Blockly.Connection.REASON_WRONG_TYPE:
throw 'Attempt to connect incompatible types.';
case Blockly.Connection.REASON_TARGET_NULL:
throw 'Target connection is null.';
case Blockly.Connection.REASON_CHECKS_FAILED:
var msg = 'Connection checks failed. ';
msg += this + ' expected ' + this.check_ + ', found ' + target.check_;
throw msg;
case Blockly.Connection.REASON_SHADOW_PARENT:
throw 'Connecting non-shadow to shadow block.';
case Blockly.Connection.REASON_CUSTOM_PROCEDURE:
throw 'Trying to replace a shadow on a custom procedure definition.';
default:
throw 'Unknown connection failure: this should never happen!';
}
};
/**
* Check if the two connections can be dragged to connect to each other.
* This is used by the connection database when searching for the closest
* connection.
* @param {!Blockly.Connection} candidate A nearby connection to check, which
* must be a previous connection.
* @return {boolean} True if the connection is allowed, false otherwise.
*/
Blockly.Connection.prototype.canConnectToPrevious_ = function(candidate) {
if (this.targetConnection) {
// This connection is already occupied.
// A next connection will never disconnect itself mid-drag.
return false;
}
// Don't let blocks try to connect to themselves or ones they nest.
if (Blockly.draggingConnections_.indexOf(candidate) != -1) {
return false;
}
var firstStatementConnection =
this.sourceBlock_.getFirstStatementConnection();
// Is it a C-shaped (e.g. repeat) or E-shaped (e.g. if-else) block?
var isComplexStatement = firstStatementConnection != null;
var isFirstStatementConnection = this == firstStatementConnection;
var isNextConnection = this == this.sourceBlock_.nextConnection;
// Scratch-specific behaviour: can connect to the first statement input of a
// C-shaped or E-shaped block, or to the next connection of any statement
// block, but not to the second statement input of an E-shaped block.
if (isComplexStatement && !isFirstStatementConnection && !isNextConnection) {
return false;
}
// Complex blocks with no previous connection will not be allowed to connect
// mid-stack.
var sourceHasPreviousConn = this.sourceBlock_.previousConnection != null;
if (isFirstStatementConnection && sourceHasPreviousConn) {
return true;
}
if (isNextConnection ||
(isFirstStatementConnection && !sourceHasPreviousConn)) {
// If the candidate is the first connection in a stack, we can connect.
if (!candidate.targetConnection) {
return true;
}
var targetBlock = candidate.targetBlock();
// If it is connected a real block, game over.
if (!targetBlock.isInsertionMarker()) {
return false;
}
// If it's connected to an insertion marker but that insertion marker
// is the first block in a stack, it's still fine. If that insertion
// marker is in the middle of a stack, it won't work.
return !targetBlock.getPreviousBlock();
}
};
/**
* Check if the two connections can be dragged to connect to each other.
* This is used by the connection database when searching for the closest
* connection.
* @param {!Blockly.Connection} candidate A nearby connection to check.
* @return {boolean} True if the connection is allowed, false otherwise.
*/
Blockly.Connection.prototype.isConnectionAllowed = function(candidate) {
// Don't consider insertion markers.
if (candidate.sourceBlock_.isInsertionMarker()) {
return false;
}
// Type checking.
var canConnect = this.canConnectWithReason_(candidate);
if (canConnect != Blockly.Connection.CAN_CONNECT) {
return false;
}
var firstStatementConnection =
this.sourceBlock_.getFirstStatementConnection();
switch (candidate.type) {
case Blockly.PREVIOUS_STATEMENT:
return this.canConnectToPrevious_(candidate);
case Blockly.OUTPUT_VALUE: {
// Can't drag an input to an output--you have to move the inferior block.
return false;
}
case Blockly.INPUT_VALUE: {
// Offering to connect the left (male) of a value block to an already
// connected value pair is ok, we'll splice it in.
// However, don't offer to splice into an unmovable block.
if (candidate.targetConnection &&
!candidate.targetBlock().isMovable() &&
!candidate.targetBlock().isShadow()) {
return false;
}
break;
}
case Blockly.NEXT_STATEMENT: {
// Scratch-specific behaviour:
// If this is a c-block, we can't connect this block's
// previous connection unless we're connecting to the end of the last
// block on a stack or there's already a block connected inside the c.
if (firstStatementConnection &&
this == this.sourceBlock_.previousConnection &&
candidate.isConnectedToNonInsertionMarker() &&
!firstStatementConnection.targetConnection) {
return false;
}
// Don't let a block with no next connection bump other blocks out of the
// stack. But covering up a shadow block or stack of shadow blocks is
// fine. Similarly, replacing a terminal statement with another terminal
// statement is allowed.
if (candidate.isConnectedToNonInsertionMarker() &&
!this.sourceBlock_.nextConnection &&
!candidate.targetBlock().isShadow() &&
candidate.targetBlock().nextConnection) {
return false;
}
break;
}
default:
throw 'Unknown connection type in isConnectionAllowed';
}
// Don't let blocks try to connect to themselves or ones they nest.
if (Blockly.draggingConnections_.indexOf(candidate) != -1) {
return false;
}
return true;
};
/**
* Connect this connection to another connection.
* @param {!Blockly.Connection} otherConnection Connection to connect to.
*/
Blockly.Connection.prototype.connect = function(otherConnection) {
if (this.targetConnection == otherConnection) {
// Already connected together. NOP.
return;
}
this.checkConnection_(otherConnection);
// Determine which block is superior (higher in the source stack).
if (this.isSuperior()) {
// Superior block.
this.connect_(otherConnection);
} else {
// Inferior block.
otherConnection.connect_(this);
}
};
/**
* Update two connections to target each other.
* @param {Blockly.Connection} first The first connection to update.
* @param {Blockly.Connection} second The second connection to update.
* @private
*/
Blockly.Connection.connectReciprocally_ = function(first, second) {
goog.asserts.assert(first && second, 'Cannot connect null connections.');
first.targetConnection = second;
second.targetConnection = first;
};
/**
* Does the given block have one and only one connection point that will accept
* an orphaned block?
* @param {!Blockly.Block} block The superior block.
* @param {!Blockly.Block} orphanBlock The inferior block.
* @return {Blockly.Connection} The suitable connection point on 'block',
* or null.
* @private
*/
Blockly.Connection.singleConnection_ = function(block, orphanBlock) {
var connection = false;
for (var i = 0; i < block.inputList.length; i++) {
var thisConnection = block.inputList[i].connection;
if (thisConnection && thisConnection.type == Blockly.INPUT_VALUE &&
orphanBlock.outputConnection.checkType_(thisConnection)) {
if (connection) {
return null; // More than one connection.
}
connection = thisConnection;
}
}
return connection;
};
/**
* Disconnect this connection.
*/
Blockly.Connection.prototype.disconnect = function() {
var otherConnection = this.targetConnection;
goog.asserts.assert(otherConnection, 'Source connection not connected.');
goog.asserts.assert(otherConnection.targetConnection == this,
'Target connection not connected to source connection.');
var parentBlock, childBlock, parentConnection;
if (this.isSuperior()) {
// Superior block.
parentBlock = this.sourceBlock_;
childBlock = otherConnection.getSourceBlock();
parentConnection = this;
} else {
// Inferior block.
parentBlock = otherConnection.getSourceBlock();
childBlock = this.sourceBlock_;
parentConnection = otherConnection;
}
this.disconnectInternal_(parentBlock, childBlock);
parentConnection.respawnShadow_();
};
/**
* Disconnect two blocks that are connected by this connection.
* @param {!Blockly.Block} parentBlock The superior block.
* @param {!Blockly.Block} childBlock The inferior block.
* @protected
*/
Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock,
childBlock) {
if (Blockly.Events.isEnabled() && !childBlock.isInsertionMarker()) {
childBlock.workspace.procedureReturnsWillChange();
}
var event;
if (Blockly.Events.isEnabled()) {
event = new Blockly.Events.BlockMove(childBlock);
}
var otherConnection = this.targetConnection;
otherConnection.targetConnection = null;
this.targetConnection = null;
childBlock.setParent(null);
if (event) {
event.recordNew();
Blockly.Events.fire(event);
}
};
/**
* Respawn the shadow block if there was one connected to the this connection.
* @protected
*/
Blockly.Connection.prototype.respawnShadow_ = function() {
var parentBlock = this.getSourceBlock();
var shadow = this.getShadowDom();
if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) {
var blockShadow =
Blockly.Xml.domToBlock(shadow, parentBlock.workspace);
if (blockShadow.outputConnection) {
this.connect(blockShadow.outputConnection);
} else if (blockShadow.previousConnection) {
this.connect(blockShadow.previousConnection);
} else {
throw 'Child block does not have output or previous statement.';
}
}
};
/**
* Returns the block that this connection connects to.
* @return {Blockly.Block} The connected block or null if none is connected.
*/
Blockly.Connection.prototype.targetBlock = function() {
if (this.isConnected()) {
return this.targetConnection.getSourceBlock();
}
return null;
};
/**
* Is this connection compatible with another connection with respect to the
* value type system. E.g. square_root("Hello") is not compatible.
* @param {!Blockly.Connection} otherConnection Connection to compare against.
* @return {boolean} True if the connections share a type.
* @protected
*/
Blockly.Connection.prototype.checkType_ = function(otherConnection) {
if (!this.check_ || !otherConnection.check_) {
// One or both sides are promiscuous enough that anything will fit.
return true;
}
// Find any intersection in the check lists.
for (var i = 0; i < this.check_.length; i++) {
if (otherConnection.check_.indexOf(this.check_[i]) != -1) {
return true;
}
}
// No intersection.
return false;
};
/**
* Function to be called when this connection's compatible types have changed.
* @private
*/
Blockly.Connection.prototype.onCheckChanged_ = function() {
// The new value type may not be compatible with the existing connection.
if (this.isConnected() && !this.checkType_(this.targetConnection)) {
var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
child.unplug();
}
};
/**
* Change a connection's compatibility.
* @param {*} check Compatible value type or list of value types.
* Null if all types are compatible.
* @return {!Blockly.Connection} The connection being modified
* (to allow chaining).
*/
Blockly.Connection.prototype.setCheck = function(check) {
if (check) {
// Ensure that check is in an array.
if (!goog.isArray(check)) {
check = [check];
}
this.check_ = check;
this.onCheckChanged_();
} else {
this.check_ = null;
}
return this;
};
/**
* Returns a shape enum for this connection.
* Used in scratch-blocks to draw unoccupied inputs.
* @return {number} Enum representing shape.
*/
Blockly.Connection.prototype.getOutputShape = function() {
if (!this.check_) return Blockly.OUTPUT_SHAPE_ROUND;
if (this.check_.indexOf('Boolean') !== -1) {
return Blockly.OUTPUT_SHAPE_HEXAGONAL;
}
if (this.check_.indexOf('Number') !== -1) {
return Blockly.OUTPUT_SHAPE_ROUND;
}
if (this.check_.indexOf('String') !== -1) {
return Blockly.OUTPUT_SHAPE_SQUARE;
}
return Blockly.OUTPUT_SHAPE_ROUND;
};
/**
* Change a connection's shadow block.
* @param {Element} shadow DOM representation of a block or null.
*/
Blockly.Connection.prototype.setShadowDom = function(shadow) {
this.shadowDom_ = shadow;
};
/**
* Return a connection's shadow block.
* @return {Element} shadow DOM representation of a block or null.
*/
Blockly.Connection.prototype.getShadowDom = function() {
return this.shadowDom_;
};
/**
* Find all nearby compatible connections to this connection.
* Type checking does not apply, since this function is used for bumping.
*
* Headless configurations (the default) do not have neighboring connection,
* and always return an empty list (the default).
* {@link Blockly.RenderedConnection} overrides this behavior with a list
* computed from the rendered positioning.
* @param {number} maxLimit The maximum radius to another connection.
* @return {!Array.<!Blockly.Connection>} List of connections.
* @private
*/
Blockly.Connection.prototype.neighbours_ = function(/* maxLimit */) {
return [];
};
/**
* This method returns a string describing this Connection in developer terms
* (English only). Intended to on be used in console logs and errors.
* @return {string} The description.
*/
Blockly.Connection.prototype.toString = function() {
var msg;
var block = this.sourceBlock_;
if (!block) {
return 'Orphan Connection';
} else if (block.outputConnection == this) {
msg = 'Output Connection of ';
} else if (block.previousConnection == this) {
msg = 'Previous Connection of ';
} else if (block.nextConnection == this) {
msg = 'Next Connection of ';
} else {
var parentInput = goog.array.find(block.inputList, function(input) {
return input.connection == this;
}, this);
if (parentInput) {
msg = 'Input "' + parentInput.name + '" connection on ';
} else {
console.warn('Connection not actually connected to sourceBlock_');
return 'Orphan Connection';
}
}
return msg + block.toDevString();
};

View File

@@ -0,0 +1,300 @@
/**
* @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 Components for managing connections between blocks.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.ConnectionDB');
goog.require('Blockly.Connection');
/**
* Database of connections.
* Connections are stored in order of their vertical component. This way
* connections in an area may be looked up quickly using a binary search.
* @constructor
*/
Blockly.ConnectionDB = function() {
/**
* Array of connections sorted by y coordinate.
* @type {!Array.<!Blockly.Connection>}
* @private
*/
this.connections_ = [];
};
/**
* Add a connection to the database. Must not already exist in DB.
* @param {!Blockly.Connection} connection The connection to be added.
*/
Blockly.ConnectionDB.prototype.addConnection = function(connection) {
if (connection.inDB_) {
throw Error('Connection already in database.');
}
if (connection.getSourceBlock().isInFlyout) {
// Don't bother maintaining a database of connections in a flyout.
return;
}
var position = this.findPositionForConnection_(connection);
this.connections_.splice(position, 0, connection);
connection.inDB_ = true;
};
/**
* Find the given connection.
* Starts by doing a binary search to find the approximate location, then
* linearly searches nearby for the exact connection.
* @param {!Blockly.Connection} conn The connection to find.
* @return {number} The index of the connection, or -1 if the connection was
* not found.
*/
Blockly.ConnectionDB.prototype.findConnection = function(conn) {
if (!this.connections_.length) {
return -1;
}
var bestGuess = this.findPositionForConnection_(conn);
if (bestGuess >= this.connections_.length) {
// Not in list
return -1;
}
var yPos = conn.y_;
// Walk forward and back on the y axis looking for the connection.
var pointerMin = bestGuess;
var pointerMax = bestGuess;
while (pointerMin >= 0 && this.connections_[pointerMin].y_ == yPos) {
if (this.connections_[pointerMin] == conn) {
return pointerMin;
}
pointerMin--;
}
while (pointerMax < this.connections_.length &&
this.connections_[pointerMax].y_ == yPos) {
if (this.connections_[pointerMax] == conn) {
return pointerMax;
}
pointerMax++;
}
return -1;
};
/**
* Finds a candidate position for inserting this connection into the list.
* This will be in the correct y order but makes no guarantees about ordering in
* the x axis.
* @param {!Blockly.Connection} connection The connection to insert.
* @return {number} The candidate index.
* @private
*/
Blockly.ConnectionDB.prototype.findPositionForConnection_ = function(
connection) {
if (!this.connections_.length) {
return 0;
}
var pointerMin = 0;
var pointerMax = this.connections_.length;
while (pointerMin < pointerMax) {
var pointerMid = Math.floor((pointerMin + pointerMax) / 2);
if (this.connections_[pointerMid].y_ < connection.y_) {
pointerMin = pointerMid + 1;
} else if (this.connections_[pointerMid].y_ > connection.y_) {
pointerMax = pointerMid;
} else {
pointerMin = pointerMid;
break;
}
}
return pointerMin;
};
/**
* Remove a connection from the database. Must already exist in DB.
* @param {!Blockly.Connection} connection The connection to be removed.
* @private
*/
Blockly.ConnectionDB.prototype.removeConnection_ = function(connection) {
if (!connection.inDB_) {
throw Error('Connection not in database.');
}
var removalIndex = this.findConnection(connection);
if (removalIndex == -1) {
throw Error('Unable to find connection in connectionDB.');
}
connection.inDB_ = false;
this.connections_.splice(removalIndex, 1);
};
/**
* Find all nearby connections to the given connection.
* Type checking does not apply, since this function is used for bumping.
* @param {!Blockly.Connection} connection The connection whose neighbours
* should be returned.
* @param {number} maxRadius The maximum radius to another connection.
* @return {!Array.<Blockly.Connection>} List of connections.
*/
Blockly.ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) {
var db = this.connections_;
var currentX = connection.x_;
var currentY = connection.y_;
// Binary search to find the closest y location.
var pointerMin = 0;
var pointerMax = db.length - 2;
var pointerMid = pointerMax;
while (pointerMin < pointerMid) {
if (db[pointerMid].y_ < currentY) {
pointerMin = pointerMid;
} else {
pointerMax = pointerMid;
}
pointerMid = Math.floor((pointerMin + pointerMax) / 2);
}
var neighbours = [];
/**
* Computes if the current connection is within the allowed radius of another
* connection.
* This function is a closure and has access to outside variables.
* @param {number} yIndex The other connection's index in the database.
* @return {boolean} True if the current connection's vertical distance from
* the other connection is less than the allowed radius.
*/
function checkConnection_(yIndex) {
var dx = currentX - db[yIndex].x_;
var dy = currentY - db[yIndex].y_;
var r = Math.sqrt(dx * dx + dy * dy);
if (r <= maxRadius) {
neighbours.push(db[yIndex]);
}
return dy < maxRadius;
}
// Walk forward and back on the y axis looking for the closest x,y point.
pointerMin = pointerMid;
pointerMax = pointerMid;
if (db.length) {
while (pointerMin >= 0 && checkConnection_(pointerMin)) {
pointerMin--;
}
do {
pointerMax++;
} while (pointerMax < db.length && checkConnection_(pointerMax));
}
return neighbours;
};
/**
* Is the candidate connection close to the reference connection.
* Extremely fast; only looks at Y distance.
* @param {number} index Index in database of candidate connection.
* @param {number} baseY Reference connection's Y value.
* @param {number} maxRadius The maximum radius to another connection.
* @return {boolean} True if connection is in range.
* @private
*/
Blockly.ConnectionDB.prototype.isInYRange_ = function(index, baseY, maxRadius) {
return (Math.abs(this.connections_[index].y_ - baseY) <= maxRadius);
};
/**
* Find the closest compatible connection to this connection.
* @param {!Blockly.Connection} conn The connection searching for a compatible
* mate.
* @param {number} maxRadius The maximum radius to another connection.
* @param {!goog.math.Coordinate} dxy Offset between this connection's location
* in the database and the current location (as a result of dragging).
* @return {!{connection: ?Blockly.Connection, radius: number}} Contains two
* properties:' connection' which is either another connection or null,
* and 'radius' which is the distance.
*/
Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius,
dxy) {
// Don't bother.
if (!this.connections_.length) {
return {connection: null, radius: maxRadius};
}
// Stash the values of x and y from before the drag.
var baseY = conn.y_;
var baseX = conn.x_;
conn.x_ = baseX + dxy.x;
conn.y_ = baseY + dxy.y;
// findPositionForConnection finds an index for insertion, which is always
// after any block with the same y index. We want to search both forward
// and back, so search on both sides of the index.
var closestIndex = this.findPositionForConnection_(conn);
var bestConnection = null;
var bestRadius = maxRadius;
var temp;
// Walk forward and back on the y axis looking for the closest x,y point.
var pointerMin = closestIndex - 1;
while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y_, maxRadius)) {
temp = this.connections_[pointerMin];
if (conn.isConnectionAllowed(temp, bestRadius)) {
bestConnection = temp;
bestRadius = temp.distanceFrom(conn);
}
pointerMin--;
}
var pointerMax = closestIndex;
while (pointerMax < this.connections_.length &&
this.isInYRange_(pointerMax, conn.y_, maxRadius)) {
temp = this.connections_[pointerMax];
if (conn.isConnectionAllowed(temp, bestRadius)) {
bestConnection = temp;
bestRadius = temp.distanceFrom(conn);
}
pointerMax++;
}
// Reset the values of x and y.
conn.x_ = baseX;
conn.y_ = baseY;
// If there were no valid connections, bestConnection will be null.
return {connection: bestConnection, radius: bestRadius};
};
/**
* Initialize a set of connection DBs for a specified workspace.
* @param {!Blockly.Workspace} workspace The workspace this DB is for.
*/
Blockly.ConnectionDB.init = function(workspace) {
// Create four databases, one for each connection type.
var dbList = [];
dbList[Blockly.INPUT_VALUE] = new Blockly.ConnectionDB();
dbList[Blockly.OUTPUT_VALUE] = new Blockly.ConnectionDB();
dbList[Blockly.NEXT_STATEMENT] = new Blockly.ConnectionDB();
dbList[Blockly.PREVIOUS_STATEMENT] = new Blockly.ConnectionDB();
workspace.connectionDBList = dbList;
};

View File

@@ -0,0 +1,408 @@
/**
* @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 Blockly constants.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.constants');
/**
* Number of pixels the mouse must move before a drag starts.
*/
Blockly.DRAG_RADIUS = 3;
/**
* Number of pixels the mouse must move before a drag/scroll starts from the
* flyout. Because the drag-intention is determined when this is reached, it is
* larger than Blockly.DRAG_RADIUS so that the drag-direction is clearer.
*/
Blockly.FLYOUT_DRAG_RADIUS = 10;
/**
* Maximum misalignment between connections for them to snap together.
*/
Blockly.SNAP_RADIUS = 48;
/**
* Maximum misalignment between connections for them to snap together,
* when a connection is already highlighted.
*/
Blockly.CONNECTING_SNAP_RADIUS = 68;
/**
* How much to prefer staying connected to the current connection over moving to
* a new connection. The current previewed connection is considered to be this
* much closer to the matching connection on the block than it actually is.
*/
Blockly.CURRENT_CONNECTION_PREFERENCE = 20;
/**
* Delay in ms between trigger and bumping unconnected block out of alignment.
*/
Blockly.BUMP_DELAY = 0;
/**
* Number of characters to truncate a collapsed block to.
*/
Blockly.COLLAPSE_CHARS = 30;
/**
* Length in ms for a touch to become a long press.
*/
Blockly.LONGPRESS = 750;
/**
* Distance to scroll when a mouse wheel event is received and its delta mode
* is line (0x1) instead of pixel (0x0). In these cases, a single "scroll" has
* a delta of 1, which makes the workspace scroll very slowly (just one pixel).
* To compensate, that delta is multiplied by this value.
* @const
* @package
*/
Blockly.LINE_SCROLL_MULTIPLIER = 15;
/**
* Prevent a sound from playing if another sound preceded it within this many
* milliseconds.
*/
Blockly.SOUND_LIMIT = 100;
/**
* When dragging a block out of a stack, split the stack in two (true), or drag
* out the block healing the stack (false).
*/
Blockly.DRAG_STACK = true;
/**
* The richness of block colours, regardless of the hue.
* Must be in the range of 0 (inclusive) to 1 (exclusive).
*/
Blockly.HSV_SATURATION = 0.45;
/**
* The intensity of block colours, regardless of the hue.
* Must be in the range of 0 (inclusive) to 1 (exclusive).
*/
Blockly.HSV_VALUE = 0.65;
/**
* Sprited icons and images.
*/
Blockly.SPRITE = {
width: 96,
height: 124,
url: 'sprites.png'
};
// Constants below this point are not intended to be changed.
/**
* Required name space for SVG elements.
* @const
*/
Blockly.SVG_NS = 'http://www.w3.org/2000/svg';
/**
* Required name space for HTML elements.
* @const
*/
Blockly.HTML_NS = 'http://www.w3.org/1999/xhtml';
/**
* ENUM for a right-facing value input. E.g. 'set item to' or 'return'.
* @const
*/
Blockly.INPUT_VALUE = 1;
/**
* ENUM for a left-facing value output. E.g. 'random fraction'.
* @const
*/
Blockly.OUTPUT_VALUE = 2;
/**
* ENUM for a down-facing block stack. E.g. 'if-do' or 'else'.
* @const
*/
Blockly.NEXT_STATEMENT = 3;
/**
* ENUM for an up-facing block stack. E.g. 'break out of loop'.
* @const
*/
Blockly.PREVIOUS_STATEMENT = 4;
/**
* ENUM for an dummy input. Used to add field(s) with no input.
* @const
*/
Blockly.DUMMY_INPUT = 5;
/**
* ENUM for left alignment.
* @const
*/
Blockly.ALIGN_LEFT = -1;
/**
* ENUM for centre alignment.
* @const
*/
Blockly.ALIGN_CENTRE = 0;
/**
* ENUM for right alignment.
* @const
*/
Blockly.ALIGN_RIGHT = 1;
/**
* ENUM for no drag operation.
* @const
*/
Blockly.DRAG_NONE = 0;
/**
* ENUM for inside the sticky DRAG_RADIUS.
* @const
*/
Blockly.DRAG_STICKY = 1;
/**
* ENUM for inside the non-sticky DRAG_RADIUS, for differentiating between
* clicks and drags.
* @const
*/
Blockly.DRAG_BEGIN = 1;
/**
* ENUM for freely draggable (outside the DRAG_RADIUS, if one applies).
* @const
*/
Blockly.DRAG_FREE = 2;
/**
* Lookup table for determining the opposite type of a connection.
* @const
*/
Blockly.OPPOSITE_TYPE = [];
Blockly.OPPOSITE_TYPE[Blockly.INPUT_VALUE] = Blockly.OUTPUT_VALUE;
Blockly.OPPOSITE_TYPE[Blockly.OUTPUT_VALUE] = Blockly.INPUT_VALUE;
Blockly.OPPOSITE_TYPE[Blockly.NEXT_STATEMENT] = Blockly.PREVIOUS_STATEMENT;
Blockly.OPPOSITE_TYPE[Blockly.PREVIOUS_STATEMENT] = Blockly.NEXT_STATEMENT;
/**
* ENUM for toolbox and flyout at top of screen.
* @const
*/
Blockly.TOOLBOX_AT_TOP = 0;
/**
* ENUM for toolbox and flyout at bottom of screen.
* @const
*/
Blockly.TOOLBOX_AT_BOTTOM = 1;
/**
* ENUM for toolbox and flyout at left of screen.
* @const
*/
Blockly.TOOLBOX_AT_LEFT = 2;
/**
* ENUM for toolbox and flyout at right of screen.
* @const
*/
Blockly.TOOLBOX_AT_RIGHT = 3;
/**
* ENUM for output shape: hexagonal (booleans/predicates).
* @const
*/
Blockly.OUTPUT_SHAPE_HEXAGONAL = 1;
/**
* ENUM for output shape: rounded (numbers).
* @const
*/
Blockly.OUTPUT_SHAPE_ROUND = 2;
/**
* ENUM for output shape: squared (any/all values; strings).
* @const
*/
Blockly.OUTPUT_SHAPE_SQUARE = 3;
/**
* ENUM for categories.
* @const
*/
Blockly.Categories = {
"motion": "motion",
"looks": "looks",
"sound": "sounds",
"pen": "pen",
"data": "data",
"dataLists": "data-lists",
"event": "events",
"control": "control",
"sensing": "sensing",
"operators": "operators",
"more": "more"
};
/**
* ENUM representing that an event is not in any delete areas.
* Null for backwards compatibility reasons.
* @const
*/
Blockly.DELETE_AREA_NONE = null;
/**
* ENUM representing that an event is in the delete area of the trash can.
* @const
*/
Blockly.DELETE_AREA_TRASH = 1;
/**
* ENUM representing that an event is in the delete area of the toolbox or
* flyout.
* @const
*/
Blockly.DELETE_AREA_TOOLBOX = 2;
/**
* String for use in the "custom" attribute of a category in toolbox xml.
* This string indicates that the category should be dynamically populated with
* variable blocks.
* @const {string}
*/
Blockly.VARIABLE_CATEGORY_NAME = 'VARIABLE';
/**
* String for use in the "custom" attribute of a category in toolbox xml.
* This string indicates that the category should be dynamically populated with
* procedure blocks.
* @const {string}
*/
Blockly.PROCEDURE_CATEGORY_NAME = 'PROCEDURE';
/**
* String for use in the dropdown created in field_variable.
* This string indicates that this option in the dropdown is 'Rename
* variable...' and if selected, should trigger the prompt to rename a variable.
* @const {string}
*/
Blockly.RENAME_VARIABLE_ID = 'RENAME_VARIABLE_ID';
/**
* String for use in the dropdown created in field_variable.
* This string indicates that this option in the dropdown is 'Delete the "%1"
* variable' and if selected, should trigger the prompt to delete a variable.
* @const {string}
*/
Blockly.DELETE_VARIABLE_ID = 'DELETE_VARIABLE_ID';
/**
* String for use in the dropdown created in field_variable,
* specifically for broadcast messages.
* This string indicates that this option in the dropdown is 'New message...'
* and if selected, should trigger the prompt to create a new message.
* @const {string}
*/
Blockly.NEW_BROADCAST_MESSAGE_ID = 'NEW_BROADCAST_MESSAGE_ID';
/**
* String representing the variable type of broadcast message blocks.
* This string, for use in differentiating between types of variables,
* indicates that the current variable is a broadcast message.
* @const {string}
*/
Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE = 'broadcast_msg';
/**
* String representing the variable type of list blocks.
* This string, for use in differentiating between types of variables,
* indicates that the current variable is a list.
* @const {string}
*/
Blockly.LIST_VARIABLE_TYPE = 'list';
// TODO (#1251) Replace '' below with 'scalar', and start using this constant
// everywhere.
/**
* String representing the variable type of scalar variables.
* This string, for use in differentiating between types of variables,
* indicates that the current variable is a scalar variable.
* @const {string}
*/
Blockly.SCALAR_VARIABLE_TYPE = '';
/**
* The type of all procedure definition blocks.
* @const {string}
*/
Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE = 'procedures_definition';
/**
* The type of all procedure prototype blocks.
* @const {string}
*/
Blockly.PROCEDURES_PROTOTYPE_BLOCK_TYPE = 'procedures_prototype';
/**
* The type of all procedure call blocks.
* @const {string}
*/
Blockly.PROCEDURES_CALL_BLOCK_TYPE = 'procedures_call';
/**
* Enum for procedure call statements.
*/
Blockly.PROCEDURES_CALL_TYPE_STATEMENT = 0;
/**
* Enum for procedure call round reporters.
*/
Blockly.PROCEDURES_CALL_TYPE_REPORTER = 1;
/**
* Enum for procedure call booleans.
*/
Blockly.PROCEDURES_CALL_TYPE_BOOLEAN = 2;
/**
* The type of all procedure return blocks.
* @const {string}
*/
Blockly.PROCEDURES_RETURN_BLOCK_TYPE = 'procedures_return';
/**
* ENUM for flyout status button states.
* @const
*/
Blockly.StatusButtonState = {
"READY": "ready",
"NOT_READY": "not ready",
};

View File

@@ -0,0 +1,534 @@
/**
* @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 Functionality for the right-click context menus.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* @name Blockly.ContextMenu
* @namespace
*/
goog.provide('Blockly.ContextMenu');
goog.require('Blockly.Events.BlockCreate');
goog.require('Blockly.scratchBlocksUtils');
goog.require('Blockly.utils');
goog.require('Blockly.utils.uiMenu');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.style');
goog.require('goog.ui.Menu');
goog.require('goog.ui.MenuItem');
goog.require('goog.userAgent');
/**
* Which block is the context menu attached to?
* @type {Blockly.Block}
*/
Blockly.ContextMenu.currentBlock = null;
/**
* Opaque data that can be passed to unbindEvent_.
* @type {Array.<!Array>}
* @private
*/
Blockly.ContextMenu.eventWrapper_ = null;
/**
* Construct the menu based on the list of options and show the menu.
* @param {!Event} e Mouse event.
* @param {!Array.<!Object>} options Array of menu options.
* @param {boolean} rtl True if RTL, false if LTR.
*/
Blockly.ContextMenu.show = function(e, options, rtl) {
Blockly.WidgetDiv.show(Blockly.ContextMenu, rtl, null);
if (!options.length) {
Blockly.ContextMenu.hide();
return;
}
var menu = Blockly.ContextMenu.populate_(options, rtl);
goog.events.listen(
menu, goog.ui.Component.EventType.ACTION, Blockly.ContextMenu.hide);
Blockly.ContextMenu.position_(menu, e, rtl);
// 1ms delay is required for focusing on context menus because some other
// mouse event is still waiting in the queue and clears focus.
setTimeout(function() {menu.getElement().focus();}, 1);
Blockly.ContextMenu.currentBlock = null; // May be set by Blockly.Block.
};
/**
* Create the context menu object and populate it with the given options.
* @param {!Array.<!Object>} options Array of menu options.
* @param {boolean} rtl True if RTL, false if LTR.
* @return {!goog.ui.Menu} The menu that will be shown on right click.
* @private
*/
Blockly.ContextMenu.populate_ = function(options, rtl) {
/* Here's what one option object looks like:
{text: 'Make It So',
enabled: true,
callback: Blockly.MakeItSo}
*/
var menu = new goog.ui.Menu();
menu.setRightToLeft(rtl);
// Sometimes the context menu can be created such that the mouse is hovering over an item in the menu
// When this happens, a contextmenu event is immediately sent to that item
// Obviously we don't want that to trigger an item to be selected.
var acceptContextMenuEvents = false;
setTimeout(function() {
acceptContextMenuEvents = true;
});
for (var i = 0, option; option = options[i]; i++) {
var menuItem = new goog.ui.MenuItem(option.text);
menuItem.setRightToLeft(rtl);
menu.addChild(menuItem, true);
menuItem.setEnabled(option.enabled);
if (option.enabled) {
goog.events.listen(
menuItem, goog.ui.Component.EventType.ACTION, option.callback);
menuItem.handleContextMenu = function(/* e */) {
if (!acceptContextMenuEvents) {
return;
}
// Right-clicking on menu option should count as a click.
goog.events.dispatchEvent(this, goog.ui.Component.EventType.ACTION);
};
}
}
return menu;
};
/**
* Add the menu to the page and position it correctly.
* @param {!goog.ui.Menu} menu The menu to add and position.
* @param {!Event} e Mouse event for the right click that is making the context
* menu appear.
* @param {boolean} rtl True if RTL, false if LTR.
* @private
*/
Blockly.ContextMenu.position_ = function(menu, e, rtl) {
// Record windowSize and scrollOffset before adding menu.
var viewportBBox = Blockly.utils.getViewportBBox();
// This one is just a point, but we'll pretend that it's a rect so we can use
// some helper functions.
var anchorBBox = {
top: e.clientY + viewportBBox.top,
bottom: e.clientY + viewportBBox.top,
left: e.clientX + viewportBBox.left,
right: e.clientX + viewportBBox.left
};
Blockly.ContextMenu.createWidget_(menu);
var menuSize = Blockly.utils.uiMenu.getSize(menu);
if (rtl) {
Blockly.utils.uiMenu.adjustBBoxesForRTL(viewportBBox, anchorBBox, menuSize);
}
Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, menuSize, rtl);
// Calling menuDom.focus() has to wait until after the menu has been placed
// correctly. Otherwise it will cause a page scroll to get the misplaced menu
// in view. See issue #1329.
menu.getElement().focus();
// https://github.com/LLK/scratch-blocks/pull/2834
menu.setVisible(true, true, e);
};
/**
* Create and render the menu widget inside Blockly's widget div.
* @param {!goog.ui.Menu} menu The menu to add to the widget div.
* @private
*/
Blockly.ContextMenu.createWidget_ = function(menu) {
var div = Blockly.WidgetDiv.DIV;
menu.render(div);
var menuDom = menu.getElement();
Blockly.utils.addClass(menuDom, 'blocklyContextMenu');
// Prevent system context menu when right-clicking a Blockly context menu.
Blockly.bindEventWithChecks_(
menuDom, 'contextmenu', null, Blockly.utils.noEvent);
// Enable autofocus after the initial render to avoid issue #1329.
menu.setAllowAutoFocus(true);
};
/**
* Hide the context menu.
*/
Blockly.ContextMenu.hide = function() {
Blockly.WidgetDiv.hideIfOwner(Blockly.ContextMenu);
Blockly.ContextMenu.currentBlock = null;
if (Blockly.ContextMenu.eventWrapper_) {
Blockly.unbindEvent_(Blockly.ContextMenu.eventWrapper_);
}
};
/**
* Create a callback function that creates and configures a block,
* then places the new block next to the original.
* @param {!Blockly.Block} block Original block.
* @param {!Element} xml XML representation of new block.
* @return {!Function} Function that creates a block.
*/
Blockly.ContextMenu.callbackFactory = function(block, xml) {
return function() {
Blockly.Events.disable();
try {
var newBlock = Blockly.Xml.domToBlock(xml, block.workspace);
// Move the new block next to the old block.
var xy = block.getRelativeToSurfaceXY();
if (block.RTL) {
xy.x -= Blockly.SNAP_RADIUS;
} else {
xy.x += Blockly.SNAP_RADIUS;
}
xy.y += Blockly.SNAP_RADIUS * 2;
newBlock.moveBy(xy.x, xy.y);
} finally {
Blockly.Events.enable();
}
if (Blockly.Events.isEnabled() && !newBlock.isShadow()) {
Blockly.Events.fire(new Blockly.Events.BlockCreate(newBlock));
}
newBlock.select();
};
};
// Helper functions for creating context menu options.
/**
* Make a context menu option for deleting the current block.
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.blockDeleteOption = function(block) {
// Option to delete this block but not blocks lower in the stack.
// Count the number of blocks that are nested in this block,
// ignoring shadows and without ordering.
var descendantCount = block.getDescendants(false, true).length;
var nextBlock = block.getNextBlock();
if (nextBlock) {
// Blocks in the current stack would survive this block's deletion.
descendantCount -= nextBlock.getDescendants(false, true).length;
}
var deleteOption = {
text: descendantCount == 1 ? Blockly.Msg.DELETE_BLOCK :
Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(descendantCount)),
enabled: true,
callback: function() {
Blockly.Events.setGroup(true);
block.dispose(true, true);
Blockly.Events.setGroup(false);
}
};
return deleteOption;
};
/**
* Make a context menu option for showing help for the current block.
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.blockHelpOption = function(block) {
var url = goog.isFunction(block.helpUrl) ? block.helpUrl() : block.helpUrl;
var helpOption = {
enabled: !!url,
text: Blockly.Msg.HELP,
callback: function() {
block.showHelp_();
}
};
return helpOption;
};
/**
* Make a context menu option for duplicating the current block.
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
* @param {!Event} event Event that caused the context menu to open.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.blockDuplicateOption = function(block, event) {
var duplicateOption = {
text: Blockly.Msg.DUPLICATE,
enabled: true,
callback:
Blockly.scratchBlocksUtils.duplicateAndDragCallback(block, event)
};
return duplicateOption;
};
/**
* Make a context menu option for adding or removing comments on the current
* block.
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.blockCommentOption = function(block) {
var commentOption = {
enabled: !goog.userAgent.IE
};
// If there's already a comment, add an option to delete it.
if (block.comment) {
commentOption.text = Blockly.Msg.REMOVE_COMMENT;
commentOption.callback = function() {
block.setCommentText(null);
};
} else {
// If there's no comment, add an option to create a comment.
commentOption.text = Blockly.Msg.ADD_COMMENT;
commentOption.callback = function() {
block.setCommentText('');
block.comment.focus();
};
}
return commentOption;
};
/**
* Make a context menu option for undoing the most recent action on the
* workspace.
* @param {!Blockly.WorkspaceSvg} ws The workspace where the right-click
* originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.wsUndoOption = function(ws) {
return {
text: Blockly.Msg.UNDO,
enabled: ws.hasUndoStack(),
callback: ws.undo.bind(ws, false)
};
};
/**
* Make a context menu option for redoing the most recent action on the
* workspace.
* @param {!Blockly.WorkspaceSvg} ws The workspace where the right-click
* originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.wsRedoOption = function(ws) {
return {
text: Blockly.Msg.REDO,
enabled: ws.hasRedoStack(),
callback: ws.undo.bind(ws, true)
};
};
/**
* Make a context menu option for cleaning up blocks on the workspace, by
* aligning them vertically.
* @param {!Blockly.WorkspaceSvg} ws The workspace where the right-click
* originated.
* @param {number} numTopBlocks The number of top blocks on the workspace.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.wsCleanupOption = function(ws, numTopBlocks) {
return {
text: Blockly.Msg.CLEAN_UP,
enabled: numTopBlocks > 1,
callback: ws.cleanUp.bind(ws, true)
};
};
/**
* Helper function for toggling delete state on blocks on the workspace, to be
* called from a right-click menu.
* @param {!Array.<!Blockly.BlockSvg>} topBlocks The list of top blocks on the
* the workspace.
* @param {boolean} shouldCollapse True if the blocks should be collapsed, false
* if they should be expanded.
* @private
*/
Blockly.ContextMenu.toggleCollapseFn_ = function(topBlocks, shouldCollapse) {
// Add a little animation to collapsing and expanding.
var DELAY = 10;
var ms = 0;
for (var i = 0; i < topBlocks.length; i++) {
var block = topBlocks[i];
while (block) {
setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms);
block = block.getNextBlock();
ms += DELAY;
}
}
};
/**
* Make a context menu option for collapsing all block stacks on the workspace.
* @param {boolean} hasExpandedBlocks Whether there are any non-collapsed blocks
* on the workspace.
* @param {!Array.<!Blockly.BlockSvg>} topBlocks The list of top blocks on the
* the workspace.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.wsCollapseOption = function(hasExpandedBlocks, topBlocks) {
return {
enabled: hasExpandedBlocks,
text: Blockly.Msg.COLLAPSE_ALL,
callback: function() {
Blockly.ContextMenu.toggleCollapseFn_(topBlocks, true);
}
};
};
/**
* Make a context menu option for expanding all block stacks on the workspace.
* @param {boolean} hasCollapsedBlocks Whether there are any collapsed blocks
* on the workspace.
* @param {!Array.<!Blockly.BlockSvg>} topBlocks The list of top blocks on the
* the workspace.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.wsExpandOption = function(hasCollapsedBlocks, topBlocks) {
return {
enabled: hasCollapsedBlocks,
text: Blockly.Msg.EXPAND_ALL,
callback: function() {
Blockly.ContextMenu.toggleCollapseFn_(topBlocks, false);
}
};
};
/**
* Make a context menu option for deleting the current workspace comment.
* @param {!Blockly.WorkspaceCommentSvg} comment The workspace comment where the
* right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.commentDeleteOption = function(comment) {
var deleteOption = {
text: Blockly.Msg.DELETE,
enabled: true,
callback: function() {
Blockly.Events.setGroup(true);
comment.dispose(true, true);
Blockly.Events.setGroup(false);
}
};
return deleteOption;
};
/**
* Make a context menu option for duplicating the current workspace comment.
* @param {!Blockly.WorkspaceCommentSvg} comment The workspace comment where the
* right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.commentDuplicateOption = function(comment) {
var duplicateOption = {
text: Blockly.Msg.DUPLICATE,
enabled: true,
callback: function() {
Blockly.duplicate_(comment);
}
};
return duplicateOption;
};
/**
* Make a context menu option for adding a comment on the workspace.
* @param {!Blockly.WorkspaceSvg} ws The workspace where the right-click
* originated.
* @param {!Event} e The right-click mouse event.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.workspaceCommentOption = function(ws, e) {
// Helper function to create and position a comment correctly based on the
// location of the mouse event.
var addWsComment = function() {
// Disable events while this comment is getting created
// so that we can fire a single create event for this comment
// at the end (instead of CommentCreate followed by CommentMove,
// which results in unexpected undo behavior).
var disabled = false;
if (Blockly.Events.isEnabled()) {
Blockly.Events.disable();
disabled = true;
}
var comment = new Blockly.WorkspaceCommentSvg(
ws, '', Blockly.WorkspaceCommentSvg.DEFAULT_SIZE,
Blockly.WorkspaceCommentSvg.DEFAULT_SIZE, false);
var injectionDiv = ws.getInjectionDiv();
// Bounding rect coordinates are in client coordinates, meaning that they
// are in pixels relative to the upper left corner of the visible browser
// window. These coordinates change when you scroll the browser window.
var boundingRect = injectionDiv.getBoundingClientRect();
// The client coordinates offset by the injection div's upper left corner.
var clientOffsetPixels = new goog.math.Coordinate(
e.clientX - boundingRect.left, e.clientY - boundingRect.top);
// The offset in pixels between the main workspace's origin and the upper
// left corner of the injection div.
var mainOffsetPixels = ws.getOriginOffsetInPixels();
// The position of the new comment in pixels relative to the origin of the
// main workspace.
var finalOffsetPixels = goog.math.Coordinate.difference(clientOffsetPixels,
mainOffsetPixels);
// The position of the new comment in main workspace coordinates.
var finalOffsetMainWs = finalOffsetPixels.scale(1 / ws.scale);
var commentX = finalOffsetMainWs.x;
var commentY = finalOffsetMainWs.y;
comment.moveBy(commentX, commentY);
if (ws.rendered) {
comment.initSvg();
comment.render(false);
comment.select();
}
if (disabled) {
Blockly.Events.enable();
}
Blockly.WorkspaceComment.fireCreateEvent(comment);
};
var wsCommentOption = {enabled: true};
wsCommentOption.text = Blockly.Msg.ADD_COMMENT;
wsCommentOption.callback = function() {
addWsComment();
};
return wsCommentOption;
};
// End helper functions for creating context menu options.

1356
scratch-blocks/core/css.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,490 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Data Flyout components including variable and list blocks.
* @author marisaleung@google.com (Marisa Leung)
*/
'use strict';
/**
* @name Blockly.DataCategory
* @namespace
**/
goog.provide('Blockly.DataCategory');
goog.require('Blockly.Blocks');
goog.require('Blockly.VariableModel');
goog.require('Blockly.Variables');
goog.require('Blockly.Workspace');
/**
* Construct the blocks required by the flyout for the variable category.
* @param {!Blockly.Workspace} workspace The workspace containing variables.
* @return {!Array.<!Element>} Array of XML block elements.
*/
Blockly.DataCategory = function(workspace) {
var variableModelList = workspace.getVariablesOfType('');
variableModelList.sort(Blockly.VariableModel.compareByName);
var xmlList = [];
Blockly.DataCategory.addCreateButton(xmlList, workspace, 'VARIABLE');
for (var i = 0; i < variableModelList.length; i++) {
Blockly.DataCategory.addDataVariable(xmlList, variableModelList[i]);
}
if (variableModelList.length > 0) {
xmlList[xmlList.length - 1].setAttribute('gap', 28);
var firstVariable = variableModelList[0];
Blockly.DataCategory.addSetVariableTo(xmlList, firstVariable);
Blockly.DataCategory.addChangeVariableBy(xmlList, firstVariable);
Blockly.DataCategory.addShowVariable(xmlList, firstVariable);
Blockly.DataCategory.addHideVariable(xmlList, firstVariable);
}
// Now add list variables to the flyout
Blockly.DataCategory.addCreateButton(xmlList, workspace, 'LIST');
variableModelList = workspace.getVariablesOfType(Blockly.LIST_VARIABLE_TYPE);
variableModelList.sort(Blockly.VariableModel.compareByName);
for (var i = 0; i < variableModelList.length; i++) {
Blockly.DataCategory.addDataList(xmlList, variableModelList[i]);
}
if (variableModelList.length > 0) {
xmlList[xmlList.length - 1].setAttribute('gap', 28);
var firstVariable = variableModelList[0];
Blockly.DataCategory.addAddToList(xmlList, firstVariable);
Blockly.DataCategory.addSep(xmlList);
Blockly.DataCategory.addDeleteOfList(xmlList, firstVariable);
Blockly.DataCategory.addDeleteAllOfList(xmlList, firstVariable);
Blockly.DataCategory.addInsertAtList(xmlList, firstVariable);
Blockly.DataCategory.addReplaceItemOfList(xmlList, firstVariable);
Blockly.DataCategory.addSep(xmlList);
Blockly.DataCategory.addItemOfList(xmlList, firstVariable);
Blockly.DataCategory.addItemNumberOfList(xmlList, firstVariable);
Blockly.DataCategory.addLengthOfList(xmlList, firstVariable);
Blockly.DataCategory.addListContainsItem(xmlList, firstVariable);
Blockly.DataCategory.addSep(xmlList);
Blockly.DataCategory.addShowList(xmlList, firstVariable);
Blockly.DataCategory.addHideList(xmlList, firstVariable);
}
return xmlList;
};
/**
* Construct and add a data_variable block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addDataVariable = function(xmlList, variable) {
// <block id="variableId" type="data_variable">
// <field name="VARIABLE">variablename</field>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_variable', 'VARIABLE');
// In the flyout, this ID must match variable ID for monitor syncing reasons
xmlList[xmlList.length - 1].setAttribute('id', variable.getId());
};
/**
* Construct and add a data_setvariableto block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addSetVariableTo = function(xmlList, variable) {
// <block type="data_setvariableto" gap="20">
// <value name="VARIABLE">
// <shadow type="data_variablemenu"></shadow>
// </value>
// <value name="VALUE">
// <shadow type="text">
// <field name="TEXT">0</field>
// </shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_setvariableto',
'VARIABLE', ['VALUE', 'text', 0]);
};
/**
* Construct and add a data_changevariableby block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addChangeVariableBy = function(xmlList, variable) {
// <block type="data_changevariableby">
// <value name="VARIABLE">
// <shadow type="data_variablemenu"></shadow>
// </value>
// <value name="VALUE">
// <shadow type="math_number">
// <field name="NUM">1</field>
// </shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_changevariableby',
'VARIABLE', ['VALUE', 'math_number', 1]);
};
/**
* Construct and add a data_showVariable block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addShowVariable = function(xmlList, variable) {
// <block type="data_showvariable">
// <value name="VARIABLE">
// <shadow type="data_variablemenu"></shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_showvariable',
'VARIABLE');
};
/**
* Construct and add a data_hideVariable block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addHideVariable = function(xmlList, variable) {
// <block type="data_hidevariable">
// <value name="VARIABLE">
// <shadow type="data_variablemenu"></shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_hidevariable',
'VARIABLE');
};
/**
* Construct and add a data_listcontents block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addDataList = function(xmlList, variable) {
// <block id="variableId" type="data_listcontents">
// <field name="LIST">variablename</field>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_listcontents', 'LIST');
// In the flyout, this ID must match variable ID for monitor syncing reasons
xmlList[xmlList.length - 1].setAttribute('id', variable.getId());
};
/**
* Construct and add a data_addtolist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addAddToList = function(xmlList, variable) {
// <block type="data_addtolist">
// <field name="LIST" variabletype="list" id="">variablename</field>
// <value name="ITEM">
// <shadow type="text">
// <field name="TEXT">thing</field>
// </shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_addtolist', 'LIST',
['ITEM', 'text', Blockly.Msg.DEFAULT_LIST_ITEM]);
};
/**
* Construct and add a data_deleteoflist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addDeleteOfList = function(xmlList, variable) {
// <block type="data_deleteoflist">
// <field name="LIST" variabletype="list" id="">variablename</field>
// <value name="INDEX">
// <shadow type="math_integer">
// <field name="NUM">1</field>
// </shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_deleteoflist', 'LIST',
['INDEX', 'math_integer', 1]);
};
/**
* Construct and add a data_deleteoflist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addDeleteAllOfList = function(xmlList, variable) {
// <block type="data_deletealloflist">
// <field name="LIST" variabletype="list" id="">variablename</field>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_deletealloflist',
'LIST');
};
/**
* Construct and add a data_insertatlist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addInsertAtList = function(xmlList, variable) {
// <block type="data_insertatlist">
// <field name="LIST" variabletype="list" id="">variablename</field>
// <value name="INDEX">
// <shadow type="math_integer">
// <field name="NUM">1</field>
// </shadow>
// </value>
// <value name="ITEM">
// <shadow type="text">
// <field name="TEXT">thing</field>
// </shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_insertatlist', 'LIST',
['INDEX', 'math_integer', 1], ['ITEM', 'text', Blockly.Msg.DEFAULT_LIST_ITEM]);
};
/**
* Construct and add a data_replaceitemoflist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addReplaceItemOfList = function(xmlList, variable) {
// <block type="data_replaceitemoflist">
// <field name="LIST" variabletype="list" id="">variablename</field>
// <value name="INDEX">
// <shadow type="math_integer">
// <field name="NUM">1</field>
// </shadow>
// </value>
// <value name="ITEM">
// <shadow type="text">
// <field name="TEXT">thing</field>
// </shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_replaceitemoflist',
'LIST', ['INDEX', 'math_integer', 1], ['ITEM', 'text', Blockly.Msg.DEFAULT_LIST_ITEM]);
};
/**
* Construct and add a data_itemoflist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addItemOfList = function(xmlList, variable) {
// <block type="data_itemoflist">
// <field name="LIST" variabletype="list" id="">variablename</field>
// <value name="INDEX">
// <shadow type="math_integer">
// <field name="NUM">1</field>
// </shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_itemoflist', 'LIST',
['INDEX', 'math_integer', 1]);
};
/** Construct and add a data_itemnumoflist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addItemNumberOfList = function(xmlList, variable) {
// <block type="data_itemnumoflist">
// <value name="ITEM">
// <shadow type="text">
// <field name="TEXT">thing</field>
// </shadow>
// </value>
// <field name="LIST" variabletype="list" id="">variablename</field>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_itemnumoflist',
'LIST', ['ITEM', 'text', Blockly.Msg.DEFAULT_LIST_ITEM]);
};
/**
* Construct and add a data_lengthoflist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addLengthOfList = function(xmlList, variable) {
// <block type="data_lengthoflist">
// <field name="LIST" variabletype="list" id="">variablename</field>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_lengthoflist', 'LIST');
};
/**
* Construct and add a data_listcontainsitem block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addListContainsItem = function(xmlList, variable) {
// <block type="data_listcontainsitem">
// <field name="LIST" variabletype="list" id="">variablename</field>
// <value name="ITEM">
// <shadow type="text">
// <field name="TEXT">thing</field>
// </shadow>
// </value>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_listcontainsitem',
'LIST', ['ITEM', 'text', Blockly.Msg.DEFAULT_LIST_ITEM]);
};
/**
* Construct and add a data_showlist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addShowList = function(xmlList, variable) {
// <block type="data_showlist">
// <field name="LIST" variabletype="list" id="">variablename</field>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_showlist', 'LIST');
};
/**
* Construct and add a data_hidelist block to xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
*/
Blockly.DataCategory.addHideList = function(xmlList, variable) {
// <block type="data_hidelist">
// <field name="LIST" variabletype="list" id="">variablename</field>
// </block>
Blockly.DataCategory.addBlock(xmlList, variable, 'data_hidelist', 'LIST');
};
/**
* Construct a create variable button and push it to the xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {Blockly.Workspace} workspace Workspace to register callback to.
* @param {string} type Type of variable this is for. For example, 'LIST' or
* 'VARIABLE'.
*/
Blockly.DataCategory.addCreateButton = function(xmlList, workspace, type) {
var button = goog.dom.createDom('button');
// Set default msg, callbackKey, and callback values for type 'VARIABLE'
var msg = Blockly.Msg.NEW_VARIABLE;
var callbackKey = 'CREATE_VARIABLE';
var callback = function(button) {
Blockly.Variables.createVariable(button.getTargetWorkspace(), null, '');};
if (type === 'LIST') {
msg = Blockly.Msg.NEW_LIST;
callbackKey = 'CREATE_LIST';
callback = function(button) {
Blockly.Variables.createVariable(button.getTargetWorkspace(), null,
Blockly.LIST_VARIABLE_TYPE);};
}
button.setAttribute('text', msg);
button.setAttribute('callbackKey', callbackKey);
workspace.registerButtonCallback(callbackKey, callback);
xmlList.push(button);
};
/**
* Construct a variable block with the given variable, blockType, and optional
* value tags. Add the variable block to the given xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
* @param {?Blockly.VariableModel} variable Variable to select in the field.
* @param {string} blockType Type of block. For example, 'data_hidelist' or
* data_showlist'.
* @param {string} fieldName Name of field in block. For example: 'VARIABLE' or
* 'LIST'.
* @param {?Array.<string>} opt_value Optional array containing the value name
* and shadow type of value tags.
* @param {?Array.<string>} opt_secondValue Optional array containing the value
* name and shadow type of a second pair of value tags.
*/
Blockly.DataCategory.addBlock = function(xmlList, variable, blockType,
fieldName, opt_value, opt_secondValue) {
if (Blockly.Blocks[blockType]) {
var firstValueField;
var secondValueField;
if (opt_value) {
firstValueField = Blockly.DataCategory.createValue(opt_value[0],
opt_value[1], opt_value[2]);
}
if (opt_secondValue) {
secondValueField = Blockly.DataCategory.createValue(opt_secondValue[0],
opt_secondValue[1], opt_secondValue[2]);
}
var gap = 10;
var blockText = '<xml>' +
'<block type="' + blockType + '" gap="' + gap + '">' +
Blockly.Variables.generateVariableFieldXml_(variable, fieldName) +
firstValueField + secondValueField +
'</block>' +
'</xml>';
var block = Blockly.Xml.textToDom(blockText).firstChild;
xmlList.push(block);
}
};
/**
* Create the text representation of a value dom element with a shadow of the
* indicated type inside.
* @param {string} valueName Name of the value tags.
* @param {string} type The type of the shadow tags.
* @param {string|number} value The default shadow value.
* @return {string} The generated dom element in text.
*/
Blockly.DataCategory.createValue = function(valueName, type, value) {
var fieldName;
switch (valueName) {
case 'ITEM':
fieldName = 'TEXT';
break;
case 'INDEX':
fieldName = 'NUM';
break;
case 'VALUE':
if (type === 'math_number') {
fieldName = 'NUM';
} else {
fieldName = 'TEXT';
}
break;
}
var valueField =
'<value name="' + valueName + '">' +
'<shadow type="' + type + '">' +
'<field name="' + fieldName + '">' + value + '</field>' +
'</shadow>' +
'</value>';
return valueField;
};
/**
* Construct a block separator. Add the separator to the given xmlList.
* @param {!Array.<!Element>} xmlList Array of XML block elements.
*/
Blockly.DataCategory.addSep = function(xmlList) {
var gap = 36;
var sepText = '<xml>' +
'<sep gap="' + gap + '"/>' +
'</xml>';
var sep = Blockly.Xml.textToDom(sepText).firstChild;
xmlList.push(sep);
};

View File

@@ -0,0 +1,260 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Class that controls updates to connections during drags.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.DraggedConnectionManager');
goog.require('Blockly.BlockAnimations');
goog.require('Blockly.RenderedConnection');
goog.require('goog.math.Coordinate');
/**
* Class that controls updates to connections during drags. It is primarily
* responsible for finding the closest eligible connection and highlighting or
* unhiglighting it as needed during a drag.
* @param {!Blockly.BlockSvg} block The top block in the stack being dragged.
* @constructor
*/
Blockly.DraggedConnectionManager = function(block) {
Blockly.selected = block;
/**
* The top block in the stack being dragged.
* Does not change during a drag.
* @type {!Blockly.Block}
* @private
*/
this.topBlock_ = block;
/**
* The workspace on which these connections are being dragged.
* Does not change during a drag.
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = block.workspace;
/**
* The connections on the dragging blocks that are available to connect to
* other blocks. This includes all open connections on the top block, as well
* as the last connection on the block stack.
* Does not change during a drag.
* @type {!Array.<!Blockly.RenderedConnection>}
* @private
*/
this.availableConnections_ = this.initAvailableConnections_();
/**
* The connection that this block would connect to if released immediately.
* Updated on every mouse move.
* @type {Blockly.RenderedConnection}
* @private
*/
this.closestConnection_ = null;
/**
* The connection that would connect to this.closestConnection_ if this block
* were released immediately.
* Updated on every mouse move.
* @type {Blockly.RenderedConnection}
* @private
*/
this.localConnection_ = null;
/**
* The distance between this.closestConnection_ and this.localConnection_,
* in workspace units.
* Updated on every mouse move.
* @type {number}
* @private
*/
this.radiusConnection_ = 0;
/**
* Whether the block would be deleted if it were dropped immediately.
* Updated on every mouse move.
* @type {boolean}
* @private
*/
this.wouldDeleteBlock_ = false;
};
/**
* Sever all links from this object.
* @package
*/
Blockly.DraggedConnectionManager.prototype.dispose = function() {
this.topBlock_ = null;
this.workspace_ = null;
this.availableConnections_.length = 0;
this.closestConnection_ = null;
this.localConnection_ = null;
};
/**
* Return whether the block would be deleted if dropped immediately, based on
* information from the most recent move event.
* @return {boolean} true if the block would be deleted if dropped immediately.
* @package
*/
Blockly.DraggedConnectionManager.prototype.wouldDeleteBlock = function() {
return this.wouldDeleteBlock_;
};
/**
* Return whether the block would be connected if dropped immediately, based on
* information from the most recent move event.
* @return {boolean} true if the block would be connected if dropped immediately.
* @package
*/
Blockly.DraggedConnectionManager.prototype.wouldConnectBlock = function() {
return !!this.closestConnection_;
};
/**
* Connect to the closest connection and render the results.
* This should be called at the end of a drag.
* @package
*/
Blockly.DraggedConnectionManager.prototype.applyConnections = function() {
if (this.closestConnection_) {
// Connect two blocks together.
this.localConnection_.connect(this.closestConnection_);
if (this.topBlock_.rendered) {
// Trigger a connection animation.
// Determine which connection is inferior (lower in the source stack).
var inferiorConnection = this.localConnection_.isSuperior() ?
this.closestConnection_ : this.localConnection_;
Blockly.BlockAnimations.connectionUiEffect(
inferiorConnection.getSourceBlock());
// Bring the just-edited stack to the front.
var rootBlock = this.topBlock_.getRootBlock();
rootBlock.bringToFront();
}
this.removeHighlighting_();
}
};
/**
* Update highlighted connections based on the most recent move location.
* @param {!goog.math.Coordinate} dxy Position relative to drag start,
* in workspace units.
* @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @param {?boolean} isOutside True if the drag is going outside the blocks workspace
* @package
*/
Blockly.DraggedConnectionManager.prototype.update = function(dxy, deleteArea, isOutside) {
var oldClosestConnection;
var closestConnectionChanged;
// If dragged outside, don't connect, since the connections aren't visible.
if (!isOutside) {
oldClosestConnection = this.closestConnection_;
closestConnectionChanged = this.updateClosest_(dxy);
if (closestConnectionChanged && oldClosestConnection) {
oldClosestConnection.unhighlight();
}
} else if (this.closestConnection_) {
this.closestConnection_.unhighlight();
this.closestConnection_ = null;
}
// Prefer connecting over dropping into the trash can, but prefer dragging to
// the toolbox over connecting to other blocks.
var wouldConnect = !!this.closestConnection_ &&
deleteArea != Blockly.DELETE_AREA_TOOLBOX;
var wouldDelete = !!deleteArea && !this.topBlock_.getParent() &&
this.topBlock_.isDeletable();
this.wouldDeleteBlock_ = wouldDelete && !wouldConnect;
if (!this.wouldDeleteBlock_ && closestConnectionChanged &&
this.closestConnection_) {
this.addHighlighting_();
}
};
/**
* Remove highlighting from the currently highlighted connection, if it exists.
* @private
*/
Blockly.DraggedConnectionManager.prototype.removeHighlighting_ = function() {
if (this.closestConnection_) {
this.closestConnection_.unhighlight();
}
};
/**
* Add highlighting to the closest connection, if it exists.
* @private
*/
Blockly.DraggedConnectionManager.prototype.addHighlighting_ = function() {
if (this.closestConnection_) {
this.closestConnection_.highlight();
}
};
/**
* Populate the list of available connections on this block stack. This should
* only be called once, at the beginning of a drag.
* @return {!Array.<!Blockly.RenderedConnection>} a list of available
* connections.
* @private
*/
Blockly.DraggedConnectionManager.prototype.initAvailableConnections_ = function() {
var available = this.topBlock_.getConnections_(false);
// Also check the last connection on this stack
var lastOnStack = this.topBlock_.lastConnectionInStack();
if (lastOnStack && lastOnStack != this.topBlock_.nextConnection) {
available.push(lastOnStack);
}
return available;
};
/**
* Find the new closest connection, and update internal state in response.
* @param {!goog.math.Coordinate} dxy Position relative to the drag start,
* in workspace units.
* @return {boolean} Whether the closest connection has changed.
* @private
*/
Blockly.DraggedConnectionManager.prototype.updateClosest_ = function(dxy) {
var oldClosestConnection = this.closestConnection_;
this.closestConnection_ = null;
this.localConnection_ = null;
this.radiusConnection_ = Blockly.SNAP_RADIUS;
for (var i = 0; i < this.availableConnections_.length; i++) {
var myConnection = this.availableConnections_[i];
var neighbour = myConnection.closest(this.radiusConnection_, dxy);
if (neighbour.connection) {
this.closestConnection_ = neighbour.connection;
this.localConnection_ = myConnection;
this.radiusConnection_ = neighbour.radius;
}
}
return oldClosestConnection != this.closestConnection_;
};

View File

@@ -0,0 +1,408 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Massachusetts Institute of Technology
* All rights reserved.
*
* 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 A div that floats on top of the workspace, for drop-down menus.
* The drop-down can be kept inside the workspace, animate in/out, etc.
* @author tmickel@mit.edu (Tim Mickel)
*/
'use strict';
goog.provide('Blockly.DropDownDiv');
goog.require('goog.dom');
goog.require('goog.style');
/**
* Class for drop-down div.
* @constructor
*/
Blockly.DropDownDiv = function() {
};
/**
* The div element. Set once by Blockly.DropDownDiv.createDom.
* @type {Element}
* @private
*/
Blockly.DropDownDiv.DIV_ = null;
/**
* Drop-downs will appear within the bounds of this element if possible.
* Set in Blockly.DropDownDiv.setBoundsElement.
* @type {Element}
* @private
*/
Blockly.DropDownDiv.boundsElement_ = null;
/**
* The object currently using the drop-down.
* @type {Object}
* @private
*/
Blockly.DropDownDiv.owner_ = null;
/**
* Arrow size in px. Should match the value in CSS (need to position pre-render).
* @type {number}
* @const
*/
Blockly.DropDownDiv.ARROW_SIZE = 16;
/**
* Drop-down border size in px. Should match the value in CSS (need to position the arrow).
* @type {number}
* @const
*/
Blockly.DropDownDiv.BORDER_SIZE = 1;
/**
* Amount the arrow must be kept away from the edges of the main drop-down div, in px.
* @type {number}
* @const
*/
Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING = 12;
/**
* Amount drop-downs should be padded away from the source, in px.
* @type {number}
* @const
*/
Blockly.DropDownDiv.PADDING_Y = 20;
/**
* Length of animations in seconds.
* @type {number}
* @const
*/
Blockly.DropDownDiv.ANIMATION_TIME = 0.25;
/**
* Timer for animation out, to be cleared if we need to immediately hide
* without disrupting new shows.
* @type {number}
*/
Blockly.DropDownDiv.animateOutTimer_ = null;
/**
* Callback for when the drop-down is hidden.
* @type {Function}
*/
Blockly.DropDownDiv.onHide_ = 0;
/**
* Create and insert the DOM element for this div.
* @param {Element} container Element that the div should be contained in.
*/
Blockly.DropDownDiv.createDom = function() {
if (Blockly.DropDownDiv.DIV_) {
return; // Already created.
}
Blockly.DropDownDiv.DIV_ = goog.dom.createDom('div', 'blocklyDropDownDiv');
document.body.appendChild(Blockly.DropDownDiv.DIV_);
Blockly.DropDownDiv.content_ = goog.dom.createDom('div', 'blocklyDropDownContent');
Blockly.DropDownDiv.DIV_.appendChild(Blockly.DropDownDiv.content_);
Blockly.DropDownDiv.arrow_ = goog.dom.createDom('div', 'blocklyDropDownArrow');
Blockly.DropDownDiv.DIV_.appendChild(Blockly.DropDownDiv.arrow_);
// Transition animation for transform: translate() and opacity.
Blockly.DropDownDiv.DIV_.style.transition = 'transform ' +
Blockly.DropDownDiv.ANIMATION_TIME + 's, ' +
'opacity ' + Blockly.DropDownDiv.ANIMATION_TIME + 's';
};
/**
* Set an element to maintain bounds within. Drop-downs will appear
* within the box of this element if possible.
* @param {Element} boundsElement Element to bound drop-down to.
*/
Blockly.DropDownDiv.setBoundsElement = function(boundsElement) {
Blockly.DropDownDiv.boundsElement_ = boundsElement;
};
/**
* Provide the div for inserting content into the drop-down.
* @return {Element} Div to populate with content
*/
Blockly.DropDownDiv.getContentDiv = function() {
return Blockly.DropDownDiv.content_;
};
/**
* Clear the content of the drop-down.
*/
Blockly.DropDownDiv.clearContent = function() {
Blockly.DropDownDiv.content_.innerHTML = '';
Blockly.DropDownDiv.content_.style.width = '';
};
/**
* Set the colour for the drop-down.
* @param {string} backgroundColour Any CSS color for the background
* @param {string} borderColour Any CSS color for the border
*/
Blockly.DropDownDiv.setColour = function(backgroundColour, borderColour) {
Blockly.DropDownDiv.DIV_.style.backgroundColor = backgroundColour;
Blockly.DropDownDiv.DIV_.style.borderColor = borderColour;
};
/**
* Set the category for the drop-down.
* @param {string} category The new category for the drop-down.
*/
Blockly.DropDownDiv.setCategory = function(category) {
Blockly.DropDownDiv.DIV_.setAttribute('data-category', category);
};
/**
* Shortcut to show and place the drop-down with positioning determined
* by a particular block. The primary position will be below the block,
* and the secondary position above the block. Drop-down will be
* constrained to the block's workspace.
* @param {Object} owner The object showing the drop-down
* @param {!Blockly.Block} block Block to position the drop-down around.
* @param {Function=} opt_onHide Optional callback for when the drop-down is hidden.
* @param {Number} opt_secondaryYOffset Optional Y offset for above-block positioning.
* @return {boolean} True if the menu rendered below block; false if above.
*/
Blockly.DropDownDiv.showPositionedByBlock = function(owner, block,
opt_onHide, opt_secondaryYOffset) {
var scale = block.workspace.scale;
var bBox = {width: block.width, height: block.height};
bBox.width *= scale;
bBox.height *= scale;
var position = block.getSvgRoot().getBoundingClientRect();
// If we can fit it, render below the block.
var primaryX = position.left + bBox.width / 2;
var primaryY = position.top + bBox.height;
// If we can't fit it, render above the entire parent block.
var secondaryX = primaryX;
var secondaryY = position.top;
if (opt_secondaryYOffset) {
secondaryY += opt_secondaryYOffset;
}
// Set bounds to workspace; show the drop-down.
Blockly.DropDownDiv.setBoundsElement(block.workspace.getParentSvg().parentNode);
return Blockly.DropDownDiv.show(this, primaryX, primaryY, secondaryX, secondaryY, opt_onHide);
};
/**
* Show and place the drop-down.
* The drop-down is placed with an absolute "origin point" (x, y) - i.e.,
* the arrow will point at this origin and box will positioned below or above it.
* If we can maintain the container bounds at the primary point, the arrow will
* point there, and the container will be positioned below it.
* If we can't maintain the container bounds at the primary point, fall-back to the
* secondary point and position above.
* @param {Object} owner The object showing the drop-down
* @param {number} primaryX Desired origin point x, in absolute px
* @param {number} primaryY Desired origin point y, in absolute px
* @param {number} secondaryX Secondary/alternative origin point x, in absolute px
* @param {number} secondaryY Secondary/alternative origin point y, in absolute px
* @param {Function=} opt_onHide Optional callback for when the drop-down is hidden
* @return {boolean} True if the menu rendered at the primary origin point.
*/
Blockly.DropDownDiv.show = function(owner, primaryX, primaryY, secondaryX, secondaryY, opt_onHide) {
Blockly.DropDownDiv.owner_ = owner;
Blockly.DropDownDiv.onHide_ = opt_onHide;
var div = Blockly.DropDownDiv.DIV_;
var metrics = Blockly.DropDownDiv.getPositionMetrics(primaryX, primaryY, secondaryX, secondaryY);
// Update arrow CSS
Blockly.DropDownDiv.arrow_.style.transform = 'translate(' +
metrics.arrowX + 'px,' + metrics.arrowY + 'px) rotate(45deg)';
Blockly.DropDownDiv.arrow_.setAttribute('class',
metrics.arrowAtTop ? 'blocklyDropDownArrow arrowTop' : 'blocklyDropDownArrow arrowBottom');
// Set direction based on owner's rtl
div.style.direction = owner.sourceBlock_ && owner.sourceBlock_.RTL ? 'rtl' : 'ltr';
// When we change `translate` multiple times in close succession,
// Chrome may choose to wait and apply them all at once.
// Since we want the translation to initial X, Y to be immediate,
// and the translation to final X, Y to be animated,
// we saw problems where both would be applied after animation was turned on,
// making the dropdown appear to fly in from (0, 0).
// Using both `left`, `top` for the initial translation and then `translate`
// for the animated transition to final X, Y is a workaround.
// First apply initial translation.
div.style.left = metrics.initialX + 'px';
div.style.top = metrics.initialY + 'px';
// Show the div.
div.style.display = 'block';
div.style.opacity = 1;
// Add final translate, animated through `transition`.
// Coordinates are relative to (initialX, initialY),
// where the drop-down is absolutely positioned.
var dx = (metrics.finalX - metrics.initialX);
var dy = (metrics.finalY - metrics.initialY);
div.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
return metrics.arrowAtTop;
};
/**
* Helper to position the drop-down and the arrow, maintaining bounds.
* See explanation of origin points in Blockly.DropDownDiv.show.
* @param {number} primaryX Desired origin point x, in absolute px
* @param {number} primaryY Desired origin point y, in absolute px
* @param {number} secondaryX Secondary/alternative origin point x, in absolute px
* @param {number} secondaryY Secondary/alternative origin point y, in absolute px
* @returns {Object} Various final metrics, including rendered positions for drop-down and arrow.
*/
Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, secondaryX, secondaryY) {
var div = Blockly.DropDownDiv.DIV_;
var boundPosition = Blockly.DropDownDiv.boundsElement_.getBoundingClientRect();
var boundSize = goog.style.getSize(Blockly.DropDownDiv.boundsElement_);
var divSize = goog.style.getSize(div);
// First decide if we will render at primary or secondary position
// i.e., above or below
// renderX, renderY will eventually be the final rendered position of the box.
var renderX, renderY, renderedSecondary;
// Can the div fit inside the bounds if we render below the primary point?
if (primaryY + divSize.height > boundPosition.top + boundSize.height) {
// We can't fit below in terms of y. Can we fit above?
if (secondaryY - divSize.height < boundPosition.top) {
// We also can't fit above, so just render below anyway.
renderX = primaryX;
renderY = primaryY + Blockly.DropDownDiv.PADDING_Y;
renderedSecondary = false;
} else {
// We can fit above, render secondary
renderX = secondaryX;
renderY = secondaryY - divSize.height - Blockly.DropDownDiv.PADDING_Y;
renderedSecondary = true;
}
} else {
// We can fit below, render primary
renderX = primaryX;
renderY = primaryY + Blockly.DropDownDiv.PADDING_Y;
renderedSecondary = false;
}
// First calculate the absolute arrow X
// This needs to be done before positioning the div, since the arrow
// wants to be as close to the origin point as possible.
var arrowX = renderX - Blockly.DropDownDiv.ARROW_SIZE / 2;
// Keep in overall bounds
arrowX = Math.max(boundPosition.left, Math.min(arrowX, boundPosition.left + boundSize.width));
// Adjust the x-position of the drop-down so that the div is centered and within bounds.
var centerX = divSize.width / 2;
renderX -= centerX;
// Fit horizontally in the bounds.
renderX = Math.max(
boundPosition.left,
Math.min(renderX, boundPosition.left + boundSize.width - divSize.width)
);
// After we've finished caclulating renderX, adjust the arrow to be relative to it.
arrowX -= renderX;
// Pad the arrow by some pixels, primarily so that it doesn't render on top of a rounded border.
arrowX = Math.max(
Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING,
Math.min(arrowX, divSize.width - Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING - Blockly.DropDownDiv.ARROW_SIZE)
);
// Calculate arrow Y. If we rendered secondary, add on bottom.
// Extra pixels are added so that it covers the border of the div.
var arrowY = (renderedSecondary) ? divSize.height - Blockly.DropDownDiv.BORDER_SIZE : 0;
arrowY -= (Blockly.DropDownDiv.ARROW_SIZE / 2) + Blockly.DropDownDiv.BORDER_SIZE;
// Initial position calculated without any padding to provide an animation point.
var initialX = renderX; // X position remains constant during animation.
var initialY;
if (renderedSecondary) {
initialY = secondaryY - divSize.height; // No padding on Y
} else {
initialY = primaryY; // No padding on Y
}
return {
initialX: initialX,
initialY : initialY,
finalX: renderX,
finalY: renderY,
arrowX: arrowX,
arrowY: arrowY,
arrowAtTop: !renderedSecondary
};
};
/**
* Is the container visible?
* @return {boolean} True if visible.
*/
Blockly.DropDownDiv.isVisible = function() {
return !!Blockly.DropDownDiv.owner_;
};
/**
* Hide the menu only if it is owned by the provided object.
* @param {Object} owner Object which must be owning the drop-down to hide
* @return {Boolean} True if hidden
*/
Blockly.DropDownDiv.hideIfOwner = function(owner) {
if (Blockly.DropDownDiv.owner_ === owner) {
Blockly.DropDownDiv.hide();
return true;
}
return false;
};
/**
* Hide the menu, triggering animation.
*/
Blockly.DropDownDiv.hide = function() {
// Start the animation by setting the translation and fading out.
var div = Blockly.DropDownDiv.DIV_;
// Reset to (initialX, initialY) - i.e., no translation.
div.style.transform = 'translate(0px, 0px)';
div.style.opacity = 0;
Blockly.DropDownDiv.animateOutTimer_ = setTimeout(function() {
// Finish animation - reset all values to default.
Blockly.DropDownDiv.hideWithoutAnimation();
}, Blockly.DropDownDiv.ANIMATION_TIME * 1000);
if (Blockly.DropDownDiv.onHide_) {
Blockly.DropDownDiv.onHide_();
Blockly.DropDownDiv.onHide_ = null;
}
};
/**
* Hide the menu, without animation.
*/
Blockly.DropDownDiv.hideWithoutAnimation = function() {
if (!Blockly.DropDownDiv.isVisible()) {
return;
}
var div = Blockly.DropDownDiv.DIV_;
Blockly.DropDownDiv.animateOutTimer_ && window.clearTimeout(Blockly.DropDownDiv.animateOutTimer_);
div.style.transform = '';
div.style.top = '';
div.style.left = '';
div.style.display = 'none';
Blockly.DropDownDiv.clearContent();
Blockly.DropDownDiv.owner_ = null;
if (Blockly.DropDownDiv.onHide_) {
Blockly.DropDownDiv.onHide_();
Blockly.DropDownDiv.onHide_ = null;
}
};

View File

@@ -0,0 +1,429 @@
/**
* @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 Events fired as a result of actions in Blockly's editor.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* Events fired as a result of actions in Blockly's editor.
* @namespace Blockly.Events
*/
goog.provide('Blockly.Events');
goog.require('goog.array');
goog.require('goog.math.Coordinate');
/**
* Group ID for new events. Grouped events are indivisible.
* @type {string}
* @private
*/
Blockly.Events.group_ = '';
/**
* Sets whether events should be added to the undo stack.
* @type {boolean}
*/
Blockly.Events.recordUndo = true;
/**
* Allow change events to be created and fired.
* @type {number}
* @private
*/
Blockly.Events.disabled_ = 0;
/**
* Name of event that creates a block. Will be deprecated for BLOCK_CREATE.
* @const
*/
Blockly.Events.CREATE = 'create';
/**
* Name of event that creates a block.
* @const
*/
Blockly.Events.BLOCK_CREATE = Blockly.Events.CREATE;
/**
* Name of event that deletes a block. Will be deprecated for BLOCK_DELETE.
* @const
*/
Blockly.Events.DELETE = 'delete';
/**
* Name of event that deletes a block.
* @const
*/
Blockly.Events.BLOCK_DELETE = Blockly.Events.DELETE;
/**
* Name of event that changes a block. Will be deprecated for BLOCK_CHANGE.
* @const
*/
Blockly.Events.CHANGE = 'change';
/**
* Name of event that changes a block.
* @const
*/
Blockly.Events.BLOCK_CHANGE = Blockly.Events.CHANGE;
/**
* Name of event that moves a block. Will be deprecated for BLOCK_MOVE.
* @const
*/
Blockly.Events.MOVE = 'move';
/**
* Name of event that drags a block outside of or into the blocks workspace
* @const
*/
Blockly.Events.DRAG_OUTSIDE = 'dragOutside';
/**
* Name of event that ends a block drag
* @const
*/
Blockly.Events.END_DRAG = 'endDrag';
/**
* Name of event that moves a block.
* @const
*/
Blockly.Events.BLOCK_MOVE = Blockly.Events.MOVE;
/**
* Name of event that creates a variable.
* @const
*/
Blockly.Events.VAR_CREATE = 'var_create';
/**
* Name of event that deletes a variable.
* @const
*/
Blockly.Events.VAR_DELETE = 'var_delete';
/**
* Name of event that renames a variable.
* @const
*/
Blockly.Events.VAR_RENAME = 'var_rename';
/**
* Name of event that creates a comment.
* @const
*/
Blockly.Events.COMMENT_CREATE = 'comment_create';
/**
* Name of event that moves a comment.
* @const
*/
Blockly.Events.COMMENT_MOVE = 'comment_move';
/**
* Name of event that changes a comment's property
* (text content, size, or minimized state).
* @const
*/
Blockly.Events.COMMENT_CHANGE = 'comment_change';
/**
* Name of event that deletes a comment.
* @const
*/
Blockly.Events.COMMENT_DELETE = 'comment_delete';
/**
* Name of event that records a UI change.
* @const
*/
Blockly.Events.UI = 'ui';
/**
* List of events queued for firing.
* @private
*/
Blockly.Events.FIRE_QUEUE_ = [];
/**
* Create a custom event and fire it.
* @param {!Blockly.Events.Abstract} event Custom data for event.
*/
Blockly.Events.fire = function(event) {
if (!Blockly.Events.isEnabled()) {
return;
}
if (!Blockly.Events.FIRE_QUEUE_.length) {
// First event added; schedule a firing of the event queue.
setTimeout(Blockly.Events.fireNow_, 0);
}
Blockly.Events.FIRE_QUEUE_.push(event);
};
/**
* Fire all queued events.
* @private
*/
Blockly.Events.fireNow_ = function() {
var queue = Blockly.Events.filter(Blockly.Events.FIRE_QUEUE_, true);
Blockly.Events.FIRE_QUEUE_.length = 0;
for (var i = 0, event; event = queue[i]; i++) {
var workspace = Blockly.Workspace.getById(event.workspaceId);
if (workspace) {
workspace.fireChangeListener(event);
}
}
};
/**
* Filter the queued events and merge duplicates.
* @param {!Array.<!Blockly.Events.Abstract>} queueIn Array of events.
* @param {boolean} forward True if forward (redo), false if backward (undo).
* @return {!Array.<!Blockly.Events.Abstract>} Array of filtered events.
*/
Blockly.Events.filter = function(queueIn, forward) {
var queue = goog.array.clone(queueIn);
if (!forward) {
// Undo is merged in reverse order.
queue.reverse();
}
var mergedQueue = [];
var hash = Object.create(null);
// Merge duplicates.
for (var i = 0, event; event = queue[i]; i++) {
if (!event.isNull()) {
var key = [event.type, event.blockId, event.workspaceId].join(' ');
var lastEntry = hash[key];
var lastEvent = lastEntry ? lastEntry.event : null;
if (!lastEntry) {
// Each item in the hash table has the event and the index of that event
// in the input array. This lets us make sure we only merge adjacent
// move events.
hash[key] = {event: event, index: i};
mergedQueue.push(event);
} else if (event.type == Blockly.Events.MOVE &&
lastEntry.index == i - 1) {
// Merge move events.
lastEvent.newParentId = event.newParentId;
lastEvent.newInputName = event.newInputName;
lastEvent.newCoordinate = event.newCoordinate;
lastEntry.index = i;
} else if (event.type == Blockly.Events.CHANGE &&
event.element == lastEvent.element &&
event.name == lastEvent.name) {
// Merge change events.
lastEvent.newValue = event.newValue;
} else if (event.type == Blockly.Events.UI &&
event.element == 'click' &&
(lastEvent.element == 'commentOpen' ||
lastEvent.element == 'mutatorOpen' ||
lastEvent.element == 'warningOpen')) {
// Merge click events.
lastEvent.newValue = event.newValue;
} else {
// Collision: newer events should merge into this event to maintain order
hash[key] = {event: event, index: 1};
mergedQueue.push(event);
}
}
}
// Filter out any events that have become null due to merging.
queue = mergedQueue.filter(function(e) { return !e.isNull(); });
if (!forward) {
// Restore undo order.
queue.reverse();
}
// Move mutation events to the top of the queue.
// Intentionally skip first event.
for (var i = 1, event; event = queue[i]; i++) {
if (event.type == Blockly.Events.CHANGE &&
event.element == 'mutation') {
queue.unshift(queue.splice(i, 1)[0]);
}
}
return queue;
};
/**
* Modify pending undo events so that when they are fired they don't land
* in the undo stack. Called by Blockly.Workspace.clearUndo.
*/
Blockly.Events.clearPendingUndo = function() {
for (var i = 0, event; event = Blockly.Events.FIRE_QUEUE_[i]; i++) {
event.recordUndo = false;
}
};
/**
* Stop sending events. Every call to this function MUST also call enable.
*/
Blockly.Events.disable = function() {
Blockly.Events.disabled_++;
};
/**
* Start sending events. Unless events were already disabled when the
* corresponding call to disable was made.
*/
Blockly.Events.enable = function() {
Blockly.Events.disabled_--;
};
/**
* Returns whether events may be fired or not.
* @return {boolean} True if enabled.
*/
Blockly.Events.isEnabled = function() {
return Blockly.Events.disabled_ == 0;
};
/**
* Current group.
* @return {string} ID string.
*/
Blockly.Events.getGroup = function() {
return Blockly.Events.group_;
};
/**
* Start or stop a group.
* @param {boolean|string} state True to start new group, false to end group.
* String to set group explicitly.
*/
Blockly.Events.setGroup = function(state) {
if (typeof state == 'boolean') {
Blockly.Events.group_ = state ? Blockly.utils.genUid() : '';
} else {
Blockly.Events.group_ = state;
}
};
/**
* Compute a list of the IDs of the specified block and all its descendants.
* @param {!Blockly.Block} block The root block.
* @return {!Array.<string>} List of block IDs.
* @private
*/
Blockly.Events.getDescendantIds_ = function(block) {
var ids = [];
var descendants = block.getDescendants(false);
for (var i = 0, descendant; descendant = descendants[i]; i++) {
ids[i] = descendant.id;
}
return ids;
};
/**
* Decode the JSON into an event.
* @param {!Object} json JSON representation.
* @param {!Blockly.Workspace} workspace Target workspace for event.
* @return {!Blockly.Events.Abstract} The event represented by the JSON.
*/
Blockly.Events.fromJson = function(json, workspace) {
var event;
switch (json.type) {
case Blockly.Events.CREATE:
event = new Blockly.Events.Create(null);
break;
case Blockly.Events.DELETE:
event = new Blockly.Events.Delete(null);
break;
case Blockly.Events.CHANGE:
event = new Blockly.Events.Change(null);
break;
case Blockly.Events.MOVE:
event = new Blockly.Events.Move(null);
break;
case Blockly.Events.VAR_CREATE:
event = new Blockly.Events.VarCreate(null);
break;
case Blockly.Events.VAR_DELETE:
event = new Blockly.Events.VarDelete(null);
break;
case Blockly.Events.VAR_RENAME:
event = new Blockly.Events.VarRename(null);
break;
case Blockly.Events.COMMENT_CREATE:
event = new Blockly.Events.CommentCreate(null);
break;
case Blockly.Events.COMMENT_CHANGE:
event = new Blockly.Events.CommentChange(null);
break;
case Blockly.Events.COMMENT_MOVE:
event = new Blockly.Events.CommentMove(null);
break;
case Blockly.Events.COMMENT_DELETE:
event = new Blockly.Events.CommentDelete(null);
break;
case Blockly.Events.UI:
event = new Blockly.Events.Ui(null);
break;
case Blockly.Events.DRAG_OUTSIDE:
event = new Blockly.Events.DragBlockOutside(null);
break;
case Blockly.Events.END_DRAG:
event = new Blockly.Events.EndBlockDrag(null, false);
break;
default:
throw 'Unknown event type.';
}
event.fromJson(json);
event.workspaceId = workspace.id;
return event;
};
/**
* Enable/disable a block depending on whether it is properly connected.
* Use this on applications where all blocks should be connected to a top block.
* Recommend setting the 'disable' option to 'false' in the config so that
* users don't try to reenable disabled orphan blocks.
* @param {!Blockly.Events.Abstract} event Custom data for event.
*/
Blockly.Events.disableOrphans = function(event) {
if (event.type == Blockly.Events.MOVE ||
event.type == Blockly.Events.CREATE) {
Blockly.Events.disable();
var workspace = Blockly.Workspace.getById(event.workspaceId);
var block = workspace.getBlockById(event.blockId);
if (block) {
if (block.getParent() && !block.getParent().disabled) {
var children = block.getDescendants(false);
for (var i = 0, child; child = children[i]; i++) {
child.setDisabled(false);
}
} else if ((block.outputConnection || block.previousConnection) &&
!workspace.isDragging()) {
do {
block.setDisabled(true);
block = block.getNextBlock();
} while (block);
}
}
Blockly.Events.enable();
}
};

View File

@@ -0,0 +1,113 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Abstract class for events fired as a result of actions in
* Blockly's editor.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Events.Abstract');
goog.require('Blockly.Events');
goog.require('goog.array');
goog.require('goog.math.Coordinate');
/**
* Abstract class for an event.
* @constructor
*/
Blockly.Events.Abstract = function() {
/**
* The workspace identifier for this event.
* @type {string|undefined}
*/
this.workspaceId = undefined;
/**
* The event group id for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = Blockly.Events.group_;
/**
* Sets whether the event should be added to the undo stack.
* @type {boolean}
*/
this.recordUndo = Blockly.Events.recordUndo;
};
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.Abstract.prototype.toJson = function() {
var json = {
'type': this.type
};
if (this.group) {
json['group'] = this.group;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.Abstract.prototype.fromJson = function(json) {
this.group = json['group'];
};
/**
* Does this event record any change of state?
* By default we assume events are non-null. Subclasses may override to
* indicate that they do not change state.
* @return {boolean} False if something changed.
*/
Blockly.Events.Abstract.prototype.isNull = function() {
return false;
};
/**
* Run an event.
* @param {boolean} _forward True if run forward, false if run backward (undo).
*/
Blockly.Events.Abstract.prototype.run = function(_forward) {
// Defined by subclasses.
};
/**
* Get workspace the event belongs to.
* @return {Blockly.Workspace} The workspace the event belongs to.
* @throws {Error} if workspace is null.
* @protected
*/
Blockly.Events.Abstract.prototype.getEventWorkspace_ = function() {
var workspace = Blockly.Workspace.getById(this.workspaceId);
if (!workspace) {
throw Error('Workspace is null. Event must have been generated from real' +
' Blockly events.');
}
return workspace;
};

View File

@@ -0,0 +1,450 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Extensions are functions that help initialize blocks, usually
* adding dynamic behavior such as onchange handlers and mutators. These
* are applied using Block.applyExtension(), or the JSON "extensions"
* array attribute.
* @author Anm@anm.me (Andrew n marshall)
*/
'use strict';
/**
* @name Blockly.Extensions
* @namespace
**/
goog.provide('Blockly.Extensions');
goog.require('Blockly.Mutator');
goog.require('Blockly.utils');
goog.require('goog.string');
/**
* The set of all registered extensions, keyed by extension name/id.
* @private
*/
Blockly.Extensions.ALL_ = {};
/**
* Registers a new extension function. Extensions are functions that help
* initialize blocks, usually adding dynamic behavior such as onchange
* handlers and mutators. These are applied using Block.applyExtension(), or
* the JSON "extensions" array attribute.
* @param {string} name The name of this extension.
* @param {Function} initFn The function to initialize an extended block.
* @throws {Error} if the extension name is empty, the extension is already
* registered, or extensionFn is not a function.
*/
Blockly.Extensions.register = function(name, initFn) {
if (!goog.isString(name) || goog.string.isEmptyOrWhitespace(name)) {
throw new Error('Error: Invalid extension name "' + name + '"');
}
if (Blockly.Extensions.ALL_[name]) {
throw new Error('Error: Extension "' + name + '" is already registered.');
}
if (!goog.isFunction(initFn)) {
throw new Error('Error: Extension "' + name + '" must be a function');
}
Blockly.Extensions.ALL_[name] = initFn;
};
/**
* Registers a new extension function that adds all key/value of mixinObj.
* @param {string} name The name of this extension.
* @param {!Object} mixinObj The values to mix in.
* @throws {Error} if the extension name is empty or the extension is already
* registered.
*/
Blockly.Extensions.registerMixin = function(name, mixinObj) {
if (!goog.isObject(mixinObj)){
throw new Error('Error: Mixin "' + name + '" must be a object');
}
Blockly.Extensions.register(name, function() {
this.mixin(mixinObj);
});
};
/**
* Registers a new extension function that adds a mutator to the block.
* At register time this performs some basic sanity checks on the mutator.
* The wrapper may also add a mutator dialog to the block, if both compose and
* decompose are defined on the mixin.
* @param {string} name The name of this mutator extension.
* @param {!Object} mixinObj The values to mix in.
* @param {(function())=} opt_helperFn An optional function to apply after
* mixing in the object.
* @param {Array.<string>=} opt_blockList A list of blocks to appear in the
* flyout of the mutator dialog.
* @throws {Error} if the mutation is invalid or can't be applied to the block.
*/
Blockly.Extensions.registerMutator = function(name, mixinObj, opt_helperFn,
opt_blockList) {
var errorPrefix = 'Error when registering mutator "' + name + '": ';
// Sanity check the mixin object before registering it.
Blockly.Extensions.checkHasFunction_(
errorPrefix, mixinObj.domToMutation, 'domToMutation');
Blockly.Extensions.checkHasFunction_(
errorPrefix, mixinObj.mutationToDom, 'mutationToDom');
var hasMutatorDialog =
Blockly.Extensions.checkMutatorDialog_(mixinObj, errorPrefix);
if (opt_helperFn && !goog.isFunction(opt_helperFn)) {
throw new Error('Extension "' + name + '" is not a function');
}
// Sanity checks passed.
Blockly.Extensions.register(name, function() {
if (hasMutatorDialog) {
this.setMutator(new Blockly.Mutator(opt_blockList));
}
// Mixin the object.
this.mixin(mixinObj);
if (opt_helperFn) {
opt_helperFn.apply(this);
}
});
};
/**
* Applies an extension method to a block. This should only be called during
* block construction.
* @param {string} name The name of the extension.
* @param {!Blockly.Block} block The block to apply the named extension to.
* @param {boolean} isMutator True if this extension defines a mutator.
* @throws {Error} if the extension is not found.
*/
Blockly.Extensions.apply = function(name, block, isMutator) {
var extensionFn = Blockly.Extensions.ALL_[name];
if (!goog.isFunction(extensionFn)) {
throw new Error('Error: Extension "' + name + '" not found.');
}
if (isMutator) {
// Fail early if the block already has mutation properties.
Blockly.Extensions.checkNoMutatorProperties_(name, block);
} else {
// Record the old properties so we can make sure they don't change after
// applying the extension.
var mutatorProperties = Blockly.Extensions.getMutatorProperties_(block);
}
extensionFn.apply(block);
if (isMutator) {
var errorPrefix = 'Error after applying mutator "' + name + '": ';
Blockly.Extensions.checkBlockHasMutatorProperties_(errorPrefix, block);
} else {
if (!Blockly.Extensions.mutatorPropertiesMatch_(mutatorProperties, block)) {
throw new Error('Error when applying extension "' + name + '": ' +
'mutation properties changed when applying a non-mutator extension.');
}
}
};
/**
* Check that the given value is a function.
* @param {string} errorPrefix The string to prepend to any error message.
* @param {*} func Function to check.
* @param {string} propertyName Which property to check.
* @throws {Error} if the property does not exist or is not a function.
* @private
*/
Blockly.Extensions.checkHasFunction_ = function(errorPrefix, func,
propertyName) {
if (!func) {
throw new Error(errorPrefix +
'missing required property "' + propertyName + '"');
} else if (typeof func != 'function') {
throw new Error(errorPrefix +
'" required property "' + propertyName + '" must be a function');
}
};
/**
* Check that the given block does not have any of the four mutator properties
* defined on it. This function should be called before applying a mutator
* extension to a block, to make sure we are not overwriting properties.
* @param {string} mutationName The name of the mutation to reference in error
* messages.
* @param {!Blockly.Block} block The block to check.
* @throws {Error} if any of the properties already exist on the block.
* @private
*/
Blockly.Extensions.checkNoMutatorProperties_ = function(mutationName, block) {
var properties = Blockly.Extensions.getMutatorProperties_(block);
if (properties.length) {
throw new Error('Error: tried to apply mutation "' + mutationName +
'" to a block that already has mutator functions.' +
' Block id: ' + block.id);
}
};
/**
* Check that the given object has both or neither of the functions required
* to have a mutator dialog.
* These functions are 'compose' and 'decompose'. If a block has one, it must
* have both.
* @param {!Object} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions.
* @private
*/
Blockly.Extensions.checkMutatorDialog_ = function(object, errorPrefix) {
var hasCompose = object.compose !== undefined;
var hasDecompose = object.decompose !== undefined;
if (hasCompose && hasDecompose) {
if (typeof object.compose != 'function') {
throw new Error(errorPrefix + 'compose must be a function.');
} else if (typeof object.decompose != 'function') {
throw new Error(errorPrefix + 'decompose must be a function.');
}
return true;
} else if (!hasCompose && !hasDecompose) {
return false;
} else {
throw new Error(errorPrefix +
'Must have both or neither of "compose" and "decompose"');
}
};
/**
* Check that a block has required mutator properties. This should be called
* after applying a mutation extension.
* @param {string} errorPrefix The string to prepend to any error message.
* @param {!Blockly.Block} block The block to inspect.
* @private
*/
Blockly.Extensions.checkBlockHasMutatorProperties_ = function(errorPrefix,
block) {
if (typeof block.domToMutation !== 'function') {
throw new Error(errorPrefix + 'Applying a mutator didn\'t add "domToMutation"');
}
if (typeof block.mutationToDom != 'function') {
throw new Error(errorPrefix +
'Applying a mutator didn\'t add "mutationToDom"');
}
// A block with a mutator isn't required to have a mutation dialog, but
// it should still have both or neither of compose and decompose.
Blockly.Extensions.checkMutatorDialog_(block, errorPrefix);
};
/**
* Get a list of values of mutator properties on the given block.
* @param {!Blockly.Block} block The block to inspect.
* @return {!Array.<Object>} a list with all of the defined properties, which
* should be functions, but may be anything other than undefined.
* @private
*/
Blockly.Extensions.getMutatorProperties_ = function(block) {
var result = [];
// List each function explicitly by reference to allow for renaming
// during compilation.
if (block.domToMutation !== undefined) {
result.push(block.domToMutation);
}
if (block.mutationToDom !== undefined) {
result.push(block.mutationToDom);
}
if (block.compose !== undefined) {
result.push(block.compose);
}
if (block.decompose !== undefined) {
result.push(block.decompose);
}
return result;
};
/**
* Check that the current mutator properties match a list of old mutator
* properties. This should be called after applying a non-mutator extension,
* to verify that the extension didn't change properties it shouldn't.
* @param {!Array.<Object>} oldProperties The old values to compare to.
* @param {!Blockly.Block} block The block to inspect for new values.
* @return {boolean} True if the property lists match.
* @private
*/
Blockly.Extensions.mutatorPropertiesMatch_ = function(oldProperties, block) {
var newProperties = Blockly.Extensions.getMutatorProperties_(block);
if (newProperties.length != oldProperties.length) {
return false;
}
for (var i = 0; i < newProperties.length; i++) {
if (oldProperties[i] != newProperties[i]) {
return false;
}
}
return true;
};
/**
* Builds an extension function that will map a dropdown value to a tooltip
* string.
*
* This method includes multiple checks to ensure tooltips, dropdown options,
* and message references are aligned. This aims to catch errors as early as
* possible, without requiring developers to manually test tooltips under each
* option. After the page is loaded, each tooltip text string will be checked
* for matching message keys in the internationalized string table. Deferring
* this until the page is loaded decouples loading dependencies. Later, upon
* loading the first block of any given type, the extension will validate every
* dropdown option has a matching tooltip in the lookupTable. Errors are
* reported as warnings in the console, and are never fatal.
* @param {string} dropdownName The name of the field whose value is the key
* to the lookup table.
* @param {!Object.<string, string>} lookupTable The table of field values to
* tooltip text.
* @return {Function} The extension function.
*/
Blockly.Extensions.buildTooltipForDropdown = function(dropdownName,
lookupTable) {
// List of block types already validated, to minimize duplicate warnings.
var blockTypesChecked = [];
// Check the tooltip string messages for invalid references.
// Wait for load, in case Blockly.Msg is not yet populated.
// runAfterPageLoad() does not run in a Node.js environment due to lack of
// document object, in which case skip the validation.
if (document) { // Relies on document.readyState
Blockly.utils.runAfterPageLoad(function() {
for (var key in lookupTable) {
// Will print warnings is reference is missing.
Blockly.utils.checkMessageReferences(lookupTable[key]);
}
});
}
/**
* The actual extension.
* @this {Blockly.Block}
*/
var extensionFn = function() {
if (this.type && blockTypesChecked.indexOf(this.type) === -1) {
Blockly.Extensions.checkDropdownOptionsInTable_(
this, dropdownName, lookupTable);
blockTypesChecked.push(this.type);
}
this.setTooltip(function() {
var value = this.getFieldValue(dropdownName);
var tooltip = lookupTable[value];
if (tooltip == null) {
if (blockTypesChecked.indexOf(this.type) === -1) {
// Warn for missing values on generated tooltips.
var warning = 'No tooltip mapping for value ' + value +
' of field ' + dropdownName;
if (this.type != null) {
warning += (' of block type ' + this.type);
}
console.warn(warning + '.');
}
} else {
tooltip = Blockly.utils.replaceMessageReferences(tooltip);
}
return tooltip;
}.bind(this));
};
return extensionFn;
};
/**
* Checks all options keys are present in the provided string lookup table.
* Emits console warnings when they are not.
* @param {!Blockly.Block} block The block containing the dropdown
* @param {string} dropdownName The name of the dropdown
* @param {!Object.<string, string>} lookupTable The string lookup table
* @private
*/
Blockly.Extensions.checkDropdownOptionsInTable_ = function(block, dropdownName,
lookupTable) {
// Validate all dropdown options have values.
var dropdown = block.getField(dropdownName);
if (!dropdown.isOptionListDynamic()) {
var options = dropdown.getOptions();
for (var i = 0; i < options.length; ++i) {
var optionKey = options[i][1]; // label, then value
if (lookupTable[optionKey] == null) {
console.warn('No tooltip mapping for value ' + optionKey +
' of field ' + dropdownName + ' of block type ' + block.type);
}
}
}
};
/**
* Builds an extension function that will install a dynamic tooltip. The
* tooltip message should include the string '%1' and that string will be
* replaced with the value of the named field.
* @param {string} msgTemplate The template form to of the message text, with
* %1 placeholder.
* @param {string} fieldName The field with the replacement value.
* @returns {Function} The extension function.
*/
Blockly.Extensions.buildTooltipWithFieldValue =
function(msgTemplate, fieldName) {
// Check the tooltip string messages for invalid references.
// Wait for load, in case Blockly.Msg is not yet populated.
// runAfterPageLoad() does not run in a Node.js environment due to lack of
// document object, in which case skip the validation.
if (document) { // Relies on document.readyState
Blockly.utils.runAfterPageLoad(function() {
// Will print warnings is reference is missing.
Blockly.utils.checkMessageReferences(msgTemplate);
});
}
/**
* The actual extension.
* @this {Blockly.Block}
*/
var extensionFn = function() {
this.setTooltip(function() {
return Blockly.utils.replaceMessageReferences(msgTemplate)
.replace('%1', this.getFieldValue(fieldName));
}.bind(this));
};
return extensionFn;
};
/**
* Configures the tooltip to mimic the parent block when connected. Otherwise,
* uses the tooltip text at the time this extension is initialized. This takes
* advantage of the fact that all other values from JSON are initialized before
* extensions.
* @this {Blockly.Block}
* @private
*/
Blockly.Extensions.extensionParentTooltip_ = function() {
this.tooltipWhenNotConnected_ = this.tooltip;
this.setTooltip(function() {
var parent = this.getParent();
return (parent && parent.getInputsInline() && parent.tooltip) ||
this.tooltipWhenNotConnected_;
}.bind(this));
};
Blockly.Extensions.register('parent_tooltip_when_inline',
Blockly.Extensions.extensionParentTooltip_);

View File

@@ -0,0 +1,810 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Field. Used for editable titles, variables, etc.
* This is an abstract class that defines the UI on the block. Actual
* instances would be Blockly.FieldTextInput, Blockly.FieldDropdown, etc.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Field');
goog.require('Blockly.Events.BlockChange');
goog.require('Blockly.Gesture');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.math.Size');
goog.require('goog.style');
goog.require('goog.userAgent');
/**
* Abstract class for an editable field.
* @param {string} text The initial content of the field.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns either the accepted text, a replacement
* text, or null to abort the change.
* @constructor
*/
Blockly.Field = function(text, opt_validator) {
this.size_ = new goog.math.Size(
Blockly.BlockSvg.FIELD_WIDTH,
Blockly.BlockSvg.FIELD_HEIGHT);
this.setValue(text);
this.setValidator(opt_validator);
/**
* Maximum characters of text to display before adding an ellipsis.
* Same for strings and numbers.
* @type {number}
*/
this.maxDisplayLength = Blockly.BlockSvg.MAX_DISPLAY_LENGTH;
};
/**
* The set of all registered fields, keyed by field type as used in the JSON
* definition of a block.
* @type {!Object<string, !{fromJson: Function}>}
* @private
*/
Blockly.Field.TYPE_MAP_ = {};
/**
* Registers a field type. May also override an existing field type.
* Blockly.Field.fromJson uses this registry to find the appropriate field.
* @param {!string} type The field type name as used in the JSON definition.
* @param {!{fromJson: Function}} fieldClass The field class containing a
* fromJson function that can construct an instance of the field.
* @throws {Error} if the type name is empty, or the fieldClass is not an
* object containing a fromJson function.
*/
Blockly.Field.register = function(type, fieldClass) {
if (!goog.isString(type) || goog.string.isEmptyOrWhitespace(type)) {
throw new Error('Invalid field type "' + type + '"');
}
if (!goog.isObject(fieldClass) || !goog.isFunction(fieldClass.fromJson)) {
throw new Error('Field "' + fieldClass +
'" must have a fromJson function');
}
Blockly.Field.TYPE_MAP_[type] = fieldClass;
};
/**
* Construct a Field from a JSON arg object.
* Finds the appropriate registered field by the type name as registered using
* Blockly.Field.register.
* @param {!Object} options A JSON object with a type and options specific
* to the field type.
* @returns {?Blockly.Field} The new field instance or null if a field wasn't
* found with the given type name
* @package
*/
Blockly.Field.fromJson = function(options) {
var fieldClass = Blockly.Field.TYPE_MAP_[options['type']];
if (fieldClass) {
return fieldClass.fromJson(options);
}
return null;
};
/**
* Temporary cache of text widths.
* @type {Object}
* @private
*/
Blockly.Field.cacheWidths_ = null;
/**
* Number of current references to cache.
* @type {number}
* @private
*/
Blockly.Field.cacheReference_ = 0;
/**
* Name of field. Unique within each block.
* Static labels are usually unnamed.
* @type {string|undefined}
*/
Blockly.Field.prototype.name = undefined;
/**
* CSS class name for the text element.
* @type {string}
* @package
*/
Blockly.Field.prototype.className_ = 'blocklyText';
/**
* Visible text to display.
* @type {string}
* @private
*/
Blockly.Field.prototype.text_ = '';
/**
* Block this field is attached to. Starts as null, then in set in init.
* @type {Blockly.Block}
* @private
*/
Blockly.Field.prototype.sourceBlock_ = null;
/**
* Is the field visible, or hidden due to the block being collapsed?
* @type {boolean}
* @private
*/
Blockly.Field.prototype.visible_ = true;
/**
* Null, or an array of the field's argTypes (for styling).
* @type {Array}
* @private
*/
Blockly.Field.prototype.argType_ = null;
/**
* Validation function called when user edits an editable field.
* @type {Function}
* @private
*/
Blockly.Field.prototype.validator_ = null;
/**
* Whether to assume user is using a touch device for interactions.
* Used to show different UI for touch interactions, e.g.
* @type {boolean}
* @private
*/
Blockly.Field.prototype.useTouchInteraction_ = false;
/**
* Non-breaking space.
* @const
*/
Blockly.Field.NBSP = '\u00A0';
/**
* Text offset used for IE/Edge.
* @const
*/
Blockly.Field.IE_TEXT_OFFSET = '0.3em';
/**
* Editable fields usually show some sort of UI for the user to change them.
* @type {boolean}
* @public
*/
Blockly.Field.prototype.EDITABLE = true;
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should be serialized.
* @type {boolean}
* @public
*/
Blockly.Field.prototype.SERIALIZABLE = true;
/**
* Attach this field to a block.
* @param {!Blockly.Block} block The block containing this field.
*/
Blockly.Field.prototype.setSourceBlock = function(block) {
goog.asserts.assert(!this.sourceBlock_, 'Field already bound to a block.');
this.sourceBlock_ = block;
};
/**
* Install this field on a block.
*/
Blockly.Field.prototype.init = function() {
if (this.fieldGroup_) {
// Field has already been initialized once.
return;
}
// Build the DOM.
this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null);
if (!this.visible_) {
this.fieldGroup_.style.display = 'none';
}
// Add an attribute to cassify the type of field.
if (this.getArgTypes() !== null) {
if (this.sourceBlock_.isShadow()) {
this.sourceBlock_.svgGroup_.setAttribute('data-argument-type',
this.getArgTypes());
} else {
// Fields without a shadow wrapper, like square dropdowns.
this.fieldGroup_.setAttribute('data-argument-type', this.getArgTypes());
}
}
// Adjust X to be flipped for RTL. Position is relative to horizontal start of source block.
var size = this.getSize();
var fieldX = (this.sourceBlock_.RTL) ? -size.width / 2 : size.width / 2;
/** @type {!Element} */
this.textElement_ = Blockly.utils.createSvgElement('text',
{
'class': this.className_,
'x': fieldX,
'y': size.height / 2 + Blockly.BlockSvg.FIELD_TOP_PADDING,
'dominant-baseline': 'middle',
'dy': goog.userAgent.EDGE_OR_IE ? Blockly.Field.IE_TEXT_OFFSET : '0',
'text-anchor': 'middle'
}, this.fieldGroup_);
this.updateEditable();
this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
// Force a render.
this.render_();
this.size_.width = 0;
this.mouseDownWrapper_ = Blockly.bindEventWithChecks_(
this.getClickTarget_(), 'mousedown', this, this.onMouseDown_);
};
/**
* Initializes the model of the field after it has been installed on a block.
* No-op by default.
*/
Blockly.Field.prototype.initModel = function() {
};
/**
* Dispose of all DOM objects belonging to this editable field.
*/
Blockly.Field.prototype.dispose = function() {
if (this.mouseDownWrapper_) {
Blockly.unbindEvent_(this.mouseDownWrapper_);
this.mouseDownWrapper_ = null;
}
this.sourceBlock_ = null;
goog.dom.removeNode(this.fieldGroup_);
this.fieldGroup_ = null;
this.textElement_ = null;
this.validator_ = null;
};
/**
* Add or remove the UI indicating if this field is editable or not.
*/
Blockly.Field.prototype.updateEditable = function() {
var group = this.fieldGroup_;
if (!this.EDITABLE || !group) {
return;
}
if (this.sourceBlock_.isEditable()) {
Blockly.utils.addClass(group, 'blocklyEditableText');
Blockly.utils.removeClass(group, 'blocklyNonEditableText');
this.fieldGroup_.style.cursor = this.CURSOR;
} else {
Blockly.utils.addClass(group, 'blocklyNonEditableText');
Blockly.utils.removeClass(group, 'blocklyEditableText');
this.fieldGroup_.style.cursor = '';
}
};
/**
* Check whether this field is currently editable. Some fields are never
* editable (e.g. text labels). Those fields are not serialized to XML. Other
* fields may be editable, and therefore serialized, but may exist on
* non-editable blocks.
* @return {boolean} whether this field is editable and on an editable block
*/
Blockly.Field.prototype.isCurrentlyEditable = function() {
return this.EDITABLE && !!this.sourceBlock_ && this.sourceBlock_.isEditable();
};
/**
* Gets whether this editable field is visible or not.
* @return {boolean} True if visible.
*/
Blockly.Field.prototype.isVisible = function() {
return this.visible_;
};
/**
* Sets whether this editable field is visible or not.
* @param {boolean} visible True if visible.
*/
Blockly.Field.prototype.setVisible = function(visible) {
if (this.visible_ == visible) {
return;
}
this.visible_ = visible;
var root = this.getSvgRoot();
if (root) {
root.style.display = visible ? 'block' : 'none';
this.render_();
}
};
/**
* Adds a string to the field's array of argTypes (used for styling).
* @param {string} argType New argType.
*/
Blockly.Field.prototype.addArgType = function(argType) {
if (this.argType_ == null) {
this.argType_ = [];
}
this.argType_.push(argType);
};
/**
* Gets the field's argTypes joined as a string, or returns null (used for styling).
* @return {string} argType string, or null.
*/
Blockly.Field.prototype.getArgTypes = function() {
if (this.argType_ === null || this.argType_.length === 0) {
return null;
} else {
return this.argType_.join(' ');
}
};
/**
* Sets a new validation function for editable fields.
* @param {Function} handler New validation function, or null.
*/
Blockly.Field.prototype.setValidator = function(handler) {
this.validator_ = handler;
};
/**
* Gets the validation function for editable fields.
* @return {Function} Validation function, or null.
*/
Blockly.Field.prototype.getValidator = function() {
return this.validator_;
};
/**
* Validates a change. Does nothing. Subclasses may override this.
* @param {string} text The user's text.
* @return {string} No change needed.
*/
Blockly.Field.prototype.classValidator = function(text) {
return text;
};
/**
* Calls the validation function for this field, as well as all the validation
* function for the field's class and its parents.
* @param {string} text Proposed text.
* @return {?string} Revised text, or null if invalid.
*/
Blockly.Field.prototype.callValidator = function(text) {
var classResult = this.classValidator(text);
if (classResult === null) {
// Class validator rejects value. Game over.
return null;
} else if (classResult !== undefined) {
text = classResult;
}
var userValidator = this.getValidator();
if (userValidator) {
var userResult = userValidator.call(this, text);
if (userResult === null) {
// User validator rejects value. Game over.
return null;
} else if (userResult !== undefined) {
text = userResult;
}
}
return text;
};
/**
* Gets the group element for this editable field.
* Used for measuring the size and for positioning.
* @return {!Element} The group element.
*/
Blockly.Field.prototype.getSvgRoot = function() {
return /** @type {!Element} */ (this.fieldGroup_);
};
/**
* Draws the border with the correct width.
* Saves the computed width in a property.
* @private
*/
Blockly.Field.prototype.render_ = function() {
if (this.visible_ && this.textElement_) {
// Replace the text.
this.textElement_.textContent = this.getDisplayText_();
this.updateWidth();
// Update text centering, based on newly calculated width.
var centerTextX = (this.size_.width - this.arrowWidth_) / 2;
if (this.sourceBlock_.RTL) {
centerTextX += this.arrowWidth_;
}
// In a text-editing shadow block's field,
// if half the text length is not at least center of
// visible field (FIELD_WIDTH), center it there instead,
// unless there is a drop-down arrow.
if (this.sourceBlock_.isShadow() && !this.positionArrow) {
var minOffset = Blockly.BlockSvg.FIELD_WIDTH / 2;
if (this.sourceBlock_.RTL) {
// X position starts at the left edge of the block, in both RTL and LTR.
// First offset by the width of the block to move to the right edge,
// and then subtract to move to the same position as LTR.
var minCenter = this.size_.width - minOffset;
centerTextX = Math.min(minCenter, centerTextX);
} else {
// (width / 2) should exceed Blockly.BlockSvg.FIELD_WIDTH / 2
// if the text is longer.
centerTextX = Math.max(minOffset, centerTextX);
}
}
// Apply new text element x position.
this.textElement_.setAttribute('x', centerTextX);
}
// Update any drawn box to the correct width and height.
if (this.box_) {
this.box_.setAttribute('width', this.size_.width);
this.box_.setAttribute('height', this.size_.height);
}
};
/**
* Updates the width of the field. This calls getCachedWidth which won't cache
* the approximated width on IE/Edge when `getComputedTextLength` fails. Once
* it eventually does succeed, the result will be cached.
**/
Blockly.Field.prototype.updateWidth = function() {
// Calculate width of field
var width = Blockly.Field.getCachedWidth(this.textElement_);
// Add padding to left and right of text.
if (this.EDITABLE) {
width += Blockly.BlockSvg.EDITABLE_FIELD_PADDING;
}
// Adjust width for drop-down arrows.
this.arrowWidth_ = 0;
if (this.positionArrow) {
this.arrowWidth_ = this.positionArrow(width);
width += this.arrowWidth_;
}
// Add padding to any drawn box.
if (this.box_) {
width += 2 * Blockly.BlockSvg.BOX_FIELD_PADDING;
}
// Set width of the field.
this.size_.width = width;
};
/**
* Gets the width of a text element, caching it in the process.
* @param {!Element} textElement An SVG 'text' element.
* @return {number} Width of element.
*/
Blockly.Field.getCachedWidth = function(textElement) {
var key = textElement.textContent + '\n' + textElement.className.baseVal;
var width;
// Return the cached width if it exists.
if (Blockly.Field.cacheWidths_) {
width = Blockly.Field.cacheWidths_[key];
if (width) {
return width;
}
}
// Attempt to compute fetch the width of the SVG text element.
try {
if (goog.userAgent.IE || goog.userAgent.EDGE) {
width = textElement.getBBox().width;
} else {
width = textElement.getComputedTextLength();
}
} catch (e) {
// In other cases where we fail to geth the computed text. Instead, use an
// approximation and do not cache the result. At some later point in time
// when the block is inserted into the visible DOM, this method will be
// called again and, at that point in time, will not throw an exception.
return textElement.textContent.length * 8;
}
// Cache the computed width and return.
if (Blockly.Field.cacheWidths_) {
Blockly.Field.cacheWidths_[key] = width;
}
return width;
};
/**
* Start caching field widths. Every call to this function MUST also call
* stopCache. Caches must not survive between execution threads.
*/
Blockly.Field.startCache = function() {
Blockly.Field.cacheReference_++;
if (!Blockly.Field.cacheWidths_) {
Blockly.Field.cacheWidths_ = {};
}
};
/**
* Stop caching field widths. Unless caching was already on when the
* corresponding call to startCache was made.
*/
Blockly.Field.stopCache = function() {
Blockly.Field.cacheReference_--;
if (!Blockly.Field.cacheReference_) {
Blockly.Field.cacheWidths_ = null;
}
};
/**
* Returns the height and width of the field.
* @return {!goog.math.Size} Height and width.
*/
Blockly.Field.prototype.getSize = function() {
if (!this.size_.width) {
this.render_();
}
return this.size_;
};
/**
* Returns the bounding box of the rendered field, accounting for workspace
* scaling.
* @return {!Object} An object with top, bottom, left, and right in pixels
* relative to the top left corner of the page (window coordinates).
* @private
*/
Blockly.Field.prototype.getScaledBBox_ = function() {
var size = this.getSize();
var scaledHeight = size.height * this.sourceBlock_.workspace.scale;
var scaledWidth = size.width * this.sourceBlock_.workspace.scale;
var xy = this.getAbsoluteXY_();
return {
top: xy.y,
bottom: xy.y + scaledHeight,
left: xy.x,
right: xy.x + scaledWidth
};
};
/**
* Get the text from this field as displayed on screen. May differ from getText
* due to ellipsis, and other formatting.
* @return {string} Currently displayed text.
* @private
*/
Blockly.Field.prototype.getDisplayText_ = function() {
var text = this.text_;
if (!text) {
// Prevent the field from disappearing if empty.
return Blockly.Field.NBSP;
}
if (text.length > this.maxDisplayLength) {
// Truncate displayed string and add an ellipsis ('...').
text = text.substring(0, this.maxDisplayLength - 2) + '\u2026';
}
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
text = text.replace(/\s/g, Blockly.Field.NBSP);
if (this.sourceBlock_.RTL) {
// The SVG is LTR, force text to be RTL unless a number.
if (this.sourceBlock_.editable_ && this.sourceBlock_.type === 'math_number') {
text = '\u202A' + text + '\u202C';
} else {
text = '\u202B' + text + '\u202C';
}
}
return text;
};
/**
* Get the text from this field.
* @return {string} Current text.
*/
Blockly.Field.prototype.getText = function() {
return this.text_;
};
/**
* Set the text in this field. Trigger a rerender of the source block.
* @param {*} newText New text.
*/
Blockly.Field.prototype.setText = function(newText) {
if (newText === null) {
// No change if null.
return;
}
newText = String(newText);
if (newText === this.text_) {
// No change.
return;
}
this.text_ = newText;
this.forceRerender();
};
/**
* Force a rerender of the block that this field is installed on, which will
* rerender this field and adjust for any sizing changes.
* Other fields on the same block will not rerender, because their sizes have
* already been recorded.
* @package
*/
Blockly.Field.prototype.forceRerender = function() {
// Set width to 0 to force a rerender of this field.
this.size_.width = 0;
if (this.sourceBlock_ && this.sourceBlock_.rendered) {
this.sourceBlock_.render();
this.sourceBlock_.bumpNeighbours_();
}
};
/**
* Update the text node of this field to display the current text.
* @private
*/
Blockly.Field.prototype.updateTextNode_ = function() {
if (!this.textElement_) {
// Not rendered yet.
return;
}
var text = this.text_;
if (text.length > this.maxDisplayLength) {
// Truncate displayed string and add an ellipsis ('...').
text = text.substring(0, this.maxDisplayLength - 2) + '\u2026';
// Add special class for sizing font when truncated
this.textElement_.setAttribute('class', this.className_ + ' blocklyTextTruncated');
} else {
this.textElement_.setAttribute('class', this.className_);
}
// Empty the text element.
goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_));
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
text = text.replace(/\s/g, Blockly.Field.NBSP);
if (this.sourceBlock_.RTL && text) {
// The SVG is LTR, force text to be RTL.
if (this.sourceBlock_.editable_ && this.sourceBlock_.type === 'math_number') {
text = '\u202A' + text + '\u202C';
} else {
text = '\u202B' + text + '\u202C';
}
}
if (!text) {
// Prevent the field from disappearing if empty.
text = Blockly.Field.NBSP;
}
var textNode = document.createTextNode(text);
this.textElement_.appendChild(textNode);
// Cached width is obsolete. Clear it.
this.size_.width = 0;
};
/**
* By default there is no difference between the human-readable text and
* the language-neutral values. Subclasses (such as dropdown) may define this.
* @return {string} Current value.
*/
Blockly.Field.prototype.getValue = function() {
return this.getText();
};
/**
* By default there is no difference between the human-readable text and
* the language-neutral values. Subclasses (such as dropdown) may define this.
* @param {string} newValue New value.
*/
Blockly.Field.prototype.setValue = function(newValue) {
if (newValue === null) {
// No change if null.
return;
}
var oldValue = this.getValue();
if (oldValue == newValue) {
return;
}
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.sourceBlock_, 'field', this.name, oldValue, newValue));
}
this.setText(newValue);
};
/**
* Handle a mouse down event on a field.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Field.prototype.onMouseDown_ = function(e) {
if (!this.sourceBlock_ || !this.sourceBlock_.workspace) {
return;
}
if (this.sourceBlock_.workspace.isDragging()) {
return;
}
var gesture = this.sourceBlock_.workspace.getGesture(e);
if (gesture) {
gesture.setStartField(this);
}
this.useTouchInteraction_ = Blockly.Touch.getTouchIdentifierFromEvent(e) !== 'mouse';
};
/**
* Change the tooltip text for this field.
* @param {string|!Element} _newTip Text for tooltip or a parent element to
* link to for its tooltip.
* @abstract
*/
Blockly.Field.prototype.setTooltip = function(_newTip) {
// Non-abstract sub-classes may wish to implement this. See FieldLabel.
};
/**
* Select the element to bind the click handler to. When this element is
* clicked on an editable field, the editor will open.
*
* If the block has only one field and no output connection, we handle clicks
* over the whole block. Otherwise, handle clicks over the the group containing
* the field.
*
* @return {!Element} Element to bind click handler to.
* @private
*/
Blockly.Field.prototype.getClickTarget_ = function() {
var nFields = 0;
for (var i = 0, input; input = this.sourceBlock_.inputList[i]; i++) {
nFields += input.fieldRow.length;
}
if (nFields <= 1 && this.sourceBlock_.outputConnection) {
return this.sourceBlock_.getSvgRoot();
} else {
return this.getSvgRoot();
}
};
/**
* Return the absolute coordinates of the top-left corner of this field.
* The origin (0,0) is the top-left corner of the page body.
* @return {!goog.math.Coordinate} Object with .x and .y properties.
* @private
*/
Blockly.Field.prototype.getAbsoluteXY_ = function() {
return goog.style.getPageOffset(this.getClickTarget_());
};
/**
* Whether this field references any Blockly variables. If true it may need to
* be handled differently during serialization and deserialization. Subclasses
* may override this.
* @return {boolean} True if this field has any variable references.
* @package
*/
Blockly.Field.prototype.referencesVariables = function() {
return false;
};

View File

@@ -0,0 +1,398 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2013 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 Angle input field.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldAngle');
goog.require('Blockly.DropDownDiv');
goog.require('Blockly.FieldTextInput');
goog.require('goog.math');
goog.require('goog.userAgent');
/**
* Class for an editable angle field.
* @param {(string|number)=} opt_value The initial content of the field. The
* value should cast to a number, and if it does not, '0' will be used.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns the accepted text or null to abort
* the change.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
Blockly.FieldAngle = function(opt_value, opt_validator) {
// Add degree symbol: '360°' (LTR) or '°360' (RTL)
this.symbol_ = Blockly.utils.createSvgElement('tspan', {}, null);
this.symbol_.appendChild(document.createTextNode('\u00B0'));
var numRestrictor = new RegExp("[\\d]|[\\.]|[-]|[eE]");
opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0';
Blockly.FieldAngle.superClass_.constructor.call(
this, opt_value, opt_validator, numRestrictor);
this.addArgType('angle');
};
goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput);
/**
* Construct a FieldAngle from a JSON arg object.
* @param {!Object} options A JSON object with options (angle).
* @returns {!Blockly.FieldAngle} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldAngle.fromJson = function(options) {
return new Blockly.FieldAngle(options['angle']);
};
/**
* Round angles to the nearest 15 degrees when using mouse.
* Set to 0 to disable rounding.
*/
Blockly.FieldAngle.ROUND = 15;
/**
* Half the width of protractor image.
*/
Blockly.FieldAngle.HALF = 120 / 2;
/* The following two settings work together to set the behaviour of the angle
* picker. While many combinations are possible, two modes are typical:
* Math mode.
* 0 deg is right, 90 is up. This is the style used by protractors.
* Blockly.FieldAngle.CLOCKWISE = false;
* Blockly.FieldAngle.OFFSET = 0;
* Compass mode.
* 0 deg is up, 90 is right. This is the style used by maps.
* Blockly.FieldAngle.CLOCKWISE = true;
* Blockly.FieldAngle.OFFSET = 90;
*/
/**
* Angle increases clockwise (true) or counterclockwise (false).
*/
Blockly.FieldAngle.CLOCKWISE = true;
/**
* Offset the location of 0 degrees (and all angles) by a constant.
* Usually either 0 (0 = right) or 90 (0 = up).
*/
Blockly.FieldAngle.OFFSET = 90;
/**
* Maximum allowed angle before wrapping.
* Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180).
*/
Blockly.FieldAngle.WRAP = 180;
/**
* Radius of drag handle
*/
Blockly.FieldAngle.HANDLE_RADIUS = 10;
/**
* Width of drag handle arrow
*/
Blockly.FieldAngle.ARROW_WIDTH = Blockly.FieldAngle.HANDLE_RADIUS;
/**
* Half the stroke-width used for the "glow" around the drag handle, rounded up to nearest whole pixel
*/
Blockly.FieldAngle.HANDLE_GLOW_WIDTH = 3;
/**
* Radius of protractor circle. Slightly smaller than protractor size since
* otherwise SVG crops off half the border at the edges.
*/
Blockly.FieldAngle.RADIUS = Blockly.FieldAngle.HALF
- Blockly.FieldAngle.HANDLE_RADIUS - Blockly.FieldAngle.HANDLE_GLOW_WIDTH;
/**
* Radius of central dot circle.
*/
Blockly.FieldAngle.CENTER_RADIUS = 2;
/**
* Path to the arrow svg icon.
*/
Blockly.FieldAngle.ARROW_SVG_PATH = 'icons/arrow.svg';
/**
* Clean up this FieldAngle, as well as the inherited FieldTextInput.
* @return {!Function} Closure to call on destruction of the WidgetDiv.
* @private
*/
Blockly.FieldAngle.prototype.dispose_ = function() {
var thisField = this;
return function() {
Blockly.FieldAngle.superClass_.dispose_.call(thisField)();
thisField.gauge_ = null;
if (thisField.mouseDownWrapper_) {
Blockly.unbindEvent_(thisField.mouseDownWrapper_);
}
if (thisField.mouseUpWrapper_) {
Blockly.unbindEvent_(thisField.mouseUpWrapper_);
}
if (thisField.mouseMoveWrapper_) {
Blockly.unbindEvent_(thisField.mouseMoveWrapper_);
}
};
};
/**
* Show the inline free-text editor on top of the text.
* @private
*/
Blockly.FieldAngle.prototype.showEditor_ = function() {
// Mobile browsers have issues with in-line textareas (focus & keyboards).
Blockly.FieldAngle.superClass_.showEditor_.call(this, this.useTouchInteraction_);
// If there is an existing drop-down someone else owns, hide it immediately and clear it.
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.DropDownDiv.clearContent();
var div = Blockly.DropDownDiv.getContentDiv();
// Build the SVG DOM.
var svg = Blockly.utils.createSvgElement('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:html': 'http://www.w3.org/1999/xhtml',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'version': '1.1',
'height': (Blockly.FieldAngle.HALF * 2) + 'px',
'width': (Blockly.FieldAngle.HALF * 2) + 'px'
}, div);
Blockly.utils.createSvgElement('circle', {
'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF,
'r': Blockly.FieldAngle.RADIUS,
'class': 'blocklyAngleCircle'
}, svg);
this.gauge_ = Blockly.utils.createSvgElement('path',
{'class': 'blocklyAngleGauge'}, svg);
// The moving line, x2 and y2 are set in updateGraph_
this.line_ = Blockly.utils.createSvgElement('line',{
'x1': Blockly.FieldAngle.HALF,
'y1': Blockly.FieldAngle.HALF,
'class': 'blocklyAngleLine'
}, svg);
// The fixed vertical line at the offset
var offsetRadians = Math.PI * Blockly.FieldAngle.OFFSET / 180;
Blockly.utils.createSvgElement('line', {
'x1': Blockly.FieldAngle.HALF,
'y1': Blockly.FieldAngle.HALF,
'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS * Math.cos(offsetRadians),
'y2': Blockly.FieldAngle.HALF - Blockly.FieldAngle.RADIUS * Math.sin(offsetRadians),
'class': 'blocklyAngleLine'
}, svg);
// Draw markers around the edge.
for (var angle = 0; angle < 360; angle += 15) {
Blockly.utils.createSvgElement('line', {
'x1': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS - 13,
'y1': Blockly.FieldAngle.HALF,
'x2': Blockly.FieldAngle.HALF + Blockly.FieldAngle.RADIUS - 7,
'y2': Blockly.FieldAngle.HALF,
'class': 'blocklyAngleMarks',
'transform': 'rotate(' + angle + ',' +
Blockly.FieldAngle.HALF + ',' + Blockly.FieldAngle.HALF + ')'
}, svg);
}
// Center point
Blockly.utils.createSvgElement('circle', {
'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF,
'r': Blockly.FieldAngle.CENTER_RADIUS,
'class': 'blocklyAngleCenterPoint'
}, svg);
// Handle group: a circle and the arrow image
this.handle_ = Blockly.utils.createSvgElement('g', {}, svg);
Blockly.utils.createSvgElement('circle', {
'cx': 0,
'cy': 0,
'r': Blockly.FieldAngle.HANDLE_RADIUS,
'class': 'blocklyAngleDragHandle'
}, this.handle_);
this.arrowSvg_ = Blockly.utils.createSvgElement('image',
{
'width': Blockly.FieldAngle.ARROW_WIDTH,
'height': Blockly.FieldAngle.ARROW_WIDTH,
'x': -Blockly.FieldAngle.ARROW_WIDTH / 2,
'y': -Blockly.FieldAngle.ARROW_WIDTH / 2,
'class': 'blocklyAngleDragArrow'
},
this.handle_);
this.arrowSvg_.setAttributeNS(
'http://www.w3.org/1999/xlink',
'xlink:href',
Blockly.mainWorkspace.options.pathToMedia + Blockly.FieldAngle.ARROW_SVG_PATH
);
Blockly.DropDownDiv.setColour(this.sourceBlock_.parentBlock_.getColour(),
this.sourceBlock_.getColourTertiary());
Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory());
Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_);
this.mouseDownWrapper_ =
Blockly.bindEvent_(this.handle_, 'mousedown', this, this.onMouseDown);
this.updateGraph_();
};
/**
* Set the angle to match the mouse's position.
* @param {!Event} e Mouse move event.
*/
Blockly.FieldAngle.prototype.onMouseDown = function() {
this.mouseMoveWrapper_ = Blockly.bindEvent_(document.body, 'mousemove', this, this.onMouseMove);
this.mouseUpWrapper_ = Blockly.bindEvent_(document.body, 'mouseup', this, this.onMouseUp);
};
/**
* Set the angle to match the mouse's position.
* @param {!Event} e Mouse move event.
*/
Blockly.FieldAngle.prototype.onMouseUp = function() {
Blockly.unbindEvent_(this.mouseMoveWrapper_);
Blockly.unbindEvent_(this.mouseUpWrapper_);
};
/**
* Set the angle to match the mouse's position.
* @param {!Event} e Mouse move event.
*/
Blockly.FieldAngle.prototype.onMouseMove = function(e) {
e.preventDefault();
var bBox = this.gauge_.ownerSVGElement.getBoundingClientRect();
var dx = e.clientX - bBox.left - Blockly.FieldAngle.HALF;
var dy = e.clientY - bBox.top - Blockly.FieldAngle.HALF;
var angle = Math.atan(-dy / dx);
if (isNaN(angle)) {
// This shouldn't happen, but let's not let this error propagate further.
return;
}
angle = goog.math.toDegrees(angle);
// 0: East, 90: North, 180: West, 270: South.
if (dx < 0) {
angle += 180;
} else if (dy > 0) {
angle += 360;
}
if (Blockly.FieldAngle.CLOCKWISE) {
angle = Blockly.FieldAngle.OFFSET + 360 - angle;
} else {
angle -= Blockly.FieldAngle.OFFSET;
}
if (Blockly.FieldAngle.ROUND) {
angle = Math.round(angle / Blockly.FieldAngle.ROUND) *
Blockly.FieldAngle.ROUND;
}
angle = this.callValidator(angle);
Blockly.FieldTextInput.htmlInput_.value = angle;
this.setValue(angle);
this.validate_();
this.resizeEditor_();
};
/**
* Insert a degree symbol.
* @param {?string} text New text.
*/
Blockly.FieldAngle.prototype.setText = function(text) {
Blockly.FieldAngle.superClass_.setText.call(this, text);
if (!this.textElement_) {
// Not rendered yet.
return;
}
this.updateGraph_();
// Cached width is obsolete. Clear it.
this.size_.width = 0;
};
/**
* Redraw the graph with the current angle.
* @private
*/
Blockly.FieldAngle.prototype.updateGraph_ = function() {
if (!this.gauge_) {
return;
}
var angleDegrees = Number(this.getText()) % 360 + Blockly.FieldAngle.OFFSET;
var angleRadians = goog.math.toRadians(angleDegrees);
var path = ['M ', Blockly.FieldAngle.HALF, ',', Blockly.FieldAngle.HALF];
var x2 = Blockly.FieldAngle.HALF;
var y2 = Blockly.FieldAngle.HALF;
if (!isNaN(angleRadians)) {
var angle1 = goog.math.toRadians(Blockly.FieldAngle.OFFSET);
var x1 = Math.cos(angle1) * Blockly.FieldAngle.RADIUS;
var y1 = Math.sin(angle1) * -Blockly.FieldAngle.RADIUS;
if (Blockly.FieldAngle.CLOCKWISE) {
angleRadians = 2 * angle1 - angleRadians;
}
x2 += Math.cos(angleRadians) * Blockly.FieldAngle.RADIUS;
y2 -= Math.sin(angleRadians) * Blockly.FieldAngle.RADIUS;
// Use large arc only if input value is greater than wrap
var largeFlag = Math.abs(angleDegrees - Blockly.FieldAngle.OFFSET) > 180 ? 1 : 0;
var sweepFlag = Number(Blockly.FieldAngle.CLOCKWISE);
if (angleDegrees < Blockly.FieldAngle.OFFSET) {
sweepFlag = 1 - sweepFlag; // Sweep opposite direction if less than the offset
}
path.push(' l ', x1, ',', y1,
' A ', Blockly.FieldAngle.RADIUS, ',', Blockly.FieldAngle.RADIUS,
' 0 ', largeFlag, ' ', sweepFlag, ' ', x2, ',', y2, ' z');
// Image rotation needs to be set in degrees
if (Blockly.FieldAngle.CLOCKWISE) {
var imageRotation = angleDegrees + 2 * Blockly.FieldAngle.OFFSET;
} else {
var imageRotation = -angleDegrees;
}
this.arrowSvg_.setAttribute('transform', 'rotate(' + (imageRotation) + ')');
}
this.gauge_.setAttribute('d', path.join(''));
this.line_.setAttribute('x2', x2);
this.line_.setAttribute('y2', y2);
this.handle_.setAttribute('transform', 'translate(' + x2 + ',' + y2 + ')');
};
/**
* Ensure that only an angle may be entered.
* @param {string} text The user's text.
* @return {?string} A string representing a valid angle, or null if invalid.
*/
Blockly.FieldAngle.prototype.classValidator = function(text) {
if (text === null) {
return null;
}
var n = parseFloat(text || 0);
if (isNaN(n)) {
return null;
}
n = n % 360;
if (n < 0) {
n += 360;
}
if (n > Blockly.FieldAngle.WRAP) {
n -= 360;
}
return String(n);
};
Blockly.Field.register('field_angle', Blockly.FieldAngle);

View File

@@ -0,0 +1,133 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Checkbox field. Checked or not checked.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldCheckbox');
goog.require('Blockly.Field');
/**
* Class for a checkbox field.
* @param {string} state The initial state of the field ('TRUE' or 'FALSE').
* @param {Function=} opt_validator A function that is executed when a new
* option is selected. Its sole argument is the new checkbox state. If
* it returns a value, this becomes the new checkbox state, unless the
* value is null, in which case the change is aborted.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldCheckbox = function(state, opt_validator) {
Blockly.FieldCheckbox.superClass_.constructor.call(this, '', opt_validator);
// Set the initial state.
this.setValue(state);
this.addArgType('checkbox');
};
goog.inherits(Blockly.FieldCheckbox, Blockly.Field);
/**
* Construct a FieldCheckbox from a JSON arg object.
* @param {!Object} options A JSON object with options (checked).
* @returns {!Blockly.FieldCheckbox} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldCheckbox.fromJson = function(options) {
return new Blockly.FieldCheckbox(options['checked'] ? 'TRUE' : 'FALSE');
};
/**
* Character for the checkmark.
*/
Blockly.FieldCheckbox.CHECK_CHAR = '\u2713';
/**
* Mouse cursor style when over the hotspot that initiates editability.
*/
Blockly.FieldCheckbox.prototype.CURSOR = 'default';
/**
* Install this checkbox on a block.
*/
Blockly.FieldCheckbox.prototype.init = function() {
if (this.fieldGroup_) {
// Checkbox has already been initialized once.
return;
}
Blockly.FieldCheckbox.superClass_.init.call(this);
// The checkbox doesn't use the inherited text element.
// Instead it uses a custom checkmark element that is either visible or not.
this.checkElement_ = Blockly.utils.createSvgElement('text',
{'class': 'blocklyText blocklyCheckbox', 'x': -3, 'y': 14},
this.fieldGroup_);
var textNode = document.createTextNode(Blockly.FieldCheckbox.CHECK_CHAR);
this.checkElement_.appendChild(textNode);
this.checkElement_.style.display = this.state_ ? 'block' : 'none';
};
/**
* Return 'TRUE' if the checkbox is checked, 'FALSE' otherwise.
* @return {string} Current state.
*/
Blockly.FieldCheckbox.prototype.getValue = function() {
return String(this.state_).toUpperCase();
};
/**
* Set the checkbox to be checked if newBool is 'TRUE' or true,
* unchecks otherwise.
* @param {string|boolean} newBool New state.
*/
Blockly.FieldCheckbox.prototype.setValue = function(newBool) {
var newState = (typeof newBool == 'string') ?
(newBool.toUpperCase() == 'TRUE') : !!newBool;
if (this.state_ !== newState) {
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.sourceBlock_, 'field', this.name, this.state_, newState));
}
this.state_ = newState;
if (this.checkElement_) {
this.checkElement_.style.display = newState ? 'block' : 'none';
}
}
};
/**
* Toggle the state of the checkbox.
* @private
*/
Blockly.FieldCheckbox.prototype.showEditor_ = function() {
var newState = !this.state_;
if (this.sourceBlock_) {
// Call any validation function, and allow it to override.
newState = this.callValidator(newState);
}
if (newState !== null) {
this.setValue(String(newState).toUpperCase());
}
};
Blockly.Field.register('field_checkbox', Blockly.FieldCheckbox);

View File

@@ -0,0 +1,253 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Colour input field.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldColour');
goog.require('Blockly.Field');
goog.require('Blockly.utils');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.style');
goog.require('goog.ui.ColorPicker');
/**
* Class for a colour input field.
* @param {string} colour The initial colour in '#rrggbb' format.
* @param {Function=} opt_validator A function that is executed when a new
* colour is selected. Its sole argument is the new colour value. Its
* return value becomes the selected colour, unless it is undefined, in
* which case the new colour stands, or it is null, in which case the change
* is aborted.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldColour = function(colour, opt_validator) {
Blockly.FieldColour.superClass_.constructor.call(this, colour, opt_validator);
this.addArgType('colour');
};
goog.inherits(Blockly.FieldColour, Blockly.Field);
/**
* Construct a FieldColour from a JSON arg object.
* @param {!Object} options A JSON object with options (colour).
* @returns {!Blockly.FieldColour} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldColour.fromJson = function(options) {
return new Blockly.FieldColour(options['colour']);
};
/**
* By default use the global constants for colours.
* @type {Array.<string>}
* @private
*/
Blockly.FieldColour.prototype.colours_ = null;
/**
* By default use the global constants for columns.
* @type {number}
* @private
*/
Blockly.FieldColour.prototype.columns_ = 0;
/**
* Install this field on a block.
* @param {!Blockly.Block} block The block containing this field.
*/
Blockly.FieldColour.prototype.init = function(block) {
if (this.fieldGroup_) {
// Colour field has already been initialized once.
return;
}
Blockly.FieldColour.superClass_.init.call(this, block);
this.setValue(this.getValue());
};
/**
* Mouse cursor style when over the hotspot that initiates the editor.
*/
Blockly.FieldColour.prototype.CURSOR = 'default';
/**
* Close the colour picker if this input is being deleted.
*/
Blockly.FieldColour.prototype.dispose = function() {
Blockly.WidgetDiv.hideIfOwner(this);
Blockly.FieldColour.superClass_.dispose.call(this);
};
/**
* Return the current colour.
* @return {string} Current colour in '#rrggbb' format.
*/
Blockly.FieldColour.prototype.getValue = function() {
return this.colour_;
};
/**
* Set the colour.
* @param {string} colour The new colour in '#rrggbb' format.
*/
Blockly.FieldColour.prototype.setValue = function(colour) {
if (this.sourceBlock_ && Blockly.Events.isEnabled() &&
this.colour_ != colour) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.sourceBlock_, 'field', this.name, this.colour_, colour));
}
this.colour_ = colour;
if (this.sourceBlock_) {
// Set the primary, secondary, tertiary, and quaternary colour to this value.
// The renderer expects to be able to use the secondary color as the fill for a shadow.
this.sourceBlock_.setColour(colour, colour, colour, colour);
}
};
/**
* Get the text from this field. Used when the block is collapsed.
* @return {string} Current text.
*/
Blockly.FieldColour.prototype.getText = function() {
var colour = this.colour_;
// Try to use #rgb format if possible, rather than #rrggbb.
var m = colour.match(/^#(.)\1(.)\2(.)\3$/);
if (m) {
colour = '#' + m[1] + m[2] + m[3];
}
return colour;
};
/**
* Returns the fixed height and width.
* @return {!goog.math.Size} Height and width.
*/
Blockly.FieldColour.prototype.getSize = function() {
return new goog.math.Size(Blockly.BlockSvg.FIELD_WIDTH, Blockly.BlockSvg.FIELD_HEIGHT);
};
/**
* An array of colour strings for the palette.
* See bottom of this page for the default:
* http://docs.closure-library.googlecode.com/git/closure_goog_ui_colorpicker.js.source.html
* @type {!Array.<string>}
*/
Blockly.FieldColour.COLOURS = goog.ui.ColorPicker.SIMPLE_GRID_COLORS;
/**
* Number of columns in the palette.
*/
Blockly.FieldColour.COLUMNS = 7;
/**
* Set a custom colour grid for this field.
* @param {Array.<string>} colours Array of colours for this block,
* or null to use default (Blockly.FieldColour.COLOURS).
* @return {!Blockly.FieldColour} Returns itself (for method chaining).
*/
Blockly.FieldColour.prototype.setColours = function(colours) {
this.colours_ = colours;
return this;
};
/**
* Set a custom grid size for this field.
* @param {number} columns Number of columns for this block,
* or 0 to use default (Blockly.FieldColour.COLUMNS).
* @return {!Blockly.FieldColour} Returns itself (for method chaining).
*/
Blockly.FieldColour.prototype.setColumns = function(columns) {
this.columns_ = columns;
return this;
};
/**
* Create a palette under the colour field.
* @private
*/
Blockly.FieldColour.prototype.showEditor_ = function() {
Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL,
Blockly.FieldColour.widgetDispose_);
// Record viewport dimensions before adding the widget.
var viewportBBox = Blockly.utils.getViewportBBox();
var anchorBBox = this.getScaledBBox_();
// Create and add the colour picker, then record the size.
var picker = this.createWidget_();
var paletteSize = goog.style.getSize(picker.getElement());
// Position the picker to line up with the field.
Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, paletteSize,
this.sourceBlock_.RTL);
// Configure event handler.
var thisField = this;
Blockly.FieldColour.changeEventKey_ = goog.events.listen(picker,
goog.ui.ColorPicker.EventType.CHANGE,
function(event) {
var colour = event.target.getSelectedColor() || '#000000';
Blockly.WidgetDiv.hide();
if (thisField.sourceBlock_) {
// Call any validation function, and allow it to override.
colour = thisField.callValidator(colour);
}
if (colour !== null) {
thisField.setValue(colour);
}
});
};
/**
* Create a color picker widget and render it inside the widget div.
* @return {!goog.ui.ColorPicker} The newly created color picker.
* @private
*/
Blockly.FieldColour.prototype.createWidget_ = function() {
// Create the palette using Closure.
var picker = new goog.ui.ColorPicker();
picker.setSize(this.columns_ || Blockly.FieldColour.COLUMNS);
picker.setColors(this.colours_ || Blockly.FieldColour.COLOURS);
var div = Blockly.WidgetDiv.DIV;
picker.render(div);
picker.setSelectedColor(this.getValue());
return picker;
};
/**
* Hide the colour palette.
* @private
*/
Blockly.FieldColour.widgetDispose_ = function() {
if (Blockly.FieldColour.changeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldColour.changeEventKey_);
}
Blockly.Events.setGroup(false);
};
Blockly.Field.register('field_colour', Blockly.FieldColour);

View File

@@ -0,0 +1,387 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Colour input field.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldColourSlider');
goog.require('Blockly.Field');
goog.require('Blockly.DropDownDiv');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.style');
goog.require('goog.color');
goog.require('goog.ui.Slider');
/**
* Class for a slider-based colour input field.
* @param {string} colour The initial colour in '#rrggbb' format.
* @param {Function=} opt_validator A function that is executed when a new
* colour is selected. Its sole argument is the new colour value. Its
* return value becomes the selected colour, unless it is undefined, in
* which case the new colour stands, or it is null, in which case the change
* is aborted.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldColourSlider = function(colour, opt_validator) {
Blockly.FieldColourSlider.superClass_.constructor.call(this, colour, opt_validator);
this.addArgType('colour');
// Flag to track whether or not the slider callbacks should execute
this.sliderCallbacksEnabled_ = false;
};
goog.inherits(Blockly.FieldColourSlider, Blockly.Field);
/**
* Construct a FieldColourSlider from a JSON arg object.
* @param {!Object} options A JSON object with options (colour).
* @returns {!Blockly.FieldColourSlider} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldColourSlider.fromJson = function(options) {
return new Blockly.FieldColourSlider(options['colour']);
};
/**
* Function to be called if eyedropper can be activated.
* If defined, an eyedropper button will be added to the color picker.
* The button calls this function with a callback to update the field value.
* BEWARE: This is not a stable API, so it is being marked as private. It may change.
* @private
*/
Blockly.FieldColourSlider.activateEyedropper_ = null;
/**
* Path to the eyedropper svg icon.
*/
Blockly.FieldColourSlider.EYEDROPPER_PATH = 'eyedropper.svg';
/**
* Install this field on a block.
* @param {!Blockly.Block} block The block containing this field.
*/
Blockly.FieldColourSlider.prototype.init = function(block) {
if (this.fieldGroup_) {
// Colour slider has already been initialized once.
return;
}
Blockly.FieldColourSlider.superClass_.init.call(this, block);
this.setValue(this.getValue());
};
/**
* Return the current colour.
* @return {string} Current colour in '#rrggbb' format.
*/
Blockly.FieldColourSlider.prototype.getValue = function() {
return this.colour_;
};
/**
* Set the colour.
* @param {string} colour The new colour in '#rrggbb' format.
*/
Blockly.FieldColourSlider.prototype.setValue = function(colour) {
if (this.sourceBlock_ && Blockly.Events.isEnabled() &&
this.colour_ != colour) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.sourceBlock_, 'field', this.name, this.colour_, colour));
}
this.colour_ = colour;
if (this.sourceBlock_) {
// Set the colours to this value.
// The renderer expects to be able to use the secondary colour as the fill for a shadow.
this.sourceBlock_.setColour(colour, colour, this.sourceBlock_.getColourTertiary(),
this.sourceBlock_.getColourQuaternary());
}
this.updateSliderHandles_();
this.updateDom_();
};
/**
* Create the hue, saturation or value CSS gradient for the slide backgrounds.
* @param {string} channel Either "hue", "saturation" or "value".
* @return {string} Array colour hex colour stops for the given channel
* @private
*/
Blockly.FieldColourSlider.prototype.createColourStops_ = function(channel) {
var stops = [];
for(var n = 0; n <= 360; n += 20) {
switch (channel) {
case 'hue':
stops.push(goog.color.hsvToHex(n, this.saturation_, this.brightness_));
break;
case 'saturation':
stops.push(goog.color.hsvToHex(this.hue_, n / 360, this.brightness_));
break;
case 'brightness':
stops.push(goog.color.hsvToHex(this.hue_, this.saturation_, 255 * n / 360));
break;
default:
throw new Error("Unknown channel for colour sliders: " + channel);
}
}
return stops;
};
/**
* Set the gradient CSS properties for the given node and channel
* @param {Node} node - The DOM node the gradient will be set on.
* @param {string} channel Either "hue", "saturation" or "value".
* @private
*/
Blockly.FieldColourSlider.prototype.setGradient_ = function(node, channel) {
var gradient = this.createColourStops_(channel).join(',');
goog.style.setStyle(node, 'background',
'-moz-linear-gradient(left, ' + gradient + ')');
goog.style.setStyle(node, 'background',
'-webkit-linear-gradient(left, ' + gradient + ')');
goog.style.setStyle(node, 'background',
'-o-linear-gradient(left, ' + gradient + ')');
goog.style.setStyle(node, 'background',
'-ms-linear-gradient(left, ' + gradient + ')');
goog.style.setStyle(node, 'background',
'linear-gradient(left, ' + gradient + ')');
};
/**
* Update the readouts and slider backgrounds after value has changed.
* @private
*/
Blockly.FieldColourSlider.prototype.updateDom_ = function() {
if (this.hueSlider_) {
// Update the slider backgrounds
this.setGradient_(this.hueSlider_.getElement(), 'hue');
this.setGradient_(this.saturationSlider_.getElement(), 'saturation');
this.setGradient_(this.brightnessSlider_.getElement(), 'brightness');
// Update the readouts
this.hueReadout_.textContent = Math.floor(100 * this.hue_ / 360).toFixed(0);
this.saturationReadout_.textContent = Math.floor(100 * this.saturation_).toFixed(0);
this.brightnessReadout_.textContent = Math.floor(100 * this.brightness_ / 255).toFixed(0);
}
};
/**
* Update the slider handle positions from the current field value.
* @private
*/
Blockly.FieldColourSlider.prototype.updateSliderHandles_ = function() {
if (this.hueSlider_) {
// Don't let the following calls to setValue for each of the sliders
// trigger the slider callbacks (which then call setValue on this field again
// unnecessarily)
this.sliderCallbacksEnabled_ = false;
this.hueSlider_.setValue(this.hue_);
this.saturationSlider_.setValue(this.saturation_);
this.brightnessSlider_.setValue(this.brightness_);
this.sliderCallbacksEnabled_ = true;
}
};
/**
* Get the text from this field. Used when the block is collapsed.
* @return {string} Current text.
*/
Blockly.FieldColourSlider.prototype.getText = function() {
var colour = this.colour_;
// Try to use #rgb format if possible, rather than #rrggbb.
var m = colour.match(/^#(.)\1(.)\2(.)\3$/);
if (m) {
colour = '#' + m[1] + m[2] + m[3];
}
return colour;
};
/**
* Create label and readout DOM elements, returning the readout
* @param {string} labelText - Text for the label
* @return {Array} The container node and the readout node.
* @private
*/
Blockly.FieldColourSlider.prototype.createLabelDom_ = function(labelText) {
var labelContainer = document.createElement('div');
labelContainer.setAttribute('class', 'scratchColourPickerLabel');
var readout = document.createElement('span');
readout.setAttribute('class', 'scratchColourPickerReadout');
var label = document.createElement('span');
label.setAttribute('class', 'scratchColourPickerLabelText');
label.textContent = labelText;
labelContainer.appendChild(label);
labelContainer.appendChild(readout);
return [labelContainer, readout];
};
/**
* Factory for creating the different slider callbacks
* @param {string} channel - One of "hue", "saturation" or "brightness"
* @return {function} the callback for slider update
* @private
*/
Blockly.FieldColourSlider.prototype.sliderCallbackFactory_ = function(channel) {
var thisField = this;
return function(event) {
if (!thisField.sliderCallbacksEnabled_) return;
var channelValue = event.target.getValue();
switch (channel) {
case 'hue':
thisField.hue_ = channelValue;
break;
case 'saturation':
thisField.saturation_ = channelValue;
break;
case 'brightness':
thisField.brightness_ = channelValue;
break;
}
var colour = goog.color.hsvToHex(thisField.hue_, thisField.saturation_, thisField.brightness_);
if (thisField.sourceBlock_) {
// Call any validation function, and allow it to override.
colour = thisField.callValidator(colour);
}
if (colour !== null) {
thisField.setValue(colour, true);
}
};
};
/**
* Activate the eyedropper, passing in a callback for setting the field value.
* @private
*/
Blockly.FieldColourSlider.prototype.activateEyedropperInternal_ = function() {
var thisField = this;
Blockly.FieldColourSlider.activateEyedropper_(function(value) {
// Update the internal hue/saturation/brightness values so sliders update.
var hsv = goog.color.hexToHsv(value);
thisField.hue_ = hsv[0];
thisField.saturation_ = hsv[1];
thisField.brightness_ = hsv[2];
thisField.setValue(value);
});
};
/**
* Create hue, saturation and brightness sliders under the colour field.
* @private
*/
Blockly.FieldColourSlider.prototype.showEditor_ = function() {
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.DropDownDiv.clearContent();
var div = Blockly.DropDownDiv.getContentDiv();
// Init color component values that are used while the editor is open
// in order to keep the slider values stable.
var hsv = goog.color.hexToHsv(this.getValue());
this.hue_ = hsv[0];
this.saturation_ = hsv[1];
this.brightness_ = hsv[2];
var hueElements = this.createLabelDom_(Blockly.Msg.COLOUR_HUE_LABEL);
div.appendChild(hueElements[0]);
this.hueReadout_ = hueElements[1];
this.hueSlider_ = new goog.ui.Slider();
this.hueSlider_.setUnitIncrement(5);
this.hueSlider_.setMinimum(0);
this.hueSlider_.setMaximum(360);
this.hueSlider_.setMoveToPointEnabled(true);
this.hueSlider_.render(div);
var saturationElements =
this.createLabelDom_(Blockly.Msg.COLOUR_SATURATION_LABEL);
div.appendChild(saturationElements[0]);
this.saturationReadout_ = saturationElements[1];
this.saturationSlider_ = new goog.ui.Slider();
this.saturationSlider_.setMoveToPointEnabled(true);
this.saturationSlider_.setUnitIncrement(0.01);
this.saturationSlider_.setStep(0.001);
this.saturationSlider_.setMinimum(0);
this.saturationSlider_.setMaximum(1.0);
this.saturationSlider_.render(div);
var brightnessElements =
this.createLabelDom_(Blockly.Msg.COLOUR_BRIGHTNESS_LABEL);
div.appendChild(brightnessElements[0]);
this.brightnessReadout_ = brightnessElements[1];
this.brightnessSlider_ = new goog.ui.Slider();
this.brightnessSlider_.setUnitIncrement(2);
this.brightnessSlider_.setMinimum(0);
this.brightnessSlider_.setMaximum(255);
this.brightnessSlider_.setMoveToPointEnabled(true);
this.brightnessSlider_.render(div);
if (Blockly.FieldColourSlider.activateEyedropper_) {
var button = document.createElement('button');
button.setAttribute('class', 'scratchEyedropper');
var image = document.createElement('img');
image.src = Blockly.mainWorkspace.options.pathToMedia + Blockly.FieldColourSlider.EYEDROPPER_PATH;
button.appendChild(image);
div.appendChild(button);
Blockly.FieldColourSlider.eyedropperEventData_ =
Blockly.bindEventWithChecks_(button, 'click', this,
this.activateEyedropperInternal_);
}
Blockly.DropDownDiv.setColour(Blockly.Colours.valueReportBackground, Blockly.Colours.valueReportBorder);
Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory());
Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_);
// Set value updates the slider positions
// Do this before attaching callbacks to avoid extra events from initial set
this.setValue(this.getValue());
// Enable callbacks for the sliders
this.sliderCallbacksEnabled_ = true;
Blockly.FieldColourSlider.hueChangeEventKey_ = goog.events.listen(this.hueSlider_,
goog.ui.Component.EventType.CHANGE,
this.sliderCallbackFactory_('hue'));
Blockly.FieldColourSlider.saturationChangeEventKey_ = goog.events.listen(this.saturationSlider_,
goog.ui.Component.EventType.CHANGE,
this.sliderCallbackFactory_('saturation'));
Blockly.FieldColourSlider.brightnessChangeEventKey_ = goog.events.listen(this.brightnessSlider_,
goog.ui.Component.EventType.CHANGE,
this.sliderCallbackFactory_('brightness'));
};
Blockly.FieldColourSlider.prototype.dispose = function() {
if (Blockly.FieldColourSlider.hueChangeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldColourSlider.hueChangeEventKey_);
}
if (Blockly.FieldColourSlider.saturationChangeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldColourSlider.saturationChangeEventKey_);
}
if (Blockly.FieldColourSlider.brightnessChangeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldColourSlider.brightnessChangeEventKey_);
}
if (Blockly.FieldColourSlider.eyedropperEventData_) {
Blockly.unbindEvent_(Blockly.FieldColourSlider.eyedropperEventData_);
}
Blockly.Events.setGroup(false);
Blockly.FieldColourSlider.superClass_.dispose.call(this);
};
Blockly.Field.register('field_colour_slider', Blockly.FieldColourSlider);

View File

@@ -0,0 +1,353 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2015 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 Date input field.
* @author pkendall64@gmail.com (Paul Kendall)
*/
'use strict';
goog.provide('Blockly.FieldDate');
goog.require('Blockly.Field');
goog.require('Blockly.utils');
goog.require('goog.date');
goog.require('goog.date.DateTime');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.i18n.DateTimeSymbols');
goog.require('goog.i18n.DateTimeSymbols_he');
goog.require('goog.style');
goog.require('goog.ui.DatePicker');
/**
* Class for a date input field.
* @param {string} date The initial date.
* @param {Function=} opt_validator A function that is executed when a new
* date is selected. Its sole argument is the new date value. Its
* return value becomes the selected date, unless it is undefined, in
* which case the new date stands, or it is null, in which case the change
* is aborted.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldDate = function(date, opt_validator) {
if (!date) {
date = new goog.date.Date().toIsoString(true);
}
Blockly.FieldDate.superClass_.constructor.call(this, date, opt_validator);
this.setValue(date);
this.addArgType('date');
};
goog.inherits(Blockly.FieldDate, Blockly.Field);
/**
* Construct a FieldDate from a JSON arg object.
* @param {!Object} options A JSON object with options (date).
* @returns {!Blockly.FieldDate} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldDate.fromJson = function(options) {
return new Blockly.FieldDate(options['date']);
};
/**
* Mouse cursor style when over the hotspot that initiates the editor.
*/
Blockly.FieldDate.prototype.CURSOR = 'text';
/**
* Close the colour picker if this input is being deleted.
*/
Blockly.FieldDate.prototype.dispose = function() {
Blockly.WidgetDiv.hideIfOwner(this);
Blockly.FieldDate.superClass_.dispose.call(this);
};
/**
* Return the current date.
* @return {string} Current date.
*/
Blockly.FieldDate.prototype.getValue = function() {
return this.date_;
};
/**
* Set the date.
* @param {string} date The new date.
*/
Blockly.FieldDate.prototype.setValue = function(date) {
if (this.sourceBlock_) {
var validated = this.callValidator(date);
// If the new date is invalid, validation returns null.
// In this case we still want to display the illegal result.
if (validated !== null) {
date = validated;
}
}
this.date_ = date;
Blockly.Field.prototype.setText.call(this, date);
};
/**
* Create a date picker under the date field.
* @private
*/
Blockly.FieldDate.prototype.showEditor_ = function() {
Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL,
Blockly.FieldDate.widgetDispose_);
// Record viewport dimensions before adding the picker.
var viewportBBox = Blockly.utils.getViewportBBox();
var anchorBBox = this.getScaledBBox_();
// Create and add the date picker, then record the size.
var picker = this.createWidget_();
var pickerSize = goog.style.getSize(picker.getElement());
// Position the picker to line up with the field.
Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, pickerSize,
this.sourceBlock_.RTL);
// Configure event handler.
var thisField = this;
Blockly.FieldDate.changeEventKey_ = goog.events.listen(picker,
goog.ui.DatePicker.Events.CHANGE,
function(event) {
var date = event.date ? event.date.toIsoString(true) : '';
Blockly.WidgetDiv.hide();
if (thisField.sourceBlock_) {
// Call any validation function, and allow it to override.
date = thisField.callValidator(date);
}
thisField.setValue(date);
});
};
/**
* Create a date picker widget and render it inside the widget div.
* @return {!goog.ui.DatePicker} The newly created date picker.
* @private
*/
Blockly.FieldDate.prototype.createWidget_ = function() {
// Create the date picker using Closure.
Blockly.FieldDate.loadLanguage_();
var picker = new goog.ui.DatePicker();
picker.setAllowNone(false);
picker.setShowWeekNum(false);
var div = Blockly.WidgetDiv.DIV;
picker.render(div);
picker.setDate(goog.date.DateTime.fromIsoString(this.getValue()));
return picker;
};
/**
* Hide the date picker.
* @private
*/
Blockly.FieldDate.widgetDispose_ = function() {
if (Blockly.FieldDate.changeEventKey_) {
goog.events.unlistenByKey(Blockly.FieldDate.changeEventKey_);
}
Blockly.Events.setGroup(false);
};
/**
* Load the best language pack by scanning the Blockly.Msg object for a
* language that matches the available languages in Closure.
* @private
*/
Blockly.FieldDate.loadLanguage_ = function() {
var reg = /^DateTimeSymbols_(.+)$/;
for (var prop in goog.i18n) {
var m = prop.match(reg);
if (m) {
var lang = m[1].toLowerCase().replace('_', '.'); // E.g. 'pt.br'
if (goog.getObjectByName(lang, Blockly.Msg)) {
goog.i18n.DateTimeSymbols = goog.i18n[prop];
}
}
}
};
/**
* CSS for date picker. See css.js for use.
*/
Blockly.FieldDate.CSS = [
/* Copied from: goog/css/datepicker.css */
/**
* Copyright 2009 The Closure Library Authors. All Rights Reserved.
*
* Use of this source code is governed by the Apache License, Version 2.0.
* See the COPYING file for details.
*/
/**
* Standard styling for a goog.ui.DatePicker.
*
* @author arv@google.com (Erik Arvidsson)
*/
'.blocklyWidgetDiv .goog-date-picker,',
'.blocklyWidgetDiv .goog-date-picker th,',
'.blocklyWidgetDiv .goog-date-picker td {',
' font: 13px Arial, sans-serif;',
'}',
'.blocklyWidgetDiv .goog-date-picker {',
' -moz-user-focus: normal;',
' -moz-user-select: none;',
' position: relative;',
' border: 1px solid #000;',
' float: left;',
' padding: 2px;',
' color: #000;',
' background: #c3d9ff;',
' cursor: default;',
'}',
'.blocklyWidgetDiv .goog-date-picker th {',
' text-align: center;',
'}',
'.blocklyWidgetDiv .goog-date-picker td {',
' text-align: center;',
' vertical-align: middle;',
' padding: 1px 3px;',
'}',
'.blocklyWidgetDiv .goog-date-picker-menu {',
' position: absolute;',
' background: threedface;',
' border: 1px solid gray;',
' -moz-user-focus: normal;',
' z-index: 1;',
' outline: none;',
'}',
'.blocklyWidgetDiv .goog-date-picker-menu ul {',
' list-style: none;',
' margin: 0px;',
' padding: 0px;',
'}',
'.blocklyWidgetDiv .goog-date-picker-menu ul li {',
' cursor: default;',
'}',
'.blocklyWidgetDiv .goog-date-picker-menu-selected {',
' background: #ccf;',
'}',
'.blocklyWidgetDiv .goog-date-picker th {',
' font-size: .9em;',
'}',
'.blocklyWidgetDiv .goog-date-picker td div {',
' float: left;',
'}',
'.blocklyWidgetDiv .goog-date-picker button {',
' padding: 0px;',
' margin: 1px 0;',
' border: 0;',
' color: #20c;',
' font-weight: bold;',
' background: transparent;',
'}',
'.blocklyWidgetDiv .goog-date-picker-date {',
' background: #fff;',
'}',
'.blocklyWidgetDiv .goog-date-picker-week,',
'.blocklyWidgetDiv .goog-date-picker-wday {',
' padding: 1px 3px;',
' border: 0;',
' border-color: #a2bbdd;',
' border-style: solid;',
'}',
'.blocklyWidgetDiv .goog-date-picker-week {',
' border-right-width: 1px;',
'}',
'.blocklyWidgetDiv .goog-date-picker-wday {',
' border-bottom-width: 1px;',
'}',
'.blocklyWidgetDiv .goog-date-picker-head td {',
' text-align: center;',
'}',
/** Use td.className instead of !important */
'.blocklyWidgetDiv td.goog-date-picker-today-cont {',
' text-align: center;',
'}',
/** Use td.className instead of !important */
'.blocklyWidgetDiv td.goog-date-picker-none-cont {',
' text-align: center;',
'}',
'.blocklyWidgetDiv .goog-date-picker-month {',
' min-width: 11ex;',
' white-space: nowrap;',
'}',
'.blocklyWidgetDiv .goog-date-picker-year {',
' min-width: 6ex;',
' white-space: nowrap;',
'}',
'.blocklyWidgetDiv .goog-date-picker-monthyear {',
' white-space: nowrap;',
'}',
'.blocklyWidgetDiv .goog-date-picker table {',
' border-collapse: collapse;',
'}',
'.blocklyWidgetDiv .goog-date-picker-other-month {',
' color: #888;',
'}',
'.blocklyWidgetDiv .goog-date-picker-wkend-start,',
'.blocklyWidgetDiv .goog-date-picker-wkend-end {',
' background: #eee;',
'}',
/** Use td.className instead of !important */
'.blocklyWidgetDiv td.goog-date-picker-selected {',
' background: #c3d9ff;',
'}',
'.blocklyWidgetDiv .goog-date-picker-today {',
' background: #9ab;',
' font-weight: bold !important;',
' border-color: #246 #9bd #9bd #246;',
' color: #fff;',
'}'
];
Blockly.Field.register('field_date', Blockly.FieldDate);

View File

@@ -0,0 +1,447 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Dropdown input field. Used for editable titles and variables.
* In the interests of a consistent UI, the toolbox shares some functions and
* properties with the context menu.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldDropdown');
goog.require('Blockly.Field');
goog.require('Blockly.DropDownDiv');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.style');
goog.require('goog.ui.Menu');
goog.require('goog.ui.MenuItem');
goog.require('goog.userAgent');
/**
* Class for an editable dropdown field.
* @param {(!Array.<!Array>|!Function)} menuGenerator An array of options
* for a dropdown list, or a function which generates these options.
* @param {Function=} opt_validator A function that is executed when a new
* option is selected, with the newly selected value as its sole argument.
* If it returns a value, that value (which must be one of the options) will
* become selected in place of the newly selected option, unless the return
* value is null, in which case the change is aborted.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldDropdown = function(menuGenerator, opt_validator) {
this.menuGenerator_ = menuGenerator;
this.trimOptions_();
var firstTuple = this.getOptions()[0];
// Call parent's constructor.
Blockly.FieldDropdown.superClass_.constructor.call(this, firstTuple[1],
opt_validator);
this.addArgType('dropdown');
};
goog.inherits(Blockly.FieldDropdown, Blockly.Field);
/**
* Construct a FieldDropdown from a JSON arg object.
* @param {!Object} element A JSON object with options.
* @returns {!Blockly.FieldDropdown} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldDropdown.fromJson = function(element) {
return new Blockly.FieldDropdown(element['options']);
};
/**
* Horizontal distance that a checkmark overhangs the dropdown.
*/
Blockly.FieldDropdown.CHECKMARK_OVERHANG = 25;
/**
* Mouse cursor style when over the hotspot that initiates the editor.
*/
Blockly.FieldDropdown.prototype.CURSOR = 'default';
/**
* Closure menu item currently selected.
* @type {?goog.ui.MenuItem}
*/
Blockly.FieldDropdown.prototype.selectedItem = null;
/**
* Language-neutral currently selected string or image object.
* @type {string|!Object}
* @private
*/
Blockly.FieldDropdown.prototype.value_ = '';
/**
* SVG image element if currently selected option is an image, or null.
* @type {SVGElement}
* @private
*/
Blockly.FieldDropdown.prototype.imageElement_ = null;
/**
* Object with src, height, width, and alt attributes if currently selected
* option is an image, or null.
* @type {Object}
* @private
*/
Blockly.FieldDropdown.prototype.imageJson_ = null;
/**
* Install this dropdown on a block.
*/
Blockly.FieldDropdown.prototype.init = function() {
if (this.fieldGroup_) {
// Dropdown has already been initialized once.
return;
}
// Add dropdown arrow: "option ▾" (LTR) or "▾ אופציה" (RTL)
// Positioned on render, after text size is calculated.
/** @type {Number} */
this.arrowSize_ = 12;
/** @type {Number} */
this.arrowX_ = 0;
/** @type {Number} */
this.arrowY_ = 11;
this.arrow_ = Blockly.utils.createSvgElement('image', {
'height': this.arrowSize_ + 'px',
'width': this.arrowSize_ + 'px'
});
this.arrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'dropdown-arrow.svg');
this.className_ += ' blocklyDropdownText';
Blockly.FieldDropdown.superClass_.init.call(this);
// If not in a shadow block, draw a box.
if (!this.sourceBlock_.isShadow()) {
this.box_ = Blockly.utils.createSvgElement('rect', {
'rx': Blockly.BlockSvg.CORNER_RADIUS,
'ry': Blockly.BlockSvg.CORNER_RADIUS,
'x': 0,
'y': 0,
'width': this.size_.width,
'height': this.size_.height,
'stroke': this.sourceBlock_.getColourTertiary(),
'fill': this.sourceBlock_.getColour(),
'class': 'blocklyBlockBackground',
'fill-opacity': 1
}, null);
this.fieldGroup_.insertBefore(this.box_, this.textElement_);
}
// Force a reset of the text to add the arrow.
var text = this.text_;
this.text_ = null;
this.setText(text);
};
/**
* Create a dropdown menu under the text.
* @private
*/
Blockly.FieldDropdown.prototype.showEditor_ = function() {
var options = this.getOptions();
if (options.length == 0) return;
this.dropDownOpen_ = true;
// If there is an existing drop-down someone else owns, hide it immediately and clear it.
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.DropDownDiv.clearContent();
var contentDiv = Blockly.DropDownDiv.getContentDiv();
var thisField = this;
function callback(e) {
var menu = this;
var menuItem = e.target;
if (menuItem) {
thisField.onItemSelected(menu, menuItem);
}
Blockly.DropDownDiv.hide();
Blockly.Events.setGroup(false);
}
var menu = new goog.ui.Menu();
menu.setRightToLeft(this.sourceBlock_.RTL);
for (var i = 0; i < options.length; i++) {
var content = options[i][0]; // Human-readable text or image.
var value = options[i][1]; // Language-neutral value.
if (typeof content == 'object') {
// An image, not text.
var image = new Image(content['width'], content['height']);
image.src = content['src'];
image.alt = content['alt'] || '';
content = image;
}
var menuItem = new goog.ui.MenuItem(content);
menuItem.setRightToLeft(this.sourceBlock_.RTL);
menuItem.setValue(value);
menuItem.setCheckable(true);
menu.addChild(menuItem, true);
var checked = (value == this.value_);
menuItem.setChecked(checked);
if (checked) {
this.selectedItem = menuItem;
}
}
// Listen for mouse/keyboard events.
goog.events.listen(menu, goog.ui.Component.EventType.ACTION, callback);
// Record windowSize and scrollOffset before adding menu.
menu.render(contentDiv);
var menuDom = menu.getElement();
Blockly.utils.addClass(menuDom, 'blocklyDropdownMenu');
// Record menuSize after adding menu.
var menuSize = goog.style.getSize(menuDom);
// Recalculate height for the total content, not only box height.
menuSize.height = menuDom.scrollHeight;
var primaryColour = (this.sourceBlock_.isShadow()) ?
this.sourceBlock_.parentBlock_.getColour() : this.sourceBlock_.getColour();
Blockly.DropDownDiv.setColour(primaryColour, this.sourceBlock_.getColourTertiary());
var category = (this.sourceBlock_.isShadow()) ?
this.sourceBlock_.parentBlock_.getCategory() : this.sourceBlock_.getCategory();
Blockly.DropDownDiv.setCategory(category);
// Calculate positioning based on the field position.
var scale = this.sourceBlock_.workspace.scale;
var bBox = {width: this.size_.width, height: this.size_.height};
bBox.width *= scale;
bBox.height *= scale;
var position = this.fieldGroup_.getBoundingClientRect();
var primaryX = position.left + bBox.width / 2;
var primaryY = position.top + bBox.height;
var secondaryX = primaryX;
var secondaryY = position.top;
// Set bounds to workspace; show the drop-down.
Blockly.DropDownDiv.setBoundsElement(this.sourceBlock_.workspace.getParentSvg().parentNode);
Blockly.DropDownDiv.show(
this, primaryX, primaryY, secondaryX, secondaryY, this.onHide.bind(this));
menu.setAllowAutoFocus(true);
menuDom.focus();
// Update colour to look selected.
if (!this.disableColourChange_) {
if (this.sourceBlock_.isShadow()) {
this.sourceBlock_.setShadowColour(this.sourceBlock_.getColourQuaternary());
} else if (this.box_) {
this.box_.setAttribute('fill', this.sourceBlock_.getColourQuaternary());
}
}
};
/**
* Callback for when the drop-down is hidden.
*/
Blockly.FieldDropdown.prototype.onHide = function() {
this.dropDownOpen_ = false;
// Update colour to look selected.
if (!this.disableColourChange_ && this.sourceBlock_) {
if (this.sourceBlock_.isShadow()) {
this.sourceBlock_.clearShadowColour();
} else if (this.box_) {
this.box_.setAttribute('fill', this.sourceBlock_.getColour());
}
}
};
/**
* Handle the selection of an item in the dropdown menu.
* @param {!goog.ui.Menu} menu The Menu component clicked.
* @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu.
*/
Blockly.FieldDropdown.prototype.onItemSelected = function(menu, menuItem) {
var value = menuItem.getValue();
if (this.sourceBlock_) {
// Call any validation function, and allow it to override.
value = this.callValidator(value);
}
// If the value of the menu item is a function, call it and do not select it.
if (typeof value == 'function') {
value();
return;
}
if (value !== null) {
this.setValue(value);
}
};
/**
* @private
*/
Blockly.FieldDropdown.prototype.trimOptions_ = function() {
this.prefixField = null;
this.suffixField = null;
var options = this.menuGenerator_;
if (!goog.isArray(options)) {
return;
}
// Localize label text and image alt text.
for (var i = 0; i < options.length; i++) {
var label = options[i][0];
if (typeof label == 'string') {
options[i][0] = Blockly.utils.replaceMessageReferences(label);
} else {
if (label.alt != null) {
options[i][0].alt = Blockly.utils.replaceMessageReferences(label.alt);
}
}
}
};
/**
* @return {boolean} True if the option list is generated by a function.
* Otherwise false.
*/
Blockly.FieldDropdown.prototype.isOptionListDynamic = function() {
return goog.isFunction(this.menuGenerator_);
};
/**
* Return a list of the options for this dropdown.
* @return {!Array.<!Array>} Array of option tuples:
* (human-readable text or image, language-neutral name).
*/
Blockly.FieldDropdown.prototype.getOptions = function() {
if (goog.isFunction(this.menuGenerator_)) {
return this.menuGenerator_.call(this);
}
return /** @type {!Array.<!Array.<string>>} */ (this.menuGenerator_);
};
/**
* Get the language-neutral value from this dropdown menu.
* @return {string} Current text.
*/
Blockly.FieldDropdown.prototype.getValue = function() {
return this.value_;
};
/**
* Set the language-neutral value for this dropdown menu.
* @param {string} newValue New value to set.
*/
Blockly.FieldDropdown.prototype.setValue = function(newValue) {
if (newValue === null || newValue === this.value_) {
return; // No change if null.
}
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.sourceBlock_, 'field', this.name, this.value_, newValue));
}
// Clear menu item for old value.
if (this.selectedItem) {
this.selectedItem.setChecked(false);
this.selectedItem = null;
}
this.value_ = newValue;
// Look up and display the human-readable text.
var options = this.getOptions();
for (var i = 0; i < options.length; i++) {
// Options are tuples of human-readable text and language-neutral values.
if (options[i][1] == newValue) {
var content = options[i][0];
if (typeof content == 'object') {
this.imageJson_ = content;
this.text_ = content.alt;
} else {
this.imageJson_ = null;
this.text_ = content;
}
// Always rerender if either the value or the text has changed.
this.forceRerender();
return;
}
}
// Value not found. Add it, maybe it will become valid once set
// (like variable names).
this.text_ = newValue;
this.forceRerender();
};
/**
* Sets the text in this field. Trigger a rerender of the source block.
* @param {?string} text New text.
*/
Blockly.FieldDropdown.prototype.setText = function(text) {
if (text === null || text === this.text_) {
// No change if null.
return;
}
this.text_ = text;
this.updateTextNode_();
if (this.textElement_) {
this.textElement_.parentNode.appendChild(this.arrow_);
}
if (this.sourceBlock_ && this.sourceBlock_.rendered) {
this.sourceBlock_.render();
this.sourceBlock_.bumpNeighbours_();
}
};
/**
* Position a drop-down arrow at the appropriate location at render-time.
* @param {number} x X position the arrow is being rendered at, in px.
* @return {number} Amount of space the arrow is taking up, in px.
*/
Blockly.FieldDropdown.prototype.positionArrow = function(x) {
if (!this.arrow_) {
return 0;
}
var addedWidth = 0;
if (this.sourceBlock_.RTL) {
this.arrowX_ = this.arrowSize_ - Blockly.BlockSvg.DROPDOWN_ARROW_PADDING;
addedWidth = this.arrowSize_ + Blockly.BlockSvg.DROPDOWN_ARROW_PADDING;
} else {
this.arrowX_ = x + Blockly.BlockSvg.DROPDOWN_ARROW_PADDING / 2;
addedWidth = this.arrowSize_ + Blockly.BlockSvg.DROPDOWN_ARROW_PADDING;
}
if (this.box_) {
// Bump positioning to the right for a box-type drop-down.
this.arrowX_ += Blockly.BlockSvg.BOX_FIELD_PADDING;
}
this.arrow_.setAttribute('transform',
'translate(' + this.arrowX_ + ',' + this.arrowY_ + ')');
return addedWidth;
};
/**
* Close the dropdown menu if this input is being deleted.
*/
Blockly.FieldDropdown.prototype.dispose = function() {
this.selectedItem = null;
Blockly.WidgetDiv.hideIfOwner(this);
Blockly.FieldDropdown.superClass_.dispose.call(this);
};
Blockly.Field.register('field_dropdown', Blockly.FieldDropdown);

View File

@@ -0,0 +1,309 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Massachusetts Institute of Technology
* All rights reserved.
*
* 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 Icon picker input field.
* This is primarily for use in Scratch Horizontal blocks.
* Pops open a drop-down with icons; when an icon is selected, it replaces
* the icon (image field) in the original block.
* @author tmickel@mit.edu (Tim Mickel)
*/
'use strict';
goog.provide('Blockly.FieldIconMenu');
goog.require('Blockly.DropDownDiv');
/**
* Class for an icon menu field.
* @param {Object} icons List of icons. These take the same options as an Image Field.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldIconMenu = function(icons) {
/** @type {object} */
this.icons_ = icons;
// Example:
// [{src: '...', width: 20, height: 20, alt: '...', value: 'machine_value'}, ...]
// First icon provides the default values.
var defaultValue = icons[0].value;
Blockly.FieldIconMenu.superClass_.constructor.call(this, defaultValue);
this.addArgType('iconmenu');
};
goog.inherits(Blockly.FieldIconMenu, Blockly.Field);
/**
* Construct a FieldIconMenu from a JSON arg object.
* @param {!Object} element A JSON object with options.
* @returns {!Blockly.FieldIconMenu} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldIconMenu.fromJson = function(element) {
return new Blockly.FieldIconMenu(element['options']);
};
/**
* Fixed width of the drop-down, in px. Icon buttons will flow inside this width.
* @type {number}
* @const
*/
Blockly.FieldIconMenu.DROPDOWN_WIDTH = 168;
/**
* Save the primary colour of the source block while the menu is open, for reset.
* @type {number|string}
* @private
*/
Blockly.FieldIconMenu.savedPrimary_ = null;
/**
* Called when the field is placed on a block.
* @param {Block} block The owning block.
*/
Blockly.FieldIconMenu.prototype.init = function(block) {
if (this.fieldGroup_) {
// Icon menu has already been initialized once.
return;
}
// Render the arrow icon
// Fixed sizes in px. Saved for creating the flip transform of the menu renders above the button.
var arrowSize = 12;
/** @type {Number} */
this.arrowX_ = 18;
/** @type {Number} */
this.arrowY_ = 10;
if (block.RTL) {
// In RTL, the icon position is flipped and rendered from the right (offset by width)
this.arrowX_ = -this.arrowX_ - arrowSize;
}
/** @type {Element} */
this.arrowIcon_ = Blockly.utils.createSvgElement('image', {
'height': arrowSize + 'px',
'width': arrowSize + 'px',
'transform': 'translate(' + this.arrowX_ + ',' + this.arrowY_ + ')'
});
this.arrowIcon_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'dropdown-arrow.svg');
block.getSvgRoot().appendChild(this.arrowIcon_);
Blockly.FieldIconMenu.superClass_.init.call(this, block);
};
/**
* Mouse cursor style when over the hotspot that initiates the editor.
* @const
*/
Blockly.FieldIconMenu.prototype.CURSOR = 'default';
/**
* Set the language-neutral value for this icon drop-down menu.
* @param {?string} newValue New value.
* @override
*/
Blockly.FieldIconMenu.prototype.setValue = function(newValue) {
if (newValue === null || newValue === this.value_) {
return; // No change
}
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.Change(
this.sourceBlock_, 'field', this.name, this.value_, newValue));
}
this.value_ = newValue;
// Find the relevant icon in this.icons_ to get the image src.
this.setParentFieldImage(this.getSrcForValue(this.value_));
};
/**
* Find the parent block's FieldImage and set its src.
* @param {?string} src New src for the parent block FieldImage.
* @private
*/
Blockly.FieldIconMenu.prototype.setParentFieldImage = function(src) {
// Only attempt if we have a set sourceBlock_ and parentBlock_
// It's possible that this function could be called before
// a parent block is set; in that case, fail silently.
if (this.sourceBlock_ && this.sourceBlock_.parentBlock_) {
var parentBlock = this.sourceBlock_.parentBlock_;
// Loop through all inputs' fields to find the first FieldImage
for (var i = 0, input; input = parentBlock.inputList[i]; i++) {
for (var j = 0, field; field = input.fieldRow[j]; j++) {
if (field instanceof Blockly.FieldImage) {
// Src for a FieldImage is stored in its value.
field.setValue(src);
return;
}
}
}
}
};
/**
* Get the language-neutral value from this drop-down menu.
* @return {string} Current language-neutral value.
*/
Blockly.FieldIconMenu.prototype.getValue = function() {
return this.value_;
};
/**
* For a language-neutral value, get the src for the image that represents it.
* @param {string} value Language-neutral value to look up.
* @return {string} Src to image representing value
*/
Blockly.FieldIconMenu.prototype.getSrcForValue = function(value) {
for (var i = 0, icon; icon = this.icons_[i]; i++) {
if (icon.value === value) {
return icon.src;
}
}
};
/**
* Show the drop-down menu for editing this field.
* @private
*/
Blockly.FieldIconMenu.prototype.showEditor_ = function() {
// If there is an existing drop-down we own, this is a request to hide the drop-down.
if (Blockly.DropDownDiv.hideIfOwner(this)) {
return;
}
// If there is an existing drop-down someone else owns, hide it immediately and clear it.
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.DropDownDiv.clearContent();
// Populate the drop-down with the icons for this field.
var contentDiv = Blockly.DropDownDiv.getContentDiv();
// Accessibility properties
contentDiv.setAttribute('role', 'menu');
contentDiv.setAttribute('aria-haspopup', 'true');
for (var i = 0, icon; icon = this.icons_[i]; i++) {
// Icons with the type property placeholder take up space but don't have any functionality
// Use for special-case layouts
if (icon.type == 'placeholder') {
var placeholder = document.createElement('span');
placeholder.setAttribute('class', 'blocklyDropDownPlaceholder');
placeholder.style.width = icon.width + 'px';
placeholder.style.height = icon.height + 'px';
contentDiv.appendChild(placeholder);
continue;
}
var button = document.createElement('button');
button.setAttribute('id', ':' + i); // For aria-activedescendant
button.setAttribute('role', 'menuitem');
button.setAttribute('class', 'blocklyDropDownButton');
button.title = icon.alt;
button.style.width = icon.width + 'px';
button.style.height = icon.height + 'px';
var backgroundColor = this.sourceBlock_.getColour();
if (icon.value == this.getValue()) {
// This icon is selected, show it in a different colour
backgroundColor = this.sourceBlock_.getColourTertiary();
button.setAttribute('aria-selected', 'true');
}
button.style.backgroundColor = backgroundColor;
button.style.borderColor = this.sourceBlock_.getColourTertiary();
Blockly.bindEvent_(button, 'click', this, this.buttonClick_);
Blockly.bindEvent_(button, 'mouseup', this, this.buttonClick_);
// These are applied manually instead of using the :hover pseudoclass
// because Android has a bad long press "helper" menu and green highlight
// that we must prevent with ontouchstart preventDefault
Blockly.bindEvent_(button, 'mousedown', button, function(e) {
this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover');
e.preventDefault();
});
Blockly.bindEvent_(button, 'mouseover', button, function() {
this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover');
contentDiv.setAttribute('aria-activedescendant', this.id);
});
Blockly.bindEvent_(button, 'mouseout', button, function() {
this.setAttribute('class', 'blocklyDropDownButton');
contentDiv.removeAttribute('aria-activedescendant');
});
var buttonImg = document.createElement('img');
buttonImg.src = icon.src;
//buttonImg.alt = icon.alt;
// Upon click/touch, we will be able to get the clicked element as e.target
// Store a data attribute on all possible click targets so we can match it to the icon.
button.setAttribute('data-value', icon.value);
buttonImg.setAttribute('data-value', icon.value);
button.appendChild(buttonImg);
contentDiv.appendChild(button);
}
contentDiv.style.width = Blockly.FieldIconMenu.DROPDOWN_WIDTH + 'px';
Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), this.sourceBlock_.getColourTertiary());
Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory());
// Update source block colour to look selected
this.savedPrimary_ = this.sourceBlock_.getColour();
this.sourceBlock_.setColour(this.sourceBlock_.getColourSecondary(),
this.sourceBlock_.getColourSecondary(),
this.sourceBlock_.getColourTertiary(),
this.sourceBlock_.getColourQuaternary());
var scale = this.sourceBlock_.workspace.scale;
// Offset for icon-type horizontal blocks.
var secondaryYOffset = (
-(Blockly.BlockSvg.MIN_BLOCK_Y * scale) - (Blockly.BlockSvg.FIELD_Y_OFFSET * scale)
);
var renderedPrimary = Blockly.DropDownDiv.showPositionedByBlock(
this, this.sourceBlock_, this.onHide_.bind(this), secondaryYOffset);
if (!renderedPrimary) {
// Adjust for rotation
var arrowX = this.arrowX_ + Blockly.DropDownDiv.ARROW_SIZE / 1.5 + 1;
var arrowY = this.arrowY_ + Blockly.DropDownDiv.ARROW_SIZE / 1.5;
// Flip the arrow on the button
this.arrowIcon_.setAttribute('transform',
'translate(' + arrowX + ',' + arrowY + ') rotate(180)');}
};
/**
* Callback for when a button is clicked inside the drop-down.
* Should be bound to the FieldIconMenu.
* @param {Event} e DOM event for the click/touch
* @private
*/
Blockly.FieldIconMenu.prototype.buttonClick_ = function(e) {
var value = e.target.getAttribute('data-value');
this.setValue(value);
Blockly.DropDownDiv.hide();
};
/**
* Callback for when the drop-down is hidden.
*/
Blockly.FieldIconMenu.prototype.onHide_ = function() {
// Reset the button colour and clear accessibility properties
// Only attempt to do this reset if sourceBlock_ is not disposed.
// It could become disposed before an onHide_, for example,
// when a block is dragged from the flyout.
if (this.sourceBlock_) {
this.sourceBlock_.setColour(this.savedPrimary_,
this.sourceBlock_.getColourSecondary(),
this.sourceBlock_.getColourTertiary(),
this.sourceBlock_.getColourQuaternary());
}
Blockly.DropDownDiv.content_.removeAttribute('role');
Blockly.DropDownDiv.content_.removeAttribute('aria-haspopup');
Blockly.DropDownDiv.content_.removeAttribute('aria-activedescendant');
// Unflip the arrow if appropriate
this.arrowIcon_.setAttribute('transform', 'translate(' + this.arrowX_ + ',' + this.arrowY_ + ')');
};
Blockly.Field.register('field_iconmenu', Blockly.FieldIconMenu);

View File

@@ -0,0 +1,200 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Image field. Used for pictures, icons, etc.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldImage');
goog.require('Blockly.Field');
goog.require('goog.dom');
goog.require('goog.math.Size');
goog.require('goog.userAgent');
/**
* Class for an image on a block.
* @param {string} src The URL of the image.
* @param {number} width Width of the image.
* @param {number} height Height of the image.
* @param {string=} opt_alt Optional alt text for when block is collapsed.
* @param {boolean} flip_rtl Whether to flip the icon in RTL
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldImage = function(src, width, height, opt_alt, flip_rtl) {
this.sourceBlock_ = null;
// Ensure height and width are numbers. Strings are bad at math.
this.height_ = Number(height);
this.width_ = Number(width);
this.size_ = new goog.math.Size(this.width_, this.height_);
this.text_ = opt_alt || '';
this.flipRTL_ = flip_rtl;
this.setValue(src);
};
goog.inherits(Blockly.FieldImage, Blockly.Field);
/**
* Construct a FieldImage from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (src, width, height, alt,
* and flipRtl/flip_rtl).
* @returns {!Blockly.FieldImage} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldImage.fromJson = function(options) {
var src = Blockly.utils.replaceMessageReferences(options['src']);
var width = Number(Blockly.utils.replaceMessageReferences(options['width']));
var height =
Number(Blockly.utils.replaceMessageReferences(options['height']));
var alt = Blockly.utils.replaceMessageReferences(options['alt']);
var flip_rtl = !!options['flip_rtl'] || !!options['flipRtl'];
return new Blockly.FieldImage(src, width, height, alt, flip_rtl);
};
/**
* Editable fields are saved by the XML renderer, non-editable fields are not.
*/
Blockly.FieldImage.prototype.EDITABLE = false;
/**
* Install this image on a block.
*/
Blockly.FieldImage.prototype.init = function() {
if (this.fieldGroup_) {
// Image has already been initialized once.
return;
}
// Build the DOM.
/** @type {SVGElement} */
this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null);
if (!this.visible_) {
this.fieldGroup_.style.display = 'none';
}
/** @type {SVGElement} */
this.imageElement_ = Blockly.utils.createSvgElement(
'image',
{
'height': this.height_ + 'px',
'width': this.width_ + 'px'
},
this.fieldGroup_);
this.setValue(this.src_);
this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
// Configure the field to be transparent with respect to tooltips.
this.setTooltip(this.sourceBlock_);
Blockly.Tooltip.bindMouseEvents(this.imageElement_);
};
/**
* Dispose of all DOM objects belonging to this text.
*/
Blockly.FieldImage.prototype.dispose = function() {
goog.dom.removeNode(this.fieldGroup_);
this.fieldGroup_ = null;
this.imageElement_ = null;
};
/**
* Change the tooltip text for this field.
* @param {string|!Element} newTip Text for tooltip or a parent element to
* link to for its tooltip.
*/
Blockly.FieldImage.prototype.setTooltip = function(newTip) {
this.imageElement_.tooltip = newTip;
};
/**
* Get the source URL of this image.
* @return {string} Current text.
* @override
*/
Blockly.FieldImage.prototype.getValue = function() {
return this.src_;
};
/**
* Set the source URL of this image.
* @param {?string} src New source.
* @override
*/
Blockly.FieldImage.prototype.setValue = function(src) {
if (src === null) {
// No change if null.
return;
}
this.src_ = src;
if (this.imageElement_) {
// Extension blocks can't rely on having access to pathToMedia, so we allow this fake URL
// protocol instead.
var mediaPrefix = 'media://';
if (src.startsWith(mediaPrefix)) {
var pathToMedia = this.sourceBlock_.workspace.options.pathToMedia;
src = pathToMedia + src.substring(mediaPrefix.length);
}
this.imageElement_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', src || '');
}
};
/**
* Get whether to flip this image in RTL
* @return {boolean} True if we should flip in RTL.
*/
Blockly.FieldImage.prototype.getFlipRTL = function() {
return this.flipRTL_;
};
/**
* Set the alt text of this image.
* @param {?string} alt New alt text.
* @override
*/
Blockly.FieldImage.prototype.setText = function(alt) {
if (alt === null) {
// No change if null.
return;
}
this.text_ = alt;
};
/**
* Images are fixed width, no need to render.
* @private
*/
Blockly.FieldImage.prototype.render_ = function() {
// NOP
};
/**
* Images are fixed width, no need to update.
* @private
*/
Blockly.FieldImage.prototype.updateWidth = function() {
// NOP
};
Blockly.Field.register('field_image', Blockly.FieldImage);

View File

@@ -0,0 +1,136 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Non-editable text field. Used for titles, labels, etc.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldLabel');
goog.require('Blockly.Field');
goog.require('Blockly.Tooltip');
goog.require('goog.dom');
goog.require('goog.math.Size');
goog.require('goog.userAgent');
/**
* Class for a non-editable field.
* @param {string} text The initial content of the field.
* @param {string=} opt_class Optional CSS class for the field's text.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldLabel = function(text, opt_class) {
this.size_ = new goog.math.Size(0, 0);
this.class_ = opt_class;
this.setValue(text);
};
goog.inherits(Blockly.FieldLabel, Blockly.Field);
/**
* Construct a FieldLabel from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @returns {!Blockly.FieldLabel} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldLabel.fromJson = function(options) {
var text = Blockly.utils.replaceMessageReferences(options['text']);
return new Blockly.FieldLabel(text, options['class']);
};
/**
* Editable fields usually show some sort of UI for the user to change them.
* @type {boolean}
* @public
*/
Blockly.FieldLabel.prototype.EDITABLE = false;
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should be serialized.
* @type {boolean}
* @public
*/
Blockly.FieldLabel.prototype.SERIALIZABLE = false;
/**
* Install this text on a block.
*/
Blockly.FieldLabel.prototype.init = function() {
if (this.textElement_) {
// Text has already been initialized once.
return;
}
// Build the DOM.
this.textElement_ = Blockly.utils.createSvgElement('text',
{
'class': 'blocklyText',
'y': Blockly.BlockSvg.FIELD_TOP_PADDING,
'text-anchor': 'middle',
'dominant-baseline': 'middle',
'dy': goog.userAgent.EDGE_OR_IE ? Blockly.Field.IE_TEXT_OFFSET : '0'
}, null);
if (this.class_) {
Blockly.utils.addClass(this.textElement_, this.class_);
}
if (!this.visible_) {
this.textElement_.style.display = 'none';
}
this.sourceBlock_.getSvgRoot().appendChild(this.textElement_);
// Configure the field to be transparent with respect to tooltips.
this.textElement_.tooltip = this.sourceBlock_;
Blockly.Tooltip.bindMouseEvents(this.textElement_);
// Force a render.
this.render_();
};
/**
* Dispose of all DOM objects belonging to this text.
*/
Blockly.FieldLabel.prototype.dispose = function() {
goog.dom.removeNode(this.textElement_);
this.textElement_ = null;
};
/**
* Gets the group element for this field.
* Used for measuring the size and for positioning.
* @return {!Element} The group element.
*/
Blockly.FieldLabel.prototype.getSvgRoot = function() {
return /** @type {!Element} */ (this.textElement_);
};
/**
* Change the tooltip text for this field.
* @param {string|!Element} newTip Text for tooltip or a parent element to
* link to for its tooltip.
*/
Blockly.FieldLabel.prototype.setTooltip = function(newTip) {
this.textElement_.tooltip = newTip;
};
Blockly.Field.register('field_label', Blockly.FieldLabel);

View File

@@ -0,0 +1,125 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Serialized label field. Behaves like a normal label but is
* always serialized to XML. It may only be edited programmatically.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.FieldLabelSerializable');
goog.require('Blockly.FieldLabel');
/**
* Class for a variable getter field.
* @param {string} text The initial content of the field.
* @param {string} opt_class Optional CSS class for the field's text.
* @extends {Blockly.FieldLabel}
* @constructor
*
*/
Blockly.FieldLabelSerializable = function(text, opt_class) {
Blockly.FieldLabelSerializable.superClass_.constructor.call(this, text,
opt_class);
// Used in base field rendering, but we don't need it.
this.arrowWidth_ = 0;
};
goog.inherits(Blockly.FieldLabelSerializable, Blockly.FieldLabel);
/**
* Construct a FieldLabelSerializable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @returns {!Blockly.FieldLabelSerializable} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldLabelSerializable.fromJson = function(options) {
var text = Blockly.utils.replaceMessageReferences(options['text']);
return new Blockly.FieldLabelSerializable(text, options['class']);
};
/**
* Editable fields usually show some sort of UI for the user to change them.
* This field should be serialized, but only edited programmatically.
* @type {boolean}
* @public
*/
Blockly.FieldLabelSerializable.prototype.EDITABLE = false;
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. This field should be serialized, but only edited programmatically.
* @type {boolean}
* @public
*/
Blockly.FieldLabelSerializable.prototype.SERIALIZABLE = true;
/**
* Updates the width of the field. This calls getCachedWidth which won't cache
* the approximated width on IE/Edge when `getComputedTextLength` fails. Once
* it eventually does succeed, the result will be cached.
**/
Blockly.FieldLabelSerializable.prototype.updateWidth = function() {
// Set width of the field.
// Unlike the base Field class, this doesn't add space to editable fields.
this.size_.width = Blockly.Field.getCachedWidth(this.textElement_);
};
/**
* Draws the border with the correct width.
* Saves the computed width in a property.
* @private
*/
Blockly.FieldLabelSerializable.prototype.render_ = function() {
if (this.visible_ && this.textElement_) {
// Replace the text.
goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_));
var textNode = document.createTextNode(this.getDisplayText_());
this.textElement_.appendChild(textNode);
this.updateWidth();
// Update text centering, based on newly calculated width.
var centerTextX = this.size_.width / 2;
// If half the text length is not at least center of
// visible field (FIELD_WIDTH), center it there instead.
var minOffset = Blockly.BlockSvg.FIELD_WIDTH / 2;
if (this.sourceBlock_.RTL) {
// X position starts at the left edge of the block, in both RTL and LTR.
// First offset by the width of the block to move to the right edge,
// and then subtract to move to the same position as LTR.
var minCenter = this.size_.width - minOffset;
centerTextX = Math.min(minCenter, centerTextX);
} else {
// (width / 2) should exceed Blockly.BlockSvg.FIELD_WIDTH / 2
// if the text is longer.
centerTextX = Math.max(minOffset, centerTextX);
}
// Apply new text element x position.
this.textElement_.setAttribute('x', centerTextX);
}
};
Blockly.Field.register(
'field_label_serializable', Blockly.FieldLabelSerializable);

View File

@@ -0,0 +1,566 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Massachusetts Institute of Technology
* All rights reserved.
*
* 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 5x5 matrix input field.
* Displays an editable 5x5 matrix for controlling LED arrays.
* @author khanning@gmail.com (Kreg Hanning)
*/
'use strict';
goog.provide('Blockly.FieldMatrix');
goog.require('Blockly.DropDownDiv');
/**
* Class for a matrix field.
* @param {number} matrix The default matrix value represented by a 25-bit integer.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldMatrix = function(matrix) {
Blockly.FieldMatrix.superClass_.constructor.call(this, matrix);
this.addArgType('matrix');
/**
* Array of SVGElement<rect> for matrix thumbnail image on block field.
* @type {!Array<SVGElement>}
* @private
*/
this.ledThumbNodes_ = [];
/**
* Array of SVGElement<rect> for matrix editor in dropdown menu.
* @type {!Array<SVGElement>}
* @private
*/
this.ledButtons_ = [];
/**
* String for storing current matrix value.
* @type {!String]
* @private
*/
this.matrix_ = '';
/**
* SVGElement for LED matrix in editor.
* @type {?SVGElement}
* @private
*/
this.matrixStage_ = null;
/**
* SVG image for dropdown arrow.
* @type {?SVGElement}
* @private
*/
this.arrow_ = null;
/**
* String indicating matrix paint style.
* value can be [null, 'fill', 'clear'].
* @type {?String}
* @private
*/
this.paintStyle_ = null;
/**
* Touch event wrapper.
* Runs when the field is selected.
* @type {!Array}
* @private
*/
this.mouseDownWrapper_ = null;
/**
* Touch event wrapper.
* Runs when the clear button editor button is selected.
* @type {!Array}
* @private
*/
this.clearButtonWrapper_ = null;
/**
* Touch event wrapper.
* Runs when the fill button editor button is selected.
* @type {!Array}
* @private
*/
this.fillButtonWrapper_ = null;
/**
* Touch event wrapper.
* Runs when the matrix editor is touched.
* @type {!Array}
* @private
*/
this.matrixTouchWrapper_ = null;
/**
* Touch event wrapper.
* Runs when the matrix editor touch event moves.
* @type {!Array}
* @private
*/
this.matrixMoveWrapper_ = null;
/**
* Touch event wrapper.
* Runs when the matrix editor is released.
* @type {!Array}
* @private
*/
this.matrixReleaseWrapper_ = null;
};
goog.inherits(Blockly.FieldMatrix, Blockly.Field);
/**
* Construct a FieldMatrix from a JSON arg object.
* @param {!Object} options A JSON object with options (matrix).
* @returns {!Blockly.FieldMatrix} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldMatrix.fromJson = function(options) {
return new Blockly.FieldMatrix(options['matrix']);
};
/**
* Fixed size of the matrix thumbnail in the input field, in px.
* @type {number}
* @const
*/
Blockly.FieldMatrix.THUMBNAIL_SIZE = 26;
/**
* Fixed size of each matrix thumbnail node, in px.
* @type {number}
* @const
*/
Blockly.FieldMatrix.THUMBNAIL_NODE_SIZE = 4;
/**
* Fixed size of each matrix thumbnail node, in px.
* @type {number}
* @const
*/
Blockly.FieldMatrix.THUMBNAIL_NODE_PAD = 1;
/**
* Fixed size of arrow icon in drop down menu, in px.
* @type {number}
* @const
*/
Blockly.FieldMatrix.ARROW_SIZE = 12;
/**
* Fixed size of each button inside the 5x5 matrix, in px.
* @type {number}
* @const
*/
Blockly.FieldMatrix.MATRIX_NODE_SIZE = 18;
/**
* Fixed corner radius for 5x5 matrix buttons, in px.
* @type {number}
* @const
*/
Blockly.FieldMatrix.MATRIX_NODE_RADIUS = 4;
/**
* Fixed padding for 5x5 matrix buttons, in px.
* @type {number}
* @const
*/
Blockly.FieldMatrix.MATRIX_NODE_PAD = 5;
/**
* String with 25 '0' chars.
* Used for clearing a matrix or filling an LED node array.
* @type {string}
* @const
*/
Blockly.FieldMatrix.ZEROS = '0000000000000000000000000';
/**
* String with 25 '1' chars.
* Used for filling a matrix.
* @type {string}
* @const
*/
Blockly.FieldMatrix.ONES = '1111111111111111111111111';
/**
* Called when the field is placed on a block.
* @param {Block} block The owning block.
*/
Blockly.FieldMatrix.prototype.init = function() {
if (this.fieldGroup_) {
// Matrix menu has already been initialized once.
return;
}
// Build the DOM.
this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null);
this.size_.width = Blockly.FieldMatrix.THUMBNAIL_SIZE +
Blockly.FieldMatrix.ARROW_SIZE + (Blockly.BlockSvg.DROPDOWN_ARROW_PADDING * 1.5);
this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
var thumbX = Blockly.BlockSvg.DROPDOWN_ARROW_PADDING / 2;
var thumbY = (this.size_.height - Blockly.FieldMatrix.THUMBNAIL_SIZE) / 2;
var thumbnail = Blockly.utils.createSvgElement('g', {
'transform': 'translate(' + thumbX + ', ' + thumbY + ')',
'pointer-events': 'bounding-box', 'cursor': 'pointer'
}, this.fieldGroup_);
this.ledThumbNodes_ = [];
var nodeSize = Blockly.FieldMatrix.THUMBNAIL_NODE_SIZE;
var nodePad = Blockly.FieldMatrix.THUMBNAIL_NODE_PAD;
for (var i = 0; i < 5; i++) {
for (var n = 0; n < 5; n++) {
var attr = {
'x': ((nodeSize + nodePad) * n) + nodePad,
'y': ((nodeSize + nodePad) * i) + nodePad,
'width': nodeSize, 'height': nodeSize,
'rx': nodePad, 'ry': nodePad
};
this.ledThumbNodes_.push(
Blockly.utils.createSvgElement('rect', attr, thumbnail)
);
}
thumbnail.style.cursor = 'default';
this.updateMatrix_();
}
if (!this.arrow_) {
var arrowX = Blockly.FieldMatrix.THUMBNAIL_SIZE +
Blockly.BlockSvg.DROPDOWN_ARROW_PADDING * 1.5;
var arrowY = (this.size_.height - Blockly.FieldMatrix.ARROW_SIZE) / 2;
this.arrow_ = Blockly.utils.createSvgElement('image', {
'height': Blockly.FieldMatrix.ARROW_SIZE + 'px',
'width': Blockly.FieldMatrix.ARROW_SIZE + 'px',
'transform': 'translate(' + arrowX + ', ' + arrowY + ')'
}, this.fieldGroup_);
this.arrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia +
'dropdown-arrow.svg');
this.arrow_.style.cursor = 'default';
}
this.mouseDownWrapper_ = Blockly.bindEventWithChecks_(
this.getClickTarget_(), 'mousedown', this, this.onMouseDown_);
};
/**
* Set the value for this matrix menu.
* @param {string} matrix The new matrix value represented by a 25-bit integer.
* @override
*/
Blockly.FieldMatrix.prototype.setValue = function(matrix) {
if (!matrix || matrix === this.matrix_) {
return; // No change
}
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.Change(
this.sourceBlock_, 'field', this.name, this.matrix_, matrix));
}
matrix = matrix + Blockly.FieldMatrix.ZEROS.substr(0, 25 - matrix.length);
this.matrix_ = matrix;
this.updateMatrix_();
};
/**
* Get the value from this matrix menu.
* @return {string} Current matrix value.
*/
Blockly.FieldMatrix.prototype.getValue = function() {
return String(this.matrix_);
};
/**
* Show the drop-down menu for editing this field.
* @private
*/
Blockly.FieldMatrix.prototype.showEditor_ = function() {
// If there is an existing drop-down someone else owns, hide it immediately and clear it.
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.DropDownDiv.clearContent();
var div = Blockly.DropDownDiv.getContentDiv();
// Build the SVG DOM.
var matrixSize = (Blockly.FieldMatrix.MATRIX_NODE_SIZE * 5) +
(Blockly.FieldMatrix.MATRIX_NODE_PAD * 6);
this.matrixStage_ = Blockly.utils.createSvgElement('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:html': 'http://www.w3.org/1999/xhtml',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'version': '1.1',
'height': matrixSize + 'px',
'width': matrixSize + 'px'
}, div);
// Create the 5x5 matrix
this.ledButtons_ = [];
for (var i = 0; i < 5; i++) {
for (var n = 0; n < 5; n++) {
var x = (Blockly.FieldMatrix.MATRIX_NODE_SIZE * n) +
(Blockly.FieldMatrix.MATRIX_NODE_PAD * (n + 1));
var y = (Blockly.FieldMatrix.MATRIX_NODE_SIZE * i) +
(Blockly.FieldMatrix.MATRIX_NODE_PAD * (i + 1));
var attr = {
'x': x + 'px', 'y': y + 'px',
'width': Blockly.FieldMatrix.MATRIX_NODE_SIZE,
'height': Blockly.FieldMatrix.MATRIX_NODE_SIZE,
'rx': Blockly.FieldMatrix.MATRIX_NODE_RADIUS,
'ry': Blockly.FieldMatrix.MATRIX_NODE_RADIUS
};
var led = Blockly.utils.createSvgElement('rect', attr, this.matrixStage_);
this.matrixStage_.appendChild(led);
this.ledButtons_.push(led);
}
}
// Div for lower button menu
var buttonDiv = document.createElement('div');
// Button to clear matrix
var clearButtonDiv = document.createElement('div');
clearButtonDiv.className = 'scratchMatrixButtonDiv';
var clearButton = this.createButton_(this.sourceBlock_.colourSecondary_);
clearButtonDiv.appendChild(clearButton);
// Button to fill matrix
var fillButtonDiv = document.createElement('div');
fillButtonDiv.className = 'scratchMatrixButtonDiv';
var fillButton = this.createButton_('#FFFFFF');
fillButtonDiv.appendChild(fillButton);
buttonDiv.appendChild(clearButtonDiv);
buttonDiv.appendChild(fillButtonDiv);
div.appendChild(buttonDiv);
Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(),
this.sourceBlock_.getColourTertiary());
Blockly.DropDownDiv.setCategory(this.sourceBlock_.getCategory());
Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_);
this.matrixTouchWrapper_ =
Blockly.bindEvent_(this.matrixStage_, 'mousedown', this, this.onMouseDown);
this.clearButtonWrapper_ =
Blockly.bindEvent_(clearButton, 'click', this, this.clearMatrix_);
this.fillButtonWrapper_ =
Blockly.bindEvent_(fillButton, 'click', this, this.fillMatrix_);
// Update the matrix for the current value
this.updateMatrix_();
};
this.nodeCallback_ = function(e, num) {
console.log(num);
};
/**
* Make an svg object that resembles a 3x3 matrix to be used as a button.
* @param {string} fill The color to fill the matrix nodes.
* @return {SvgElement} The button svg element.
*/
Blockly.FieldMatrix.prototype.createButton_ = function(fill) {
var button = Blockly.utils.createSvgElement('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:html': 'http://www.w3.org/1999/xhtml',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'version': '1.1',
'height': Blockly.FieldMatrix.MATRIX_NODE_SIZE + 'px',
'width': Blockly.FieldMatrix.MATRIX_NODE_SIZE + 'px'
});
var nodeSize = Blockly.FieldMatrix.MATRIX_NODE_SIZE / 4;
var nodePad = Blockly.FieldMatrix.MATRIX_NODE_SIZE / 16;
for (var i = 0; i < 3; i++) {
for (var n = 0; n < 3; n++) {
Blockly.utils.createSvgElement('rect', {
'x': ((nodeSize + nodePad) * n) + nodePad,
'y': ((nodeSize + nodePad) * i) + nodePad,
'width': nodeSize, 'height': nodeSize,
'rx': nodePad, 'ry': nodePad,
'fill': fill
}, button);
}
}
return button;
};
/**
* Redraw the matrix with the current value.
* @private
*/
Blockly.FieldMatrix.prototype.updateMatrix_ = function() {
for (var i = 0; i < this.matrix_.length; i++) {
if (this.matrix_[i] === '0') {
this.fillMatrixNode_(this.ledButtons_, i, this.sourceBlock_.colourSecondary_);
this.fillMatrixNode_(this.ledThumbNodes_, i, this.sourceBlock_.colour_);
} else {
this.fillMatrixNode_(this.ledButtons_, i, '#FFFFFF');
this.fillMatrixNode_(this.ledThumbNodes_, i, '#FFFFFF');
}
}
};
/**
* Clear the matrix.
* @param {!Event} e Mouse event.
*/
Blockly.FieldMatrix.prototype.clearMatrix_ = function(e) {
if (e.button != 0) return;
this.setValue(Blockly.FieldMatrix.ZEROS);
};
/**
* Fill the matrix.
* @param {!Event} e Mouse event.
*/
Blockly.FieldMatrix.prototype.fillMatrix_ = function(e) {
if (e.button != 0) return;
this.setValue(Blockly.FieldMatrix.ONES);
};
/**
* Fill matrix node with specified colour.
* @param {!Array<SVGElement>} node The array of matrix nodes.
* @param {!number} index The index of the matrix node.
* @param {!string} fill The fill colour in '#rrggbb' format.
*/
Blockly.FieldMatrix.prototype.fillMatrixNode_ = function(node, index, fill) {
if (!node || !node[index] || !fill) return;
node[index].setAttribute('fill', fill);
};
Blockly.FieldMatrix.prototype.setLEDNode_ = function(led, state) {
if (led < 0 || led > 24) return;
var matrix = this.matrix_.substr(0, led) + state + this.matrix_.substr(led + 1);
this.setValue(matrix);
};
Blockly.FieldMatrix.prototype.fillLEDNode_ = function(led) {
if (led < 0 || led > 24) return;
this.setLEDNode_(led, '1');
};
Blockly.FieldMatrix.prototype.clearLEDNode_ = function(led) {
if (led < 0 || led > 24) return;
this.setLEDNode_(led, '0');
};
Blockly.FieldMatrix.prototype.toggleLEDNode_ = function(led) {
if (led < 0 || led > 24) return;
if (this.matrix_.charAt(led) === '0') {
this.setLEDNode_(led, '1');
} else {
this.setLEDNode_(led, '0');
}
};
/**
* Toggle matrix nodes on and off.
* @param {!Event} e Mouse event.
*/
Blockly.FieldMatrix.prototype.onMouseDown = function(e) {
this.matrixMoveWrapper_ =
Blockly.bindEvent_(document.body, 'mousemove', this, this.onMouseMove);
this.matrixReleaseWrapper_ =
Blockly.bindEvent_(document.body, 'mouseup', this, this.onMouseUp);
var ledHit = this.checkForLED_(e);
if (ledHit > -1) {
if (this.matrix_.charAt(ledHit) === '0') {
this.paintStyle_ = 'fill';
} else {
this.paintStyle_ = 'clear';
}
this.toggleLEDNode_(ledHit);
this.updateMatrix_();
} else {
this.paintStyle_ = null;
}
};
/**
* Unbind mouse move event and clear the paint style.
* @param {!Event} e Mouse move event.
*/
Blockly.FieldMatrix.prototype.onMouseUp = function() {
Blockly.unbindEvent_(this.matrixMoveWrapper_);
Blockly.unbindEvent_(this.matrixReleaseWrapper_);
this.paintStyle_ = null;
};
/**
* Toggle matrix nodes on and off by dragging mouse.
* @param {!Event} e Mouse move event.
*/
Blockly.FieldMatrix.prototype.onMouseMove = function(e) {
e.preventDefault();
if (this.paintStyle_) {
var led = this.checkForLED_(e);
if (led < 0) return;
if (this.paintStyle_ === 'clear') {
this.clearLEDNode_(led);
} else if (this.paintStyle_ === 'fill') {
this.fillLEDNode_(led);
}
}
};
/**
* Check if mouse coordinates collide with a matrix node.
* @param {!Event} e Mouse move event.
* @return {number} The matching matrix node or -1 for none.
*/
Blockly.FieldMatrix.prototype.checkForLED_ = function(e) {
var bBox = this.matrixStage_.getBoundingClientRect();
var nodeSize = Blockly.FieldMatrix.MATRIX_NODE_SIZE;
var nodePad = Blockly.FieldMatrix.MATRIX_NODE_PAD;
var dx = e.clientX - bBox.left;
var dy = e.clientY - bBox.top;
var min = nodePad / 2;
var max = bBox.width - (nodePad / 2);
if (dx < min || dx > max || dy < min || dy > max) {
return -1;
}
var xDiv = Math.trunc((dx - nodePad / 2) / (nodeSize + nodePad));
var yDiv = Math.trunc((dy - nodePad / 2) / (nodeSize + nodePad));
return xDiv + (yDiv * nodePad);
};
/**
* Clean up this FieldMatrix, as well as the inherited Field.
* @return {!Function} Closure to call on destruction of the WidgetDiv.
* @private
*/
Blockly.FieldMatrix.prototype.dispose_ = function() {
var thisField = this;
return function() {
Blockly.FieldMatrix.superClass_.dispose_.call(thisField)();
thisField.matrixStage_ = null;
if (thisField.mouseDownWrapper_) {
Blockly.unbindEvent_(thisField.mouseDownWrapper_);
}
if (thisField.matrixTouchWrapper_) {
Blockly.unbindEvent_(thisField.matrixTouchWrapper_);
}
if (thisField.matrixReleaseWrapper_) {
Blockly.unbindEvent_(thisField.matrixReleaseWrapper_);
}
if (thisField.matrixMoveWrapper_) {
Blockly.unbindEvent_(thisField.matrixMoveWrapper_);
}
if (thisField.clearButtonWrapper_) {
Blockly.unbindEvent_(thisField.clearButtonWrapper_);
}
if (thisField.fillButtonWrapper_) {
Blockly.unbindEvent_(thisField.fillButtonWrapper_);
}
};
};
Blockly.Field.register('field_matrix', Blockly.FieldMatrix);

View File

@@ -0,0 +1,850 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 Massachusetts Institute of Technology
* All rights reserved.
*
* 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 Note input field, for selecting a musical note on a piano.
* @author ericr@media.mit.edu (Eric Rosenbaum)
*/
'use strict';
goog.provide('Blockly.FieldNote');
goog.require('Blockly.DropDownDiv');
goog.require('Blockly.FieldTextInput');
goog.require('goog.math');
goog.require('goog.userAgent');
/**
* Class for a note input field, for selecting a musical note on a piano.
* @param {(string|number)=} opt_value The initial content of the field. The
* value should cast to a number, and if it does not, '0' will be used.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns the accepted text or null to abort
* the change.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
Blockly.FieldNote = function(opt_value, opt_validator) {
opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0';
Blockly.FieldNote.superClass_.constructor.call(
this, opt_value, opt_validator);
this.addArgType('note');
/**
* Width of the field. Computed when drawing it, and used for animation.
* @type {number}
* @private
*/
this.fieldEditorWidth_ = 0;
/**
* Height of the field. Computed when drawing it.
* @type {number}
* @private
*/
this.fieldEditorHeight_ = 0;
/**
* The piano SVG.
* @type {SVGElement}
* @private
*/
this.pianoSVG_ = null;
/**
* Array of SVG elements representing the clickable piano keys.
* @type {!Array<SVGElement>}
* @private
*/
this.keySVGs_ = [];
/**
* Note name indicator at the top of the field.
* @type {SVGElement}
* @private
*/
this.noteNameText_ = null;
/**
* Note name indicator on the low C key.
* @type {SVGElement}
* @private
*/
this.lowCText_ = null;
/**
* Note name indicator on the low C key.
* @type {SVGElement}
* @private
*/
this.highCText_ = null;
/**
* Octave number of the currently displayed range of keys.
* @type {number}
* @private
*/
this.displayedOctave_ = null;
/**
* Current animation position of the piano SVG, as it shifts left or right to
* change octaves.
* @type {number}
* @private
*/
this.animationPos_ = 0;
/**
* Target position for the animation as the piano SVG shifts left or right.
* @type {number}
* @private
*/
this.animationTarget_ = 0;
/**
* A flag indicating that the mouse is currently down. Used in combination with
* mouse enter events to update the key selection while dragging.
* @type {boolean}
* @private
*/
this.mouseIsDown_ = false;
/**
* An array of wrappers for mouse down events on piano keys.
* @type {!Array.<!Array>}
* @private
*/
this.mouseDownWrappers_ = [];
/**
* A wrapper for the mouse up event.
* @type {!Array.<!Array>}
* @private
*/
this.mouseUpWrapper_ = null;
/**
* An array of wrappers for mouse enter events on piano keys.
* @type {!Array.<!Array>}
* @private
*/
this.mouseEnterWrappers_ = [];
/**
* A wrapper for the mouse down event on the octave down button.
* @type {!Array.<!Array>}
* @private
*/
this.octaveDownMouseDownWrapper_ = null;
/**
* A wrapper for the mouse down event on the octave up button.
* @type {!Array.<!Array>}
* @private
*/
this.octaveUpMouseDownWrapper_ = null;
};
goog.inherits(Blockly.FieldNote, Blockly.FieldTextInput);
/**
* Inset in pixels of content displayed in the field, caused by parent properties.
* The inset is actually determined by the CSS property blocklyDropDownDiv- it is
* the sum of the padding and border thickness.
*/
Blockly.FieldNote.INSET = 5;
/**
* Height of the top area of the field, in px.
* @type {number}
* @const
*/
Blockly.FieldNote.TOP_MENU_HEIGHT = 32 - Blockly.FieldNote.INSET;
/**
* Padding on the top and sides of the field, in px.
* @type {number}
* @const
*/
Blockly.FieldNote.EDGE_PADDING = 1;
/**
* Height of the drop shadow on the piano, in px.
* @type {number}
* @const
*/
Blockly.FieldNote.SHADOW_HEIGHT = 4;
/**
* Color for the shadow on the piano.
* @type {string}
* @const
*/
Blockly.FieldNote.SHADOW_COLOR = '#000';
/**
* Opacity for the shadow on the piano.
* @type {string}
* @const
*/
Blockly.FieldNote.SHADOW_OPACITY = .2;
/**
* A color for the white piano keys.
* @type {string}
* @const
*/
Blockly.FieldNote.WHITE_KEY_COLOR = '#FFFFFF';
/**
* A color for the black piano keys.
* @type {string}
* @const
*/
Blockly.FieldNote.BLACK_KEY_COLOR = '#323133';
/**
* A color for stroke around black piano keys.
* @type {string}
* @const
*/
Blockly.FieldNote.BLACK_KEY_STROKE = '#555555';
/**
* A color for the selected state of a piano key.
* @type {string}
* @const
*/
Blockly.FieldNote.KEY_SELECTED_COLOR = '#b0d6ff';
/**
* The number of white keys in one octave on the piano.
* @type {number}
* @const
*/
Blockly.FieldNote.NUM_WHITE_KEYS = 8;
/**
* Height of a white piano key, in px.
* @type {string}
* @const
*/
Blockly.FieldNote.WHITE_KEY_HEIGHT = 72;
/**
* Width of a white piano key, in px.
* @type {string}
* @const
*/
Blockly.FieldNote.WHITE_KEY_WIDTH = 40;
/**
* Height of a black piano key, in px.
* @type {string}
* @const
*/
Blockly.FieldNote.BLACK_KEY_HEIGHT = 40;
/**
* Width of a black piano key, in px.
* @type {string}
* @const
*/
Blockly.FieldNote.BLACK_KEY_WIDTH = 32;
/**
* Radius of the curved bottom corner of a piano key, in px.
* @type {string}
* @const
*/
Blockly.FieldNote.KEY_RADIUS = 6;
/**
* Bottom padding for the labels on C keys.
* @type {string}
* @const
*/
Blockly.FieldNote.KEY_LABEL_PADDING = 8;
/**
* An array of objects with data describing the keys on the piano.
* @type {Array.<{name: String, pitch: Number, isBlack: boolean}>}
* @const
*/
Blockly.FieldNote.KEY_INFO = [
{name: 'C', pitch: 0},
{name: 'C♯', pitch: 1, isBlack: true},
{name: 'D', pitch: 2},
{name: 'E♭', pitch: 3, isBlack: true},
{name: 'E', pitch: 4},
{name: 'F', pitch: 5},
{name: 'F♯', pitch: 6, isBlack: true},
{name: 'G', pitch: 7},
{name: 'G♯', pitch: 8, isBlack: true},
{name: 'A', pitch: 9},
{name: 'B♭', pitch: 10, isBlack: true},
{name: 'B', pitch: 11},
{name: 'C', pitch: 12}
];
/**
* The MIDI note number of the highest note selectable on the piano.
* @type {number}
* @const
*/
Blockly.FieldNote.MAX_NOTE = 130;
/**
* The fraction of the distance to the target location to move the piano at each
* step of the animation.
* @type {number}
* @const
*/
Blockly.FieldNote.ANIMATION_FRACTION = 0.2;
/**
* Path to the arrow svg icon, used on the octave buttons.
* @type {string}
* @const
*/
Blockly.FieldNote.ARROW_SVG_PATH = 'icons/arrow_button.svg';
/**
* The size of the square octave buttons.
* @type {number}
* @const
*/
Blockly.FieldNote.OCTAVE_BUTTON_SIZE = 32;
/**
* Construct a FieldNote from a JSON arg object.
* @param {!Object} options A JSON object with options.
* @returns {!Blockly.FieldNote} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldNote.fromJson = function(options) {
return new Blockly.FieldNote(options['note']);
};
/**
* Clean up this FieldNote, as well as the inherited FieldTextInput.
* @return {!Function} Closure to call on destruction of the WidgetDiv.
* @private
*/
Blockly.FieldNote.prototype.dispose_ = function() {
var thisField = this;
return function() {
Blockly.FieldNote.superClass_.dispose_.call(thisField)();
thisField.mouseDownWrappers_.forEach(function(wrapper) {
Blockly.unbindEvent_(wrapper);
});
thisField.mouseEnterWrappers_.forEach(function(wrapper) {
Blockly.unbindEvent_(wrapper);
});
if (thisField.mouseUpWrapper_) {
Blockly.unbindEvent_(thisField.mouseUpWrapper_);
}
if (thisField.octaveDownMouseDownWrapper_) {
Blockly.unbindEvent_(thisField.octaveDownMouseDownWrapper_);
}
if (thisField.octaveUpMouseDownWrapper_) {
Blockly.unbindEvent_(thisField.octaveUpMouseDownWrapper_);
}
this.pianoSVG_ = null;
this.keySVGs_.length = 0;
this.noteNameText_ = null;
this.lowCText_ = null;
this.highCText_ = null;
};
};
/**
* Show a field with piano keys.
* @private
*/
Blockly.FieldNote.prototype.showEditor_ = function() {
// Mobile browsers have issues with in-line textareas (focus & keyboards).
Blockly.FieldNote.superClass_.showEditor_.call(this, this.useTouchInteraction_);
// If there is an existing drop-down someone else owns, hide it immediately and clear it.
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.DropDownDiv.clearContent();
// Build the SVG DOM.
var div = Blockly.DropDownDiv.getContentDiv();
this.fieldEditorWidth_ = Blockly.FieldNote.NUM_WHITE_KEYS * Blockly.FieldNote.WHITE_KEY_WIDTH +
Blockly.FieldNote.EDGE_PADDING;
this.fieldEditorHeight_ = Blockly.FieldNote.TOP_MENU_HEIGHT +
Blockly.FieldNote.WHITE_KEY_HEIGHT +
Blockly.FieldNote.EDGE_PADDING;
var svg = Blockly.utils.createSvgElement('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:html': 'http://www.w3.org/1999/xhtml',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'version': '1.1',
'height': this.fieldEditorHeight_ + 'px',
'width': this.fieldEditorWidth_ + 'px'
}, div);
// Add the white and black keys
// Since we are adding the keys from left to right in order, they need
// to be in two groups in order to layer correctly.
this.pianoSVG_ = Blockly.utils.createSvgElement('g', {}, svg);
var whiteKeyGroup = Blockly.utils.createSvgElement('g', {}, this.pianoSVG_);
var blackKeyGroup = Blockly.utils.createSvgElement('g', {}, this.pianoSVG_);
// Add three piano octaves, so we can animate moving up or down an octave.
// Only the middle octave gets bound to events.
this.keySVGs_ = [];
this.addPianoOctave_(-this.fieldEditorWidth_ + Blockly.FieldNote.EDGE_PADDING,
whiteKeyGroup, blackKeyGroup, null);
this.addPianoOctave_(0, whiteKeyGroup, blackKeyGroup, this.keySVGs_);
this.addPianoOctave_(this.fieldEditorWidth_ - Blockly.FieldNote.EDGE_PADDING,
whiteKeyGroup, blackKeyGroup, null);
// Note name indicator at the top of the field
this.noteNameText_ = Blockly.utils.createSvgElement('text',
{
'x': this.fieldEditorWidth_ / 2,
'y': Blockly.FieldNote.TOP_MENU_HEIGHT / 2,
'class': 'blocklyText',
'text-anchor': 'middle',
'dominant-baseline': 'middle',
}, svg);
// Note names on the low and high C keys
var lowCX = Blockly.FieldNote.WHITE_KEY_WIDTH / 2;
this.lowCText_ = this.addCKeyLabel_(lowCX, svg);
var highCX = lowCX + (Blockly.FieldNote.WHITE_KEY_WIDTH *
(Blockly.FieldNote.NUM_WHITE_KEYS - 1));
this.highCText_ = this.addCKeyLabel_(highCX, svg);
// Horizontal line at the top of the keys
Blockly.utils.createSvgElement('line',
{
'stroke': this.sourceBlock_.getColourTertiary(),
'x1': 0,
'y1': Blockly.FieldNote.TOP_MENU_HEIGHT,
'x2': this.fieldEditorWidth_,
'y2': Blockly.FieldNote.TOP_MENU_HEIGHT
}, svg);
// Drop shadow at the top of the keys
Blockly.utils.createSvgElement('rect',
{
'x': 0,
'y': Blockly.FieldNote.TOP_MENU_HEIGHT,
'width': this.fieldEditorWidth_,
'height': Blockly.FieldNote.SHADOW_HEIGHT,
'fill': Blockly.FieldNote.SHADOW_COLOR,
'fill-opacity': Blockly.FieldNote.SHADOW_OPACITY
}, svg);
// Octave buttons
this.octaveDownButton = this.addOctaveButton_(0, true, svg);
this.octaveUpButton = this.addOctaveButton_(
(this.fieldEditorWidth_ + Blockly.FieldNote.INSET * 2) -
Blockly.FieldNote.OCTAVE_BUTTON_SIZE, false, svg);
this.octaveDownMouseDownWrapper_ =
Blockly.bindEvent_(this.octaveDownButton, 'mousedown', this, function() {
this.changeOctaveBy_(-1);
});
this.octaveUpMouseDownWrapper_ =
Blockly.bindEvent_(this.octaveUpButton, 'mousedown', this,function() {
this.changeOctaveBy_(1);
});
Blockly.DropDownDiv.setColour(this.sourceBlock_.parentBlock_.getColour(),
this.sourceBlock_.getColourTertiary());
Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory());
Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_);
this.updateSelection_();
};
/**
* Add one octave of piano keys drawn using SVG.
* @param {number} x The x position of the left edge of this octave of keys.
* @param {SVGElement} whiteKeyGroup The group for all white piano keys.
* @param {SvgElement} blackKeyGroup The group for all black piano keys.
* @param {!Array.<SvgElement>} keySVGarray An array containing all the key SVGs.
* @private
*/
Blockly.FieldNote.prototype.addPianoOctave_ = function(x, whiteKeyGroup, blackKeyGroup, keySVGarray) {
var xIncrement, width, height, fill, stroke, group;
x += Blockly.FieldNote.EDGE_PADDING / 2;
var y = Blockly.FieldNote.TOP_MENU_HEIGHT;
for (var i = 0; i < Blockly.FieldNote.KEY_INFO.length; i++) {
// Draw a black or white key
if (Blockly.FieldNote.KEY_INFO[i].isBlack) {
// Black keys are shifted back half a key
x -= Blockly.FieldNote.BLACK_KEY_WIDTH / 2;
xIncrement = Blockly.FieldNote.BLACK_KEY_WIDTH / 2;
width = Blockly.FieldNote.BLACK_KEY_WIDTH;
height = Blockly.FieldNote.BLACK_KEY_HEIGHT;
fill = Blockly.FieldNote.BLACK_KEY_COLOR;
stroke = Blockly.FieldNote.BLACK_KEY_STROKE;
group = blackKeyGroup;
} else {
xIncrement = Blockly.FieldNote.WHITE_KEY_WIDTH;
width = Blockly.FieldNote.WHITE_KEY_WIDTH;
height = Blockly.FieldNote.WHITE_KEY_HEIGHT;
fill = Blockly.FieldNote.WHITE_KEY_COLOR;
stroke = this.sourceBlock_.getColourTertiary();
group = whiteKeyGroup;
}
var attr = {
'd': this.getPianoKeyPath_(x, y, width, height),
'fill': fill,
'stroke': stroke
};
x += xIncrement;
var keySVG = Blockly.utils.createSvgElement('path', attr, group);
if (keySVGarray) {
keySVGarray[i] = keySVG;
keySVG.setAttribute('data-pitch', Blockly.FieldNote.KEY_INFO[i].pitch);
keySVG.setAttribute('data-name', Blockly.FieldNote.KEY_INFO[i].name);
keySVG.setAttribute('data-isBlack', Blockly.FieldNote.KEY_INFO[i].isBlack);
this.mouseDownWrappers_[i] =
Blockly.bindEvent_(keySVG, 'mousedown', this, this.onMouseDownOnKey_);
this.mouseEnterWrappers_[i] =
Blockly.bindEvent_(keySVG, 'mouseenter', this, this.onMouseEnter_);
}
}
};
/**
* Construct the SVG path string for a piano key shape: a rectangle with rounded
* corners at the bottom.
* @param {number} x the x position for the key.
* @param {number} y the y position for the key.
* @param {number} width the width of the key.
* @param {number} height the height of the key.
* @returns {string} the SVG path as a string.
* @private
*/
Blockly.FieldNote.prototype.getPianoKeyPath_ = function(x, y, width, height) {
return 'M' + x + ' ' + y + ' ' +
'L' + x + ' ' + (y + height - Blockly.FieldNote.KEY_RADIUS) + ' ' +
'Q' + x + ' ' + (y + height) + ' ' +
(x + Blockly.FieldNote.KEY_RADIUS) + ' ' + (y + height) + ' ' +
'L' + (x + width - Blockly.FieldNote.KEY_RADIUS) + ' ' + (y + height) + ' ' +
'Q' + (x + width) + ' ' + (y + height) + ' ' +
(x + width) + ' ' + (y + height - Blockly.FieldNote.KEY_RADIUS) + ' ' +
'L' + (x + width) + ' ' + y + ' ' +
'L' + x + ' ' + y;
};
/**
* Add a button for switching the displayed octave of the piano up or down.
* @param {number} x The x position of the button.
* @param {boolean} flipped If true, the icon should be flipped.
* @param {SvgElement} svg The svg element to add the buttons to.
* @returns {SvgElement} A group containing the button SVG elements.
* @private
*/
Blockly.FieldNote.prototype.addOctaveButton_ = function(x, flipped, svg) {
var group = Blockly.utils.createSvgElement('g', {}, svg);
var imageSize = Blockly.FieldNote.OCTAVE_BUTTON_SIZE;
var arrow = Blockly.utils.createSvgElement('image',
{
'width': imageSize,
'height': imageSize,
'x': x - Blockly.FieldNote.INSET,
'y': -1 * Blockly.FieldNote.INSET
}, group);
arrow.setAttributeNS(
'http://www.w3.org/1999/xlink',
'xlink:href',
Blockly.mainWorkspace.options.pathToMedia + Blockly.FieldNote.ARROW_SVG_PATH
);
Blockly.utils.createSvgElement('line',
{
'stroke': this.sourceBlock_.getColourTertiary(),
'x1': x - Blockly.FieldNote.INSET,
'y1': 0,
'x2': x - Blockly.FieldNote.INSET,
'y2': Blockly.FieldNote.TOP_MENU_HEIGHT - Blockly.FieldNote.INSET
}, group);
if (flipped) {
var translateX = -1 * Blockly.FieldNote.OCTAVE_BUTTON_SIZE + (Blockly.FieldNote.INSET * 2);
group.setAttribute('transform', 'scale(-1, 1) ' +
'translate(' + translateX + ', 0)');
}
return group;
};
/**
* Add an SVG text label for display on the C keys of the piano.
* @param {number} x The x position for the label.
* @param {SvgElement} svg The SVG element to add the label to.
* @returns {SvgElement} The SVG element containing the label.
* @private
*/
Blockly.FieldNote.prototype.addCKeyLabel_ = function(x, svg) {
return Blockly.utils.createSvgElement('text',
{
'x': x,
'y': Blockly.FieldNote.TOP_MENU_HEIGHT + Blockly.FieldNote.WHITE_KEY_HEIGHT -
Blockly.FieldNote.KEY_LABEL_PADDING,
'class': 'scratchNotePickerKeyLabel',
'text-anchor': 'middle'
}, svg);
};
/**
* Set the visibility of the C key labels.
* @param {boolean} visible If true, set labels to be visible.
* @private
*/
Blockly.FieldNote.prototype.setCKeyLabelsVisible_ = function(visible) {
if (visible) {
this.fadeSvgToOpacity_(this.lowCText_, 1);
this.fadeSvgToOpacity_(this.highCText_, 1);
} else {
this.fadeSvgToOpacity_(this.lowCText_, 0);
this.fadeSvgToOpacity_(this.highCText_, 0);
}
};
/**
* Animate an SVG to fade it in or out to a target opacity.
* @param {SvgElement} svg The SVG element to apply the fade to.
* @param {number} opacity The target opacity.
* @private
*/
Blockly.FieldNote.prototype.fadeSvgToOpacity_ = function(svg, opacity) {
svg.setAttribute('style', 'opacity: ' + opacity + '; transition: opacity 0.1s;');
};
/**
* Handle the mouse down event on a piano key.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.FieldNote.prototype.onMouseDownOnKey_ = function(e) {
this.mouseIsDown_ = true;
this.mouseUpWrapper_ = Blockly.bindEvent_(document.body, 'mouseup', this, this.onMouseUp_);
this.selectNoteWithMouseEvent_(e);
};
/**
* Handle the mouse up event following a mouse down on a piano key.
* @private
*/
Blockly.FieldNote.prototype.onMouseUp_ = function() {
this.mouseIsDown_ = false;
Blockly.unbindEvent_(this.mouseUpWrapper_);
};
/**
* Handle the event when the mouse enters a piano key.
* @param {!Event} e Mouse enter event.
* @private
*/
Blockly.FieldNote.prototype.onMouseEnter_ = function(e) {
if (this.mouseIsDown_) {
this.selectNoteWithMouseEvent_(e);
}
};
/**
* Use the data in a mouse event to select a new note, and play it.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.FieldNote.prototype.selectNoteWithMouseEvent_ = function(e) {
var newNoteNum = Number(e.target.getAttribute('data-pitch')) + this.displayedOctave_ * 12;
this.setNoteNum_(newNoteNum);
this.playNoteInternal_();
};
/**
* Play a note, by calling the externally overriden play note function.
* @private
*/
Blockly.FieldNote.prototype.playNoteInternal_ = function() {
if (Blockly.FieldNote.playNote_) {
Blockly.FieldNote.playNote_(
this.getValue(),
this.sourceBlock_.parentBlock_.getCategory()
);
}
};
/**
* Function to play a musical note corresponding to the key selected.
* Overridden externally.
* @param {number} noteNum the MIDI note number to play.
* @param {string} id An id to select a scratch extension to play the note.
* @private
*/
Blockly.FieldNote.playNote_ = function(/* noteNum, id*/) {
return;
};
/**
* Change the selected note by a number of octaves, and start the animation.
* @param {number} octaves The number of octaves to change by.
* @private
*/
Blockly.FieldNote.prototype.changeOctaveBy_ = function(octaves) {
this.displayedOctave_ += octaves;
if (this.displayedOctave_ < 0) {
this.displayedOctave_ = 0;
return;
}
var maxOctave = Math.floor(Blockly.FieldNote.MAX_NOTE / 12);
if (this.displayedOctave_ > maxOctave) {
this.displayedOctave_ = maxOctave;
return;
}
var newNote = Number(this.getText()) + (octaves * 12);
this.setNoteNum_(newNote);
this.animationTarget_ = this.fieldEditorWidth_ * octaves * -1;
this.animationPos_ = 0;
this.stepOctaveAnimation_();
this.setCKeyLabelsVisible_(false);
};
/**
* Animate the piano up or down an octave by sliding it to the left or right.
* @private
*/
Blockly.FieldNote.prototype.stepOctaveAnimation_ = function() {
var absDiff = Math.abs(this.animationPos_ - this.animationTarget_);
if (absDiff < 1) {
this.pianoSVG_.setAttribute('transform', 'translate(0, 0)');
this.setCKeyLabelsVisible_(true);
this.playNoteInternal_();
return;
}
this.animationPos_ += (this.animationTarget_ - this.animationPos_) *
Blockly.FieldNote.ANIMATION_FRACTION;
this.pianoSVG_.setAttribute('transform', 'translate(' + this.animationPos_ + ',0)');
requestAnimationFrame(this.stepOctaveAnimation_.bind(this));
};
/**
* Set the selected note number, and update the piano display and the input field.
* @param {number} noteNum The MIDI note number to select.
* @private
*/
Blockly.FieldNote.prototype.setNoteNum_ = function(noteNum) {
noteNum = this.callValidator(noteNum);
this.setValue(noteNum);
Blockly.FieldTextInput.htmlInput_.value = noteNum;
};
/**
* Sets the text in this field. Triggers a rerender of the source block, and
* updates the selection on the field.
* @param {?string} text New text.
*/
Blockly.FieldNote.prototype.setText = function(text) {
Blockly.FieldNote.superClass_.setText.call(this, text);
if (!this.textElement_) {
// Not rendered yet.
return;
}
this.updateSelection_();
// Cached width is obsolete. Clear it.
this.size_.width = 0;
};
/**
* For a MIDI note number, find the index of the corresponding piano key.
* @param {number} noteNum The note number.
* @returns {number} The index of the piano key.
* @private
*/
Blockly.FieldNote.prototype.noteNumToKeyIndex_ = function(noteNum) {
return Math.floor(noteNum) - (this.displayedOctave_ * 12);
};
/**
* Update the selected note and labels on the field.
* @private
*/
Blockly.FieldNote.prototype.updateSelection_ = function() {
var noteNum = Number(this.getText());
// If the note is outside the currently displayed octave, update it
if (this.displayedOctave_ == null ||
noteNum > ((this.displayedOctave_ * 12) + 12) ||
noteNum < (this.displayedOctave_ * 12)) {
this.displayedOctave_ = Math.floor(noteNum / 12);
}
var index = this.noteNumToKeyIndex_(noteNum);
// Clear the highlight on all keys
this.keySVGs_.forEach(function(svg) {
var isBlack = svg.getAttribute('data-isBlack');
if (isBlack === 'true') {
svg.setAttribute('fill', Blockly.FieldNote.BLACK_KEY_COLOR);
} else {
svg.setAttribute('fill', Blockly.FieldNote.WHITE_KEY_COLOR);
}
});
// Set the highlight on the selected key
if (this.keySVGs_[index]) {
this.keySVGs_[index].setAttribute('fill', Blockly.FieldNote.KEY_SELECTED_COLOR);
// Update the note name text
var noteName = Blockly.FieldNote.KEY_INFO[index].name;
this.noteNameText_.textContent = noteName + ' (' + Math.floor(noteNum) + ')';
// Update the low and high C note names
var lowCNum = this.displayedOctave_ * 12;
this.lowCText_.textContent = 'C(' + lowCNum + ')';
this.highCText_.textContent = 'C(' + (lowCNum + 12) + ')';
}
};
/**
* Ensure that only a valid MIDI note number may be entered.
* @param {string} text The user's text.
* @return {?string} A string representing a valid note number, or null if invalid.
*/
Blockly.FieldNote.prototype.classValidator = function(text) {
if (text === null) {
return null;
}
var n = parseFloat(text || 0);
if (isNaN(n)) {
return null;
}
if (n < 0) {
n = 0;
}
if (n > Blockly.FieldNote.MAX_NOTE) {
n = Blockly.FieldNote.MAX_NOTE;
}
return String(n);
};
Blockly.Field.register('field_note', Blockly.FieldNote);

View File

@@ -0,0 +1,366 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Massachusetts Institute of Technology
* All rights reserved.
*
* 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 Field for numbers. Includes validator and numpad on touch.
* @author tmickel@mit.edu (Tim Mickel)
*/
'use strict';
goog.provide('Blockly.FieldNumber');
goog.require('Blockly.FieldTextInput');
goog.require('Blockly.Touch');
goog.require('goog.math');
goog.require('goog.userAgent');
/**
* Class for an editable number field.
* In scratch-blocks, the min/max/precision properties are only used
* to construct a restrictor on typable characters, and to inform the pop-up
* numpad on touch devices.
* These properties are included here (i.e. instead of just accepting a
* decimalAllowed, negativeAllowed) to maintain API compatibility with Blockly
* and Blockly for Android.
* @param {(string|number)=} opt_value The initial content of the field. The value
* should cast to a number, and if it does not, '0' will be used.
* @param {(string|number)=} opt_min Minimum value.
* @param {(string|number)=} opt_max Maximum value.
* @param {(string|number)=} opt_precision Precision for value.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns the accepted text or null to abort
* the change.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
Blockly.FieldNumber = function(opt_value, opt_min, opt_max, opt_precision,
opt_validator) {
var numRestrictor = this.getNumRestrictor(opt_min, opt_max, opt_precision);
opt_value = (opt_value && !isNaN(opt_value)) ? String(opt_value) : '0';
Blockly.FieldNumber.superClass_.constructor.call(
this, opt_value, opt_validator, numRestrictor);
this.addArgType('number');
};
goog.inherits(Blockly.FieldNumber, Blockly.FieldTextInput);
/**
* Construct a FieldNumber from a JSON arg object.
* @param {!Object} options A JSON object with options (value, min, max, and
* precision).
* @returns {!Blockly.FieldNumber} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldNumber.fromJson = function(options) {
return new Blockly.FieldNumber(options['value'],
options['min'], options['max'], options['precision']);
};
/**
* Fixed width of the num-pad drop-down, in px.
* @type {number}
* @const
*/
Blockly.FieldNumber.DROPDOWN_WIDTH = 168;
/**
* Buttons for the num-pad, in order from the top left.
* Values are strings of the number or symbol will be added to the field text
* when the button is pressed.
* @type {Array.<string>}
* @const
*/
// Calculator order
Blockly.FieldNumber.NUMPAD_BUTTONS =
['7', '8', '9', '4', '5', '6', '1', '2', '3', '.', '0', '-', ' '];
/**
* Src for the delete icon to be shown on the num-pad.
* @type {string}
* @const
*/
Blockly.FieldNumber.NUMPAD_DELETE_ICON = 'data:image/svg+xml;utf8,' +
'<svg ' +
'xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">' +
'<path d="M28.89,11.45H16.79a2.86,2.86,0,0,0-2,.84L9.09,1' +
'8a2.85,2.85,0,0,0,0,4l5.69,5.69a2.86,2.86,0,0,0,2,.84h12' +
'.1a2.86,2.86,0,0,0,2.86-2.86V14.31A2.86,2.86,0,0,0,28.89' +
',11.45ZM27.15,22.73a1,1,0,0,1,0,1.41,1,1,0,0,1-.71.3,1,1' +
',0,0,1-.71-0.3L23,21.41l-2.73,2.73a1,1,0,0,1-1.41,0,1,1,' +
'0,0,1,0-1.41L21.59,20l-2.73-2.73a1,1,0,0,1,0-1.41,1,1,0,' +
'0,1,1.41,0L23,18.59l2.73-2.73a1,1,0,1,1,1.42,1.41L24.42,20Z" fill="' +
Blockly.Colours.numPadText + '"/></svg>';
/**
* Currently active field during an edit.
* Used to give a reference to the num-pad button callbacks.
* @type {?FieldNumber}
* @private
*/
Blockly.FieldNumber.activeField_ = null;
/**
* Return an appropriate restrictor, depending on whether this FieldNumber
* allows decimal or negative numbers.
* @param {number|string|undefined} opt_min Minimum value.
* @param {number|string|undefined} opt_max Maximum value.
* @param {number|string|undefined} opt_precision Precision for value.
* @return {!RegExp} Regular expression for this FieldNumber's restrictor.
*/
Blockly.FieldNumber.prototype.getNumRestrictor = function(opt_min, opt_max,
opt_precision) {
this.setConstraints_(opt_min, opt_max, opt_precision);
var pattern = "[\\d]"; // Always allow digits.
if (this.decimalAllowed_) {
pattern += "|[\\.]";
}
if (this.negativeAllowed_) {
pattern += "|[-]";
}
if (this.exponentialAllowed_) {
pattern += "|[eE]";
}
return new RegExp(pattern);
};
/**
* Set the constraints for this field.
* @param {number=} opt_min Minimum number allowed.
* @param {number=} opt_max Maximum number allowed.
* @param {number=} opt_precision Step allowed between numbers
*/
Blockly.FieldNumber.prototype.setConstraints_ = function(opt_min, opt_max,
opt_precision) {
this.decimalAllowed_ = (typeof opt_precision == 'undefined') ||
isNaN(opt_precision) || (opt_precision == 0) ||
(Math.floor(opt_precision) != opt_precision);
this.negativeAllowed_ = (typeof opt_min == 'undefined') || isNaN(opt_min) ||
opt_min < 0;
this.exponentialAllowed_ = this.decimalAllowed_;
};
/**
* Show the inline free-text editor on top of the text and the num-pad if
* appropriate.
* @private
*/
Blockly.FieldNumber.prototype.showEditor_ = function() {
Blockly.FieldNumber.activeField_ = this;
// Do not focus on mobile devices so we can show the num-pad
var showNumPad = this.useTouchInteraction_;
Blockly.FieldNumber.superClass_.showEditor_.call(this, false, showNumPad);
// Show a numeric keypad in the drop-down on touch
if (showNumPad) {
this.showNumPad_();
}
};
/**
* Show the number pad.
* @private
*/
Blockly.FieldNumber.prototype.showNumPad_ = function() {
// If there is an existing drop-down someone else owns, hide it immediately
// and clear it.
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.DropDownDiv.clearContent();
var contentDiv = Blockly.DropDownDiv.getContentDiv();
// Accessibility properties
contentDiv.setAttribute('role', 'menu');
contentDiv.setAttribute('aria-haspopup', 'true');
this.addButtons_(contentDiv);
// Set colour and size of drop-down
Blockly.DropDownDiv.setColour(this.sourceBlock_.parentBlock_.getColour(),
this.sourceBlock_.getColourTertiary());
contentDiv.style.width = Blockly.FieldNumber.DROPDOWN_WIDTH + 'px';
this.position_();
};
/**
* Figure out where to place the drop-down, and move it there.
* @private
*/
Blockly.FieldNumber.prototype.position_ = function() {
// Calculate positioning for the drop-down
// sourceBlock_ is the rendered shadow field input box
var scale = this.sourceBlock_.workspace.scale;
var bBox = this.sourceBlock_.getHeightWidth();
bBox.width *= scale;
bBox.height *= scale;
var position = this.getAbsoluteXY_();
// If we can fit it, render below the shadow block
var primaryX = position.x + bBox.width / 2;
var primaryY = position.y + bBox.height;
// If we can't fit it, render above the entire parent block
var secondaryX = primaryX;
var secondaryY = position.y;
Blockly.DropDownDiv.setBoundsElement(
this.sourceBlock_.workspace.getParentSvg().parentNode);
Blockly.DropDownDiv.show(this, primaryX, primaryY, secondaryX, secondaryY,
this.onHide_.bind(this));
};
/**
* Add number, punctuation, and erase buttons to the numeric keypad's content
* div.
* @param {Element} contentDiv The div for the numeric keypad.
* @private
*/
Blockly.FieldNumber.prototype.addButtons_ = function(contentDiv) {
var buttonColour = this.sourceBlock_.parentBlock_.getColour();
var buttonBorderColour = this.sourceBlock_.parentBlock_.getColourTertiary();
// Add numeric keypad buttons
var buttons = Blockly.FieldNumber.NUMPAD_BUTTONS;
for (var i = 0, buttonText; buttonText = buttons[i]; i++) {
var button = document.createElement('button');
button.setAttribute('role', 'menuitem');
button.setAttribute('class', 'blocklyNumPadButton');
button.setAttribute('style',
'background:' + buttonColour + ';' +
'border: 1px solid ' + buttonBorderColour + ';');
button.title = buttonText;
button.textContent = buttonText;
Blockly.bindEvent_(button, 'mousedown', button,
Blockly.FieldNumber.numPadButtonTouch);
if (buttonText == '.' && !this.decimalAllowed_) {
// Don't show the decimal point for inputs that must be round numbers
button.setAttribute('style', 'visibility: hidden');
} else if (buttonText == '-' && !this.negativeAllowed_) {
continue;
} else if (buttonText == ' ' && !this.negativeAllowed_) {
continue;
} else if (buttonText == ' ' && this.negativeAllowed_) {
button.setAttribute('style', 'visibility: hidden');
}
contentDiv.appendChild(button);
}
// Add erase button to the end
var eraseButton = document.createElement('button');
eraseButton.setAttribute('role', 'menuitem');
eraseButton.setAttribute('class', 'blocklyNumPadButton');
eraseButton.setAttribute('style',
'background:' + buttonColour + ';' +
'border: 1px solid ' + buttonBorderColour + ';');
eraseButton.title = 'Delete';
var eraseImage = document.createElement('img');
eraseImage.src = Blockly.FieldNumber.NUMPAD_DELETE_ICON;
eraseButton.appendChild(eraseImage);
Blockly.bindEvent_(eraseButton, 'mousedown', null,
Blockly.FieldNumber.numPadEraseButtonTouch);
contentDiv.appendChild(eraseButton);
};
/**
* Call for when a num-pad number or punctuation button is touched.
* Determine what the user is inputting and update the text field appropriately.
* @param {Event} e DOM event triggering the touch.
*/
Blockly.FieldNumber.numPadButtonTouch = function(e) {
// String of the button (e.g., '7')
var spliceValue = this.innerHTML;
// Old value of the text field
var oldValue = Blockly.FieldTextInput.htmlInput_.value;
// Determine the selected portion of the text field
var selectionStart = Blockly.FieldTextInput.htmlInput_.selectionStart;
var selectionEnd = Blockly.FieldTextInput.htmlInput_.selectionEnd;
// Splice in the new value
var newValue = oldValue.slice(0, selectionStart) + spliceValue +
oldValue.slice(selectionEnd);
// Set new value and advance the cursor
Blockly.FieldNumber.updateDisplay_(newValue, selectionStart + spliceValue.length);
// This is just a click.
Blockly.Touch.clearTouchIdentifier();
// Prevent default to not lose input focus
e.preventDefault();
};
/**
* Call for when the num-pad erase button is touched.
* Determine what the user is asking to erase, and erase it.
* @param {Event} e DOM event triggering the touch.
*/
Blockly.FieldNumber.numPadEraseButtonTouch = function(e) {
// Old value of the text field
var oldValue = Blockly.FieldTextInput.htmlInput_.value;
// Determine what is selected to erase (if anything)
var selectionStart = Blockly.FieldTextInput.htmlInput_.selectionStart;
var selectionEnd = Blockly.FieldTextInput.htmlInput_.selectionEnd;
// If selection is zero-length, shift start to the left 1 character
if (selectionStart == selectionEnd) {
selectionStart = Math.max(0, selectionStart - 1);
}
// Cut out selected range
var newValue = oldValue.slice(0, selectionStart) +
oldValue.slice(selectionEnd);
Blockly.FieldNumber.updateDisplay_(newValue, selectionStart);
// This is just a click.
Blockly.Touch.clearTouchIdentifier();
// Prevent default to not lose input focus which resets cursors in Chrome
e.preventDefault();
};
/**
* Update the displayed value and resize/scroll the text field as needed.
* @param {string} newValue The new text to display.
* @param {string} newSelection The new index to put the cursor
* @private.
*/
Blockly.FieldNumber.updateDisplay_ = function(newValue, newSelection) {
var htmlInput = Blockly.FieldTextInput.htmlInput_;
// Updates the display. The actual setValue occurs when editing ends.
htmlInput.value = newValue;
// Resize and scroll the text field appropriately
Blockly.FieldNumber.superClass_.resizeEditor_.call(
Blockly.FieldNumber.activeField_);
htmlInput.setSelectionRange(newSelection, newSelection);
htmlInput.scrollLeft = htmlInput.scrollWidth;
Blockly.FieldNumber.activeField_.validate_();
};
/**
* Callback for when the drop-down is hidden.
*/
Blockly.FieldNumber.prototype.onHide_ = function() {
// Clear accessibility properties
Blockly.DropDownDiv.content_.removeAttribute('role');
Blockly.DropDownDiv.content_.removeAttribute('aria-haspopup');
};
Blockly.Field.register('field_number', Blockly.FieldNumber);

View File

@@ -0,0 +1,77 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2013 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 Combination number + drop-down field
* @author tmickel@mit.edu (Tim Mickel)
*/
'use strict';
goog.provide('Blockly.FieldNumberDropdown');
goog.require('Blockly.FieldTextDropdown');
goog.require('goog.userAgent');
/**
* Class for a combination number + drop-down field.
* @param {number|string} value The initial content of the field.
* @param {(!Array.<!Array.<string>>|!Function)} menuGenerator An array of
* options for a dropdown list, or a function which generates these options.
* @param {number|string|undefined} opt_min Minimum value.
* @param {number|string|undefined} opt_max Maximum value.
* @param {number|string|undefined} opt_precision Precision for value.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns the accepted text or null to abort
* the change.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
Blockly.FieldNumberDropdown = function(value, menuGenerator, opt_min, opt_max,
opt_precision, opt_validator) {
this.setConstraints_ = Blockly.FieldNumber.prototype.setConstraints_;
var numRestrictor = Blockly.FieldNumber.prototype.getNumRestrictor.call(
this, opt_min, opt_max, opt_precision
);
Blockly.FieldNumberDropdown.superClass_.constructor.call(
this, value, menuGenerator, opt_validator, numRestrictor
);
this.addArgType('numberdropdown');
};
goog.inherits(Blockly.FieldNumberDropdown, Blockly.FieldTextDropdown);
/**
* Construct a FieldTextDropdown from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} element A JSON object with options.
* @returns {!Blockly.FieldNumberDropdown} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldNumberDropdown.fromJson = function(element) {
return new Blockly.FieldNumberDropdown(
element['value'], element['options'],
element['min'], element['max'], element['precision']
);
};
Blockly.Field.register('field_numberdropdown', Blockly.FieldNumberDropdown);

View File

@@ -0,0 +1,164 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2013 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 Combination text + drop-down field
* @author tmickel@mit.edu (Tim Mickel)
*/
'use strict';
goog.provide('Blockly.FieldTextDropdown');
goog.require('Blockly.DropDownDiv');
goog.require('Blockly.FieldDropdown');
goog.require('Blockly.FieldTextInput');
goog.require('goog.userAgent');
/**
* Class for a combination text + drop-down field.
* @param {string} text The initial content of the text field.
* @param {(!Array.<!Array.<string>>|!Function)} menuGenerator An array of
* options for a dropdown list, or a function which generates these options.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns the accepted text or null to abort
* the change.
* @param {RegExp=} opt_restrictor An optional regular expression to restrict
* typed text to. Text that doesn't match the restrictor will never show
* in the text field.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
Blockly.FieldTextDropdown = function(text, menuGenerator, opt_validator, opt_restrictor) {
this.menuGenerator_ = menuGenerator;
Blockly.FieldDropdown.prototype.trimOptions_.call(this);
Blockly.FieldTextDropdown.superClass_.constructor.call(this, text, opt_validator, opt_restrictor);
this.addArgType('textdropdown');
};
goog.inherits(Blockly.FieldTextDropdown, Blockly.FieldTextInput);
/**
* Construct a FieldTextDropdown from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} element A JSON object with options.
* @returns {!Blockly.FieldTextDropdown} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldTextDropdown.fromJson = function(element) {
var field =
new Blockly.FieldTextDropdown(element['text'], element['options']);
if (typeof element['spellcheck'] == 'boolean') {
field.setSpellcheck(element['spellcheck']);
}
return field;
};
/**
* Install this text drop-down field on a block.
*/
Blockly.FieldTextDropdown.prototype.init = function() {
if (this.fieldGroup_) {
// Text input + dropdown has already been initialized once.
return;
}
Blockly.FieldTextDropdown.superClass_.init.call(this);
// Add dropdown arrow: "option ▾" (LTR) or "▾ אופציה" (RTL)
// Positioned on render, after text size is calculated.
if (!this.arrow_) {
/** @type {Number} */
this.arrowSize_ = 12;
/** @type {Number} */
this.arrowX_ = 0;
/** @type {Number} */
this.arrowY_ = 11;
this.arrow_ = Blockly.utils.createSvgElement('image',
{
'height': this.arrowSize_ + 'px',
'width': this.arrowSize_ + 'px'
});
this.arrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'dropdown-arrow-dark.svg');
this.arrow_.style.cursor = 'pointer';
this.fieldGroup_.appendChild(this.arrow_);
this.mouseUpWrapper_ =
Blockly.bindEvent_(this.arrow_, 'mouseup', this, this.showDropdown_);
}
// Prevent the drop-down handler from changing the field colour on open.
this.disableColourChange_ = true;
};
/**
* Close the input widget if this input is being deleted.
*/
Blockly.FieldTextDropdown.prototype.dispose = function() {
if (this.mouseUpWrapper_) {
Blockly.unbindEvent_(this.mouseUpWrapper_);
this.mouseUpWrapper_ = null;
Blockly.Touch.clearTouchIdentifier();
}
Blockly.FieldTextDropdown.superClass_.dispose.call(this);
};
/**
* If the drop-down isn't open, show the text editor.
*/
Blockly.FieldTextDropdown.prototype.showEditor_ = function() {
if (!this.dropDownOpen_) {
Blockly.FieldTextDropdown.superClass_.showEditor_.call(this, null, null,
true, function() {
// When the drop-down arrow is clicked, hide text editor and show drop-down.
Blockly.WidgetDiv.hide();
this.showDropdown_();
Blockly.Touch.clearTouchIdentifier();
});
}
};
/**
* Return a list of the options for this dropdown.
* See: Blockly.FieldDropDown.prototype.getOptions_.
* @return {!Array.<!Array.<string>>} Array of option tuples:
* (human-readable text, language-neutral name).
* @private
*/
Blockly.FieldTextDropdown.prototype.getOptions_ = Blockly.FieldDropdown.prototype.getOptions_;
/**
* Position a drop-down arrow at the appropriate location at render-time.
* See: Blockly.FieldDropDown.prototype.positionArrow.
* @param {number} x X position the arrow is being rendered at, in px.
* @return {number} Amount of space the arrow is taking up, in px.
*/
Blockly.FieldTextDropdown.prototype.positionArrow = Blockly.FieldDropdown.prototype.positionArrow;
/**
* Create the dropdown menu.
* @private
*/
Blockly.FieldTextDropdown.prototype.showDropdown_ = Blockly.FieldDropdown.prototype.showEditor_;
/**
* Callback when the drop-down menu is hidden.
*/
Blockly.FieldTextDropdown.prototype.onHide = Blockly.FieldDropdown.prototype.onHide;
Blockly.Field.register('field_textdropdown', Blockly.FieldTextDropdown);

View File

@@ -0,0 +1,675 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Text input field.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldTextInput');
goog.require('Blockly.BlockSvg.render');
goog.require('Blockly.Colours');
goog.require('Blockly.Field');
goog.require('Blockly.Msg');
goog.require('Blockly.scratchBlocksUtils');
goog.require('Blockly.utils');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.userAgent');
/**
* Class for an editable text field.
* @param {string} text The initial content of the field.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns either the accepted text, a replacement
* text, or null to abort the change.
* @param {RegExp=} opt_restrictor An optional regular expression to restrict
* typed text to. Text that doesn't match the restrictor will never show
* in the text field.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldTextInput = function(text, opt_validator, opt_restrictor) {
Blockly.FieldTextInput.superClass_.constructor.call(this, text,
opt_validator);
this.setRestrictor(opt_restrictor);
this.addArgType('text');
};
goog.inherits(Blockly.FieldTextInput, Blockly.Field);
/**
* Construct a FieldTextInput from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, class, and
* spellcheck).
* @returns {!Blockly.FieldTextInput} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldTextInput.fromJson = function(options) {
var text = Blockly.utils.replaceMessageReferences(options['text']) || '';
var field = new Blockly.FieldTextInput(text, options['class']);
if (typeof options['spellcheck'] === 'boolean') {
field.setSpellcheck(options['spellcheck']);
}
return field;
};
/**
* Length of animations in seconds.
*/
Blockly.FieldTextInput.ANIMATION_TIME = 0.25;
/**
* Padding to use for text measurement for the field during editing, in px.
*/
Blockly.FieldTextInput.TEXT_MEASURE_PADDING_MAGIC = 45;
/**
* The HTML input element for the user to type, or null if no FieldTextInput
* editor is currently open.
* @type {HTMLInputElement}
* @private
*/
Blockly.FieldTextInput.htmlInput_ = null;
/**
* Mouse cursor style when over the hotspot that initiates the editor.
*/
Blockly.FieldTextInput.prototype.CURSOR = 'text';
/**
* Allow browser to spellcheck this field.
* @private
*/
Blockly.FieldTextInput.prototype.spellcheck_ = true;
/**
* Install this text field on a block.
*/
Blockly.FieldTextInput.prototype.init = function() {
if (this.fieldGroup_) {
// Field has already been initialized once.
return;
}
var notInShadow = !this.sourceBlock_.isShadow();
if (notInShadow) {
this.className_ += ' blocklyEditableLabel';
}
Blockly.FieldTextInput.superClass_.init.call(this);
// If not in a shadow block, draw a box.
if (notInShadow) {
this.box_ = Blockly.utils.createSvgElement('rect',
{
'x': 0,
'y': 0,
'width': this.size_.width,
'height': this.size_.height,
'fill': this.sourceBlock_.getColourTertiary()
}
);
this.fieldGroup_.insertBefore(this.box_, this.textElement_);
}
};
/**
* Close the input widget if this input is being deleted.
*/
Blockly.FieldTextInput.prototype.dispose = function() {
Blockly.WidgetDiv.hideIfOwner(this);
Blockly.FieldTextInput.superClass_.dispose.call(this);
};
/**
* Set the value of this field.
* @param {?string} newValue New value.
* @override
*/
Blockly.FieldTextInput.prototype.setValue = function(newValue) {
if (newValue === null) {
return; // No change if null.
}
if (this.sourceBlock_) {
var validated = this.callValidator(newValue);
// If the new value is invalid, validation returns null.
// In this case we still want to display the illegal result.
if (validated !== null) {
newValue = validated;
}
}
Blockly.Field.prototype.setValue.call(this, newValue);
};
/**
* Set the text in this field and fire a change event.
* @param {*} newText New text.
*/
Blockly.FieldTextInput.prototype.setText = function(newText) {
if (newText === null) {
// No change if null.
return;
}
newText = String(newText);
if (newText === this.text_) {
// No change.
return;
}
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.sourceBlock_, 'field', this.name, this.text_, newText));
}
Blockly.Field.prototype.setText.call(this, newText);
};
/**
* Set whether this field is spellchecked by the browser.
* @param {boolean} check True if checked.
*/
Blockly.FieldTextInput.prototype.setSpellcheck = function(check) {
this.spellcheck_ = check;
};
/**
* Set the restrictor regex for this text input.
* Text that doesn't match the restrictor will never show in the text field.
* @param {?RegExp} restrictor Regular expression to restrict text.
*/
Blockly.FieldTextInput.prototype.setRestrictor = function(restrictor) {
this.restrictor_ = restrictor;
};
/**
* Show the inline free-text editor on top of the text.
* @param {boolean=} opt_quietInput True if editor should be created without
* focus. Defaults to false.
* @param {boolean=} opt_readOnly True if editor should be created with HTML
* input set to read-only, to prevent virtual keyboards.
* @param {boolean=} opt_withArrow True to show drop-down arrow in text editor.
* @param {Function=} opt_arrowCallback Callback for when drop-down arrow clicked.
* @private
*/
Blockly.FieldTextInput.prototype.showEditor_ = function(
opt_quietInput, opt_readOnly, opt_withArrow, opt_arrowCallback) {
this.workspace_ = this.sourceBlock_.workspace;
var quietInput = opt_quietInput || false;
var readOnly = opt_readOnly || false;
Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL,
this.widgetDispose_(), this.widgetDisposeAnimationFinished_(),
Blockly.FieldTextInput.ANIMATION_TIME);
var div = Blockly.WidgetDiv.DIV;
// Apply text-input-specific fixed CSS
div.className += ' fieldTextInput';
// Create the input.
var htmlInput =
goog.dom.createDom(goog.dom.TagName.INPUT, 'blocklyHtmlInput');
htmlInput.setAttribute('spellcheck', this.spellcheck_);
if (readOnly) {
htmlInput.setAttribute('readonly', 'true');
}
/** @type {!HTMLInputElement} */
Blockly.FieldTextInput.htmlInput_ = htmlInput;
div.appendChild(htmlInput);
if (opt_withArrow) {
// Move text in input to account for displayed drop-down arrow.
if (this.sourceBlock_.RTL) {
htmlInput.style.paddingLeft = (this.arrowSize_ + Blockly.BlockSvg.DROPDOWN_ARROW_PADDING) + 'px';
} else {
htmlInput.style.paddingRight = (this.arrowSize_ + Blockly.BlockSvg.DROPDOWN_ARROW_PADDING) + 'px';
}
// Create the arrow.
var dropDownArrow =
goog.dom.createDom(goog.dom.TagName.IMG, 'blocklyTextDropDownArrow');
dropDownArrow.setAttribute('src',
Blockly.mainWorkspace.options.pathToMedia + 'dropdown-arrow-dark.svg');
dropDownArrow.style.width = this.arrowSize_ + 'px';
dropDownArrow.style.height = this.arrowSize_ + 'px';
dropDownArrow.style.top = this.arrowY_ + 'px';
dropDownArrow.style.cursor = 'pointer';
// Magic number for positioning the drop-down arrow on top of the text editor.
var dropdownArrowMagic = '11px';
if (this.sourceBlock_.RTL) {
dropDownArrow.style.left = dropdownArrowMagic;
} else {
dropDownArrow.style.right = dropdownArrowMagic;
}
if (opt_arrowCallback) {
htmlInput.dropDownArrowMouseWrapper_ = Blockly.bindEvent_(dropDownArrow,
'mousedown', this, opt_arrowCallback);
}
div.appendChild(dropDownArrow);
}
htmlInput.value = htmlInput.defaultValue = this.text_;
htmlInput.oldValue_ = null;
this.validate_();
this.resizeEditor_();
if (!quietInput) {
htmlInput.focus();
htmlInput.select();
// For iOS only
htmlInput.setSelectionRange(0, 99999);
}
this.bindEvents_(htmlInput, quietInput || readOnly);
// Add animation transition properties
var transitionProperties = 'box-shadow ' + Blockly.FieldTextInput.ANIMATION_TIME + 's';
if (Blockly.BlockSvg.FIELD_TEXTINPUT_ANIMATE_POSITIONING) {
div.style.transition += ',padding ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
'width ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
'height ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
'margin-left ' + Blockly.FieldTextInput.ANIMATION_TIME + 's';
}
div.style.transition = transitionProperties;
htmlInput.style.transition = 'font-size ' + Blockly.FieldTextInput.ANIMATION_TIME + 's';
// The animated properties themselves
htmlInput.style.fontSize = Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_FINAL + 'pt';
div.style.boxShadow = '0px 0px 0px 4px ' + Blockly.Colours.fieldShadow;
};
/**
* Bind handlers for user input on this field and size changes on the workspace.
* @param {!HTMLInputElement} htmlInput The htmlInput created in showEditor, to
* which event handlers will be bound.
* @param {boolean} bindGlobalKeypress Whether to bind a keypress listener to enable
* keyboard editing without focusing the field.
* @private
*/
Blockly.FieldTextInput.prototype.bindEvents_ = function(
htmlInput, bindGlobalKeypress) {
// Bind to keydown -- trap Enter without IME and Esc to hide.
htmlInput.onKeyDownWrapper_ =
Blockly.bindEventWithChecks_(htmlInput, 'keydown', this,
this.onHtmlInputKeyDown_);
// Bind to keyup -- trap Enter; resize after every keystroke.
htmlInput.onKeyUpWrapper_ =
Blockly.bindEventWithChecks_(htmlInput, 'keyup', this,
this.onHtmlInputChange_);
// Bind to keyPress -- repeatedly resize when holding down a key.
htmlInput.onKeyPressWrapper_ =
Blockly.bindEventWithChecks_(htmlInput, 'keypress', this,
this.onHtmlInputChange_);
// For modern browsers (IE 9+, Chrome, Firefox, etc.) that support the
// DOM input event, also trigger onHtmlInputChange_ then. The input event
// is triggered on keypress but after the value of the text input
// has updated, allowing us to resize the block at that time.
htmlInput.onInputWrapper_ =
Blockly.bindEvent_(htmlInput, 'input', this, this.onHtmlInputChange_);
htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this);
this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_);
if (bindGlobalKeypress) {
htmlInput.onDocumentKeyDownWrapper_ =
Blockly.bindEventWithChecks_(document, 'keydown', this,
this.onDocumentKeyDown_);
}
};
/**
* Unbind handlers for user input and workspace size changes.
* @param {!HTMLInputElement} htmlInput The html for this text input.
* @private
*/
Blockly.FieldTextInput.prototype.unbindEvents_ = function(htmlInput) {
Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
Blockly.unbindEvent_(htmlInput.onInputWrapper_);
this.workspace_.removeChangeListener(
htmlInput.onWorkspaceChangeWrapper_);
// Remove document handler only if it was added (e.g. in quiet mode)
if (htmlInput.onDocumentKeyDownWrapper_) {
Blockly.unbindEvent_(htmlInput.onDocumentKeyDownWrapper_);
}
};
/**
* Handle key down to the editor.
* @param {!Event} e Keyboard event.
* @private
*/
Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) {
var htmlInput = Blockly.FieldTextInput.htmlInput_;
var tabKey = 9, enterKey = 13, escKey = 27;
if (e.keyCode == enterKey) {
Blockly.WidgetDiv.hide();
Blockly.DropDownDiv.hideWithoutAnimation();
} else if (e.keyCode == escKey) {
htmlInput.value = htmlInput.defaultValue;
Blockly.WidgetDiv.hide();
Blockly.DropDownDiv.hideWithoutAnimation();
} else if (e.keyCode == tabKey) {
Blockly.WidgetDiv.hide();
Blockly.DropDownDiv.hideWithoutAnimation();
this.sourceBlock_.tab(this, !e.shiftKey);
e.preventDefault();
}
};
Blockly.FieldTextInput.prototype.onDocumentKeyDown_ = function(e) {
var htmlInput = Blockly.FieldTextInput.htmlInput_;
var targetMatches = e.target === htmlInput;
var targetIsInput = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA';
if (targetMatches || !targetIsInput) { // Ignore keys into other inputs
htmlInput.removeAttribute('readonly');
htmlInput.value = ''; // Reset the input, new value is picked up by input keypress
htmlInput.focus();
Blockly.unbindEvent_(htmlInput.onDocumentKeyDownWrapper_);
htmlInput.onDocumentKeyDownWrapper_ = null;
}
};
/**
* Key codes that are whitelisted from the restrictor.
* These are only needed and used on Gecko (Firefox).
* See: https://github.com/LLK/scratch-blocks/issues/503.
*/
Blockly.FieldTextInput.GECKO_KEYCODE_WHITELIST = [
97, // Select all, META-A.
99, // Copy, META-C.
118, // Paste, META-V.
120 // Cut, META-X.
];
/**
* Handle a change to the editor.
* @param {!Event} e Keyboard event.
* @private
*/
Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(e) {
// Check if the key matches the restrictor.
if (e.type === 'keypress' && this.restrictor_) {
var keyCode;
var isWhitelisted = false;
if (goog.userAgent.GECKO) {
// e.keyCode is not available in Gecko.
keyCode = e.charCode;
// Gecko reports control characters (e.g., left, right, copy, paste)
// in the key event - whitelist these from being restricted.
// < 32 and 127 (delete) are control characters.
// See: http://www.theasciicode.com.ar/ascii-control-characters/delete-ascii-code-127.html
if (keyCode < 32 || keyCode == 127) {
isWhitelisted = true;
} else if (e.metaKey || e.ctrlKey) {
// For combos (ctrl-v, ctrl-c, etc.), Gecko reports the ASCII letter
// and the metaKey/ctrlKey flags.
isWhitelisted = Blockly.FieldTextInput.GECKO_KEYCODE_WHITELIST.indexOf(keyCode) > -1;
}
} else {
keyCode = e.keyCode;
}
var char = String.fromCharCode(keyCode);
if (!isWhitelisted && !this.restrictor_.test(char) && e.preventDefault) {
// Failed to pass restrictor.
e.preventDefault();
return;
}
}
var htmlInput = Blockly.FieldTextInput.htmlInput_;
// Update source block.
var text = htmlInput.value;
if (text !== htmlInput.oldValue_) {
htmlInput.oldValue_ = text;
this.setText(text);
this.validate_();
} else if (goog.userAgent.WEBKIT) {
// Cursor key. Render the source block to show the caret moving.
// Chrome only (version 26, OS X).
this.sourceBlock_.render();
}
this.resizeEditor_();
};
/**
* Check to see if the contents of the editor validates.
* Style the editor accordingly.
* @private
*/
Blockly.FieldTextInput.prototype.validate_ = function() {
var valid = true;
goog.asserts.assertObject(Blockly.FieldTextInput.htmlInput_);
var htmlInput = Blockly.FieldTextInput.htmlInput_;
if (this.sourceBlock_) {
valid = this.callValidator(htmlInput.value);
}
if (valid === null) {
Blockly.utils.addClass(htmlInput, 'blocklyInvalidInput');
} else {
Blockly.utils.removeClass(htmlInput, 'blocklyInvalidInput');
}
};
/**
* Resize the editor and the underlying block to fit the text.
* @private
*/
Blockly.FieldTextInput.prototype.resizeEditor_ = function() {
var scale = this.sourceBlock_.workspace.scale;
var div = Blockly.WidgetDiv.DIV;
var initialWidth;
if (this.sourceBlock_.isShadow()) {
initialWidth = this.sourceBlock_.getHeightWidth().width * scale;
} else {
initialWidth = this.size_.width * scale;
}
var width;
if (Blockly.BlockSvg.FIELD_TEXTINPUT_EXPAND_PAST_TRUNCATION) {
// Resize the box based on the measured width of the text, pre-truncation
var textWidth = Blockly.scratchBlocksUtils.measureText(
Blockly.FieldTextInput.htmlInput_.style.fontSize,
Blockly.FieldTextInput.htmlInput_.style.fontFamily,
Blockly.FieldTextInput.htmlInput_.style.fontWeight,
Blockly.FieldTextInput.htmlInput_.value
);
// Size drawn in the canvas needs padding and scaling
textWidth += Blockly.FieldTextInput.TEXT_MEASURE_PADDING_MAGIC;
textWidth *= scale;
width = textWidth;
} else {
// Set width to (truncated) block size.
width = initialWidth;
}
// The width must be at least FIELD_WIDTH and at most FIELD_WIDTH_MAX_EDIT
width = Math.max(width, Blockly.BlockSvg.FIELD_WIDTH_MIN_EDIT * scale);
width = Math.min(width, Blockly.BlockSvg.FIELD_WIDTH_MAX_EDIT * scale);
// Add 1px to width and height to account for border (pre-scale)
div.style.width = (width / scale + 1) + 'px';
div.style.height = (Blockly.BlockSvg.FIELD_HEIGHT_MAX_EDIT + 1) + 'px';
div.style.transform = 'scale(' + scale + ')';
// Use margin-left to animate repositioning of the box (value is unscaled).
// This is the difference between the default position and the positioning
// after growing the box.
div.style.marginLeft = -0.5 * (width - initialWidth) + 'px';
// Add 0.5px to account for slight difference between SVG and CSS border
var borderRadius = this.getBorderRadius() + 0.5;
div.style.borderRadius = borderRadius + 'px';
Blockly.FieldTextInput.htmlInput_.style.borderRadius = borderRadius + 'px';
// Pull stroke colour from the existing shadow block
var strokeColour = this.sourceBlock_.getColourTertiary();
div.style.borderColor = strokeColour;
var xy = this.getAbsoluteXY_();
// Account for border width, post-scale
xy.x -= scale / 2;
xy.y -= scale / 2;
// In RTL mode block fields and LTR input fields the left edge moves,
// whereas the right edge is fixed. Reposition the editor.
if (this.sourceBlock_.RTL) {
xy.x += width;
xy.x -= div.offsetWidth * scale;
xy.x += 1 * scale;
}
// Shift by a few pixels to line up exactly.
xy.y += 1 * scale;
if (goog.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) {
// Firefox mis-reports the location of the border by a pixel
// once the WidgetDiv is moved into position.
xy.x += 2 * scale;
xy.y += 1 * scale;
}
if (goog.userAgent.WEBKIT) {
xy.y -= 1 * scale;
}
// Finally, set the actual style
div.style.left = xy.x + 'px';
div.style.top = xy.y + 'px';
};
/**
* Border radius for drawing this field, called when rendering the owning shadow block.
* @return {Number} Border radius in px.
*/
Blockly.FieldTextInput.prototype.getBorderRadius = function() {
if (this.sourceBlock_.getOutputShape() == Blockly.OUTPUT_SHAPE_ROUND) {
return Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS;
}
return Blockly.BlockSvg.TEXT_FIELD_CORNER_RADIUS;
};
/**
* Close the editor, save the results, and start animating the disposal of elements.
* @return {!Function} Closure to call on destruction of the WidgetDiv.
* @private
*/
Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
var thisField = this;
return function() {
var div = Blockly.WidgetDiv.DIV;
var htmlInput = Blockly.FieldTextInput.htmlInput_;
// Save the edit (if it validates).
thisField.maybeSaveEdit_();
thisField.unbindEvents_(htmlInput);
if (htmlInput.dropDownArrowMouseWrapper_) {
Blockly.unbindEvent_(htmlInput.dropDownArrowMouseWrapper_);
}
Blockly.Events.setGroup(false);
// Animation of disposal
htmlInput.style.fontSize = Blockly.BlockSvg.FIELD_TEXTINPUT_FONTSIZE_INITIAL + 'pt';
div.style.boxShadow = '';
// Resize to actual size of final source block.
if (thisField.sourceBlock_) {
if (thisField.sourceBlock_.isShadow()) {
var size = thisField.sourceBlock_.getHeightWidth();
div.style.width = (size.width + 1) + 'px';
div.style.height = (size.height + 1) + 'px';
} else {
div.style.width = (thisField.size_.width + 1) + 'px';
div.style.height = (Blockly.BlockSvg.FIELD_HEIGHT_MAX_EDIT + 1) + 'px';
}
}
div.style.marginLeft = 0;
};
};
/**
* Final disposal of the text field's elements and properties.
* @return {!Function} Closure to call on finish animation of the WidgetDiv.
* @private
*/
Blockly.FieldTextInput.prototype.widgetDisposeAnimationFinished_ = function() {
return function() {
// Delete style properties.
var style = Blockly.WidgetDiv.DIV.style;
style.width = 'auto';
style.height = 'auto';
style.fontSize = '';
// Reset class
Blockly.WidgetDiv.DIV.className = 'blocklyWidgetDiv';
// Remove all styles
Blockly.WidgetDiv.DIV.removeAttribute('style');
Blockly.FieldTextInput.htmlInput_.style.transition = '';
Blockly.FieldTextInput.htmlInput_ = null;
};
};
Blockly.FieldTextInput.prototype.maybeSaveEdit_ = function() {
var htmlInput = Blockly.FieldTextInput.htmlInput_;
// Save the edit (if it validates).
var text = htmlInput.value;
if (this.sourceBlock_) {
var text1 = this.callValidator(text);
if (text1 === null) {
// Invalid edit.
text = htmlInput.defaultValue;
} else {
// Validation function has changed the text.
text = text1;
if (this.onFinishEditing_) {
this.onFinishEditing_(text);
}
}
}
this.setText(text);
this.sourceBlock_.rendered && this.sourceBlock_.render();
};
/**
* Ensure that only a number may be entered.
* @param {string} text The user's text.
* @return {?string} A string representing a valid number, or null if invalid.
*/
Blockly.FieldTextInput.numberValidator = function(text) {
console.warn('Blockly.FieldTextInput.numberValidator is deprecated. ' +
'Use Blockly.FieldNumber instead.');
if (text === null) {
return null;
}
text = String(text);
// TODO: Handle cases like 'ten', '1.203,14', etc.
// 'O' is sometimes mistaken for '0' by inexperienced users.
text = text.replace(/O/ig, '0');
// Strip out thousands separators.
text = text.replace(/,/g, '');
var n = parseFloat(text || 0);
return isNaN(n) ? null : String(n);
};
/**
* Ensure that only a nonnegative integer may be entered.
* @param {string} text The user's text.
* @return {?string} A string representing a valid int, or null if invalid.
*/
Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) {
var n = Blockly.FieldTextInput.numberValidator(text);
if (n) {
n = String(Math.max(0, Math.floor(n)));
}
return n;
};
Blockly.Field.register('field_input', Blockly.FieldTextInput);

View File

@@ -0,0 +1,105 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2016 Massachusetts Institute of Technology
* All rights reserved.
*
* 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 Text input field with floating "remove" button.
* @author pkaplan@media.mit.edu (Paul Kaplan)
*/
'use strict';
goog.provide('Blockly.FieldTextInputRemovable');
goog.require('Blockly.BlockSvg.render');
goog.require('Blockly.Colours');
goog.require('Blockly.FieldTextInput');
goog.require('Blockly.Msg');
goog.require('Blockly.utils');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
/**
* Class for an editable text field displaying a deletion icon when selected.
* @param {string} text The initial content of the field.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns either the accepted text, a replacement
* text, or null to abort the change.
* @param {RegExp=} opt_restrictor An optional regular expression to restrict
* typed text to. Text that doesn't match the restrictor will never show
* in the text field.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
Blockly.FieldTextInputRemovable = function(text, opt_validator, opt_restrictor) {
Blockly.FieldTextInputRemovable.superClass_.constructor.call(this, text,
opt_validator, opt_restrictor);
};
goog.inherits(Blockly.FieldTextInputRemovable, Blockly.FieldTextInput);
/**
* Show the inline free-text editor on top of the text with the remove button.
* @private
*/
Blockly.FieldTextInputRemovable.prototype.showEditor_ = function() {
Blockly.FieldTextInputRemovable.superClass_.showEditor_.call(this);
var div = Blockly.WidgetDiv.DIV;
div.className += ' removableTextInput';
var removeButton =
goog.dom.createDom(goog.dom.TagName.IMG, 'blocklyTextRemoveIcon');
removeButton.setAttribute('src',
Blockly.mainWorkspace.options.pathToMedia + 'icons/remove.svg');
this.removeButtonMouseWrapper_ = Blockly.bindEvent_(removeButton,
'mousedown', this, this.removeCallback_);
div.appendChild(removeButton);
};
/**
* Function to call when remove button is called. Checks for removeFieldCallback
* on sourceBlock and calls it if possible.
* @private
*/
Blockly.FieldTextInputRemovable.prototype.removeCallback_ = function() {
if (this.sourceBlock_ && this.sourceBlock_.removeFieldCallback) {
this.sourceBlock_.removeFieldCallback(this);
} else {
console.warn('Expected a source block with removeFieldCallback');
}
};
/**
* Helper function to construct a FieldTextInputRemovable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, class, and
* spellcheck).
* @returns {!Blockly.FieldTextInputRemovable} The new text input.
* @public
*/
Blockly.FieldTextInputRemovable.fromJson = function(options) {
var text = Blockly.utils.replaceMessageReferences(options['text']);
var field = new Blockly.FieldTextInputRemovable(text, options['class']);
if (typeof options['spellcheck'] == 'boolean') {
field.setSpellcheck(options['spellcheck']);
}
return field;
};
Blockly.Field.register(
'field_input_removable', Blockly.FieldTextInputRemovable);

View File

@@ -0,0 +1,385 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Variable input field.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.FieldVariable');
goog.require('Blockly.FieldDropdown');
goog.require('Blockly.Msg');
goog.require('Blockly.VariableModel');
goog.require('Blockly.Variables');
goog.require('goog.asserts');
goog.require('goog.string');
/**
* Class for a variable's dropdown field.
* @param {?string} varname The default name for the variable. If null,
* a unique variable name will be generated.
* @param {Function=} opt_validator A function that is executed when a new
* option is selected. Its sole argument is the new option value.
* @param {Array.<string>} opt_variableTypes A list of the types of variables to
* include in the dropdown.
* @extends {Blockly.FieldDropdown}
* @constructor
*/
Blockly.FieldVariable = function(varname, opt_validator, opt_variableTypes) {
// The FieldDropdown constructor would call setValue, which might create a
// spurious variable. Just do the relevant parts of the constructor.
this.menuGenerator_ = Blockly.FieldVariable.dropdownCreate;
this.size_ = new goog.math.Size(Blockly.BlockSvg.FIELD_WIDTH,
Blockly.BlockSvg.FIELD_HEIGHT);
this.setValidator(opt_validator);
// TODO (blockly #1499): Add opt_default_type to match default value.
// If not set, ''.
this.defaultVariableName = (varname || '');
var hasSingleVarType = opt_variableTypes && (opt_variableTypes.length == 1);
this.defaultType_ = hasSingleVarType ? opt_variableTypes[0] : '';
this.variableTypes = opt_variableTypes;
this.addArgType('variable');
this.value_ = null;
};
goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown);
/**
* Construct a FieldVariable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (variable,
* variableTypes, and defaultType).
* @returns {!Blockly.FieldVariable} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldVariable.fromJson = function(options) {
var varname = Blockly.utils.replaceMessageReferences(options['variable']);
var variableTypes = options['variableTypes'];
return new Blockly.FieldVariable(varname, null, variableTypes);
};
/**
* Initialize everything needed to render this field. This includes making sure
* that the field's value is valid.
* @public
*/
Blockly.FieldVariable.prototype.init = function() {
if (this.fieldGroup_) {
// Dropdown has already been initialized once.
return;
}
Blockly.FieldVariable.superClass_.init.call(this);
// TODO (blockly #1010): Change from init/initModel to initView/initModel
this.initModel();
};
/**
* Initialize the model for this field if it has not already been initialized.
* If the value has not been set to a variable by the first render, we make up a
* variable rather than let the value be invalid.
* @package
*/
Blockly.FieldVariable.prototype.initModel = function() {
if (this.variable_) {
return; // Initialization already happened.
}
this.workspace_ = this.sourceBlock_.workspace;
// Initialize this field if it's in a broadcast block in the flyout
var variable = this.initFlyoutBroadcast_(this.workspace_);
if (!variable) {
var variable = Blockly.Variables.getOrCreateVariablePackage(
this.workspace_, null, this.defaultVariableName, this.defaultType_);
}
// Don't fire a change event for this setValue. It would have null as the
// old value, which is not valid.
Blockly.Events.disable();
try {
this.setValue(variable.getId());
} finally {
Blockly.Events.enable();
}
};
/**
* Initialize broadcast blocks in the flyout.
* Implicit deletion of broadcast messages from the scratch vm may cause
* broadcast blocks in the flyout to change which variable they display as the
* selected option when the workspace is refreshed.
* Re-sort the broadcast messages by name, and set the field value to the id
* of the variable that comes first in sorted order.
* @param {!Blockly.Workspace} workspace The flyout workspace containing the
* broadcast block.
* @return {string} The variable of type 'broadcast_msg' that comes
* first in sorted order.
*/
Blockly.FieldVariable.prototype.initFlyoutBroadcast_ = function(workspace) {
// Using shorter name for this constant
var broadcastMsgType = Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE;
var broadcastVars = workspace.getVariablesOfType(broadcastMsgType);
if(workspace.isFlyout && this.defaultType_ == broadcastMsgType &&
broadcastVars.length != 0) {
broadcastVars.sort(Blockly.VariableModel.compareByName);
return broadcastVars[0];
}
};
/**
* Dispose of this field.
* @public
*/
Blockly.FieldVariable.dispose = function() {
Blockly.FieldVariable.superClass_.dispose.call(this);
this.workspace_ = null;
this.variableMap_ = null;
};
/**
* Attach this field to a block.
* @param {!Blockly.Block} block The block containing this field.
*/
Blockly.FieldVariable.prototype.setSourceBlock = function(block) {
goog.asserts.assert(!block.isShadow(),
'Variable fields are not allowed to exist on shadow blocks.');
Blockly.FieldVariable.superClass_.setSourceBlock.call(this, block);
};
/**
* Get the variable's ID.
* @return {string} Current variable's ID.
*/
Blockly.FieldVariable.prototype.getValue = function() {
return this.variable_ ? this.variable_.getId() : null;
};
/**
* Get the text from this field, which is the selected variable's name.
* @return {string} The selected variable's name, or the empty string if no
* variable is selected.
*/
Blockly.FieldVariable.prototype.getText = function() {
return this.variable_ ? this.variable_.name : '';
};
/**
* Get the variable model for the selected variable.
* Not guaranteed to be in the variable map on the workspace (e.g. if accessed
* after the variable has been deleted).
* @return {?Blockly.VariableModel} the selected variable, or null if none was
* selected.
* @package
*/
Blockly.FieldVariable.prototype.getVariable = function() {
return this.variable_;
};
/**
* Set the variable ID.
* @param {string} id New variable ID, which must reference an existing
* variable.
*/
Blockly.FieldVariable.prototype.setValue = function(id) {
var workspace = this.sourceBlock_.workspace;
var variable = Blockly.Variables.getVariable(workspace, id);
if (!variable) {
throw new Error('Variable id doesn\'t point to a real variable! ID was ' +
id);
}
// Type checks!
var type = variable.type;
if (!this.typeIsAllowed_(type)) {
throw new Error('Variable type doesn\'t match this field! Type was ' +
type);
}
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
var oldValue = this.variable_ ? this.variable_.getId() : null;
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.sourceBlock_, 'field', this.name, oldValue, id));
}
this.variable_ = variable;
this.value_ = id;
this.setText(variable.name);
};
/**
* Check whether the given variable type is allowed on this field.
* @param {string} type The type to check.
* @return {boolean} True if the type is in the list of allowed types.
* @private
*/
Blockly.FieldVariable.prototype.typeIsAllowed_ = function(type) {
var typeList = this.getVariableTypes_();
if (!typeList) {
return true; // If it's null, all types are valid.
}
for (var i = 0; i < typeList.length; i++) {
if (type == typeList[i]) {
return true;
}
}
return false;
};
/**
* Return a list of variable types to include in the dropdown.
* @return {!Array.<string>} Array of variable types.
* @throws {Error} if variableTypes is an empty array.
* @private
*/
Blockly.FieldVariable.prototype.getVariableTypes_ = function() {
// TODO (#1513): Try to avoid calling this every time the field is edited.
var variableTypes = this.variableTypes;
if (variableTypes === null) {
// If variableTypes is null, return all variable types.
if (this.sourceBlock_) {
var workspace = this.sourceBlock_.workspace;
return workspace.getVariableTypes();
}
}
variableTypes = variableTypes || [''];
if (variableTypes.length == 0) {
// Throw an error if variableTypes is an empty list.
var name = this.getText();
throw new Error('\'variableTypes\' of field variable ' +
name + ' was an empty list');
}
return variableTypes;
};
/**
* Return a sorted list of variable names for variable dropdown menus.
* Include a special option at the end for creating a new variable name.
* @return {!Array.<string>} Array of variable names.
* @this {Blockly.FieldVariable}
*/
Blockly.FieldVariable.dropdownCreate = function() {
if (!this.variable_) {
throw new Error('Tried to call dropdownCreate on a variable field with no' +
' variable selected.');
}
var variableModelList = [];
var name = this.getText();
var workspace = null;
if (this.sourceBlock_) {
workspace = this.sourceBlock_.workspace;
}
if (workspace) {
var variableTypes = this.getVariableTypes_();
var variableModelList = [];
// Get a copy of the list, so that adding rename and new variable options
// doesn't modify the workspace's list.
for (var i = 0; i < variableTypes.length; i++) {
var variableType = variableTypes[i];
var variables = workspace.getVariablesOfType(variableType);
variableModelList = variableModelList.concat(variables);
var potentialVarMap = workspace.getPotentialVariableMap();
if (potentialVarMap) {
var potentialVars = potentialVarMap.getVariablesOfType(variableType);
variableModelList = variableModelList.concat(potentialVars);
}
}
}
variableModelList.sort(Blockly.VariableModel.compareByName);
var options = [];
for (var i = 0; i < variableModelList.length; i++) {
// Set the uuid as the internal representation of the variable.
options[i] = [variableModelList[i].name, variableModelList[i].getId()];
}
if (this.defaultType_ == Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE) {
options.unshift(
[Blockly.Msg.NEW_BROADCAST_MESSAGE, Blockly.NEW_BROADCAST_MESSAGE_ID]);
} else {
// Scalar variables and lists have the same backing action, but the option
// text is different.
if (this.defaultType_ == Blockly.LIST_VARIABLE_TYPE) {
var renameText = Blockly.Msg.RENAME_LIST;
var deleteText = Blockly.Msg.DELETE_LIST;
} else {
var renameText = Blockly.Msg.RENAME_VARIABLE;
var deleteText = Blockly.Msg.DELETE_VARIABLE;
}
options.push([renameText, Blockly.RENAME_VARIABLE_ID]);
if (deleteText) {
options.push(
[
deleteText.replace('%1', name),
Blockly.DELETE_VARIABLE_ID
]);
}
}
return options;
};
/**
* Handle the selection of an item in the variable dropdown menu.
* Special case the 'Rename variable...', 'Delete variable...',
* and 'New message...' options.
* In the rename case, prompt the user for a new name.
* @param {!goog.ui.Menu} menu The Menu component clicked.
* @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu.
*/
Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) {
var id = menuItem.getValue();
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
var workspace = this.sourceBlock_.workspace;
if (id == Blockly.RENAME_VARIABLE_ID) {
// Rename variable.
Blockly.Variables.renameVariable(workspace, this.variable_);
return;
} else if (id == Blockly.DELETE_VARIABLE_ID) {
// Delete variable.
workspace.deleteVariableById(this.variable_.getId());
return;
} else if (id == Blockly.NEW_BROADCAST_MESSAGE_ID) {
var thisField = this;
var updateField = function(varId) {
if (varId) {
thisField.setValue(varId);
}
};
Blockly.Variables.createVariable(workspace, updateField,
Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE);
return;
}
// TODO (blockly #1529): Call any validation function, and allow it to override.
}
this.setValue(id);
};
/**
* Overrides referencesVariables(), indicating this field refers to a variable.
* @return {boolean} True.
* @package
* @override
*/
Blockly.FieldVariable.prototype.referencesVariables = function() {
return true;
};
Blockly.Field.register('field_variable', Blockly.FieldVariable);

View File

@@ -0,0 +1,185 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Variable getter field. Appears as a label but has a variable
* picker in the right-click menu.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.FieldVariableGetter');
goog.require('Blockly.Field');
/**
* Class for a variable getter field.
* @param {string} text The initial content of the field.
* @param {string} name Optional CSS class for the field's text.
* @param {string} opt_varType The type of variable this field is associated with.
* @extends {Blockly.FieldLabel}
* @constructor
*
*/
Blockly.FieldVariableGetter = function(text, name, opt_varType) {
this.size_ = new goog.math.Size(Blockly.BlockSvg.FIELD_WIDTH,
Blockly.BlockSvg.FIELD_HEIGHT);
this.text_ = text;
/**
* Maximum characters of text to display before adding an ellipsis.
* Same for strings and numbers.
* @type {number}
*/
this.maxDisplayLength = Blockly.BlockSvg.MAX_DISPLAY_LENGTH;
this.name_ = name;
this.variableType_ = opt_varType ? opt_varType : '';
};
goog.inherits(Blockly.FieldVariableGetter, Blockly.Field);
/**
* Construct a FieldVariableGetter from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (variable,
* variableTypes, and defaultType).
* @returns {!Blockly.FieldVariableGetter} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldVariableGetter.fromJson = function(options) {
var varname = Blockly.utils.replaceMessageReferences(options['text']);
return new Blockly.FieldVariableGetter(varname, options['name'],
options['class'], options['variableType']);
};
/**
* Editable fields usually show some sort of UI for the user to change them.
* This field should be serialized, but only edited programmatically.
* @type {boolean}
* @public
*/
Blockly.FieldVariableGetter.prototype.EDITABLE = false;
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. This field should be serialized, but only edited programmatically.
* @type {boolean}
* @public
*/
Blockly.FieldVariableGetter.prototype.SERIALIZABLE = true;
/**
* Install this field on a block.
*/
Blockly.FieldVariableGetter.prototype.init = function() {
if (this.fieldGroup_) {
// Field has already been initialized once.
return;
}
Blockly.FieldVariableGetter.superClass_.init.call(this);
if (this.variable_) {
return; // Initialization already happened.
}
this.workspace_ = this.sourceBlock_.workspace;
var variable = Blockly.Variables.getOrCreateVariablePackage(
this.workspace_, null, this.text_, this.variableType_);
this.setValue(variable.getId());
};
/**
* Get the variable's ID.
* @return {string} Current variable's ID.
*/
Blockly.FieldVariableGetter.prototype.getValue = function() {
return this.variable_ ? this.variable_.getId() : '';
};
/**
* Get the text from this field.
* @return {string} Current text.
*/
Blockly.FieldVariableGetter.prototype.getText = function() {
return this.variable_ ? this.variable_.name : '';
};
/**
* Get the variable model for the variable associated with this field.
* Not guaranteed to be in the variable map on the workspace (e.g. if accessed
* after the variable has been deleted).
* @return {?Blockly.VariableModel} the selected variable, or null if none was
* selected.
* @package
*/
Blockly.FieldVariableGetter.prototype.getVariable = function() {
return this.variable_;
};
Blockly.FieldVariableGetter.prototype.setValue = function(id) {
// What do I do when id is null? That happens when undoing a change event
// for the first time the value was set.
var workspace = this.sourceBlock_.workspace;
var variable = Blockly.Variables.getVariable(workspace, id);
if (!variable) {
throw new Error('Variable id doesn\'t point to a real variable! ID was ' +
id);
}
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
var oldValue = this.variable_ ? this.variable_.getId() : null;
Blockly.Events.fire(new Blockly.Events.BlockChange(
this.sourceBlock_, 'field', this.name, oldValue, variable.getId()));
}
this.variable_ = variable;
this.value_ = id;
this.setText(variable.name);
};
/**
* This field is editable, but only through the right-click menu.
* @private
*/
Blockly.FieldVariableGetter.prototype.showEditor_ = function() {
// nop.
};
/**
* Add or remove the UI indicating if this field is editable or not.
* This field is editable, but only through the right-click menu.
* Suppress default editable behaviour.
*/
Blockly.FieldVariableGetter.prototype.updateEditable = function() {
// nop.
};
/**
* Whether this field references any Blockly variables. If true it may need to
* be handled differently during serialization and deserialization. Subclasses
* may override this.
* @return {boolean} True if this field has any variable references.
* @package
*/
Blockly.FieldVariableGetter.prototype.referencesVariables = function() {
return true;
};
Blockly.Field.register('field_variable_getter', Blockly.FieldVariableGetter);

View File

@@ -0,0 +1,161 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 Massachusetts Institute of Technology
* 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 Vertical separator field. Draws a vertical line.
* @author ericr@media.mit.edu (Eric Rosenbaum)
*/
'use strict';
goog.provide('Blockly.FieldVerticalSeparator');
goog.require('Blockly.Field');
goog.require('goog.dom');
goog.require('goog.math.Size');
/**
* Class for a vertical separator line.
* @extends {Blockly.Field}
* @constructor
*/
Blockly.FieldVerticalSeparator = function() {
this.sourceBlock_ = null;
this.width_ = 1;
this.height_ = Blockly.BlockSvg.ICON_SEPARATOR_HEIGHT;
this.size_ = new goog.math.Size(this.width_, this.height_);
};
goog.inherits(Blockly.FieldVerticalSeparator, Blockly.Field);
/**
* Construct a FieldVerticalSeparator from a JSON arg object.
* @param {!Object} _element A JSON object with options (unused, but passed in
* by Field.fromJson).
* @returns {!Blockly.FieldVerticalSeparator} The new field instance.
* @package
* @nocollapse
*/
Blockly.FieldVerticalSeparator.fromJson = function(
/* eslint-disable no-unused-vars */ _element
/* eslint-enable no-unused-vars */) {
return new Blockly.FieldVerticalSeparator();
};
/**
* Editable fields are saved by the XML renderer, non-editable fields are not.
*/
Blockly.FieldVerticalSeparator.prototype.EDITABLE = false;
/**
* Install this field on a block.
*/
Blockly.FieldVerticalSeparator.prototype.init = function() {
if (this.fieldGroup_) {
// Image has already been initialized once.
return;
}
// Build the DOM.
/** @type {SVGElement} */
this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null);
if (!this.visible_) {
this.fieldGroup_.style.display = 'none';
}
/** @type {SVGElement} */
this.lineElement_ = Blockly.utils.createSvgElement('line',
{
'stroke': this.sourceBlock_.getColourSecondary(),
'stroke-linecap': 'round',
'x1': 0,
'y1': 0,
'x2': 0,
'y2': this.height_
}, this.fieldGroup_);
this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
};
/**
* Set the height of the line element, without adjusting the field's height.
* This allows the line's height to be changed without causing it to be
* centered with the new height (needed for correct rendering of hat blocks).
* @param {number} newHeight the new height for the line.
* @package
*/
Blockly.FieldVerticalSeparator.prototype.setLineHeight = function(newHeight) {
this.lineElement_.setAttribute('y2', newHeight);
};
/**
* Dispose of all DOM objects belonging to this text.
*/
Blockly.FieldVerticalSeparator.prototype.dispose = function() {
goog.dom.removeNode(this.fieldGroup_);
this.fieldGroup_ = null;
this.lineElement_ = null;
};
/**
* Get the value of this field. A no-op in this case.
* @return {string} null.
* @override
*/
Blockly.FieldVerticalSeparator.prototype.getValue = function() {
return null;
};
/**
* Set the value of this field. A no-op in this case.
* @param {?string} src New value.
* @override
*/
Blockly.FieldVerticalSeparator.prototype.setValue = function(
/* eslint-disable no-unused-vars */ src
/* eslint-enable no-unused-vars */) {
return;
};
/**
* Set the text of this field. A no-op in this case.
* @param {?string} alt New text.
* @override
*/
Blockly.FieldVerticalSeparator.prototype.setText = function(
/* eslint-disable no-unused-vars */ alt
/* eslint-enable no-unused-vars */) {
return;
};
/**
* Separator lines are fixed width, no need to render.
* @private
*/
Blockly.FieldVerticalSeparator.prototype.render_ = function() {
// NOP
};
/**
* Separator lines are fixed width, no need to update.
* @private
*/
Blockly.FieldVerticalSeparator.prototype.updateWidth = function() {
// NOP
};
Blockly.Field.register(
'field_vertical_separator', Blockly.FieldVerticalSeparator);

View File

@@ -0,0 +1,935 @@
/**
* @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 Flyout tray containing blocks which may be created.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Flyout');
goog.require('Blockly.Block');
goog.require('Blockly.Comment');
goog.require('Blockly.Events');
goog.require('Blockly.Events.BlockCreate');
goog.require('Blockly.Events.VarCreate');
goog.require('Blockly.FlyoutButton');
goog.require('Blockly.FlyoutExtensionCategoryHeader');
goog.require('Blockly.Gesture');
goog.require('Blockly.scratchBlocksUtils');
goog.require('Blockly.Touch');
goog.require('Blockly.WorkspaceSvg');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.math.Rect');
goog.require('goog.userAgent');
/**
* Class for a flyout.
* @param {!Object} workspaceOptions Dictionary of options for the workspace.
* @constructor
*/
Blockly.Flyout = function(workspaceOptions) {
workspaceOptions.getMetrics = this.getMetrics_.bind(this);
workspaceOptions.setMetrics = this.setMetrics_.bind(this);
/**
* @type {!Blockly.Workspace}
* @protected
*/
this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions);
this.workspace_.isFlyout = true;
// When we create blocks for this workspace, instead of using the "optional" id
// make the default `id` the same as the `type` for easier re-use.
var newBlock = this.workspace_.newBlock;
this.workspace_.newBlock = function(type, id) {
// Use `type` if `id` isn't passed. `this` will be workspace.
return newBlock.call(this, type, id || type);
};
/**
* Is RTL vs LTR.
* @type {boolean}
*/
this.RTL = !!workspaceOptions.RTL;
/**
* Flyout should be laid out horizontally vs vertically.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = workspaceOptions.horizontalLayout;
/**
* Position of the toolbox and flyout relative to the workspace.
* @type {number}
* @protected
*/
this.toolboxPosition_ = workspaceOptions.toolboxPosition;
/**
* Opaque data that can be passed to Blockly.unbindEvent_.
* @type {!Array.<!Array>}
* @private
*/
this.eventWrappers_ = [];
/**
* List of background buttons that lurk behind each block to catch clicks
* landing in the blocks' lakes and bays.
* @type {!Array.<!Element>}
* @private
*/
this.backgroundButtons_ = [];
/**
* List of visible buttons.
* @type {!Array.<!Blockly.FlyoutButton>}
* @protected
*/
this.buttons_ = [];
/**
* List of event listeners.
* @type {!Array.<!Array>}
* @private
*/
this.listeners_ = [];
/**
* List of blocks that should always be disabled.
* @type {!Array.<!Blockly.Block>}
* @private
*/
this.permanentlyDisabled_ = [];
/**
* The toolbox that this flyout belongs to, or none if tihs is a simple
* workspace.
* @type {Blockly.Toolbox}
* @private
*/
this.parentToolbox_ = null;
/**
* The target position for the flyout scroll animation in pixels.
* Is a number while animating, null otherwise.
* @type {?number}
* @package
*/
this.scrollTarget = null;
/**
* A recycle bin for blocks.
* @type {!Array.<!Blockly.Block>}
* @private
*/
this.recycleBlocks_ = [];
};
/**
* Does the flyout automatically close when a block is created?
* @type {boolean}
*/
Blockly.Flyout.prototype.autoClose = false;
/**
* Whether the flyout is visible.
* @type {boolean}
* @private
*/
Blockly.Flyout.prototype.isVisible_ = false;
/**
* Whether the workspace containing this flyout is visible.
* @type {boolean}
* @private
*/
Blockly.Flyout.prototype.containerVisible_ = true;
/**
* Corner radius of the flyout background.
* @type {number}
* @const
*/
Blockly.Flyout.prototype.CORNER_RADIUS = 0;
/**
* Margin around the edges of the blocks in the flyout.
* @type {number}
* @const
*/
Blockly.Flyout.prototype.MARGIN = 12;
// TODO: Move GAP_X and GAP_Y to their appropriate files.
/**
* Gap between items in horizontal flyouts. Can be overridden with the "sep"
* element.
* @const {number}
*/
Blockly.Flyout.prototype.GAP_X = Blockly.Flyout.prototype.MARGIN * 3;
/**
* Gap between items in vertical flyouts. Can be overridden with the "sep"
* element.
* @const {number}
*/
Blockly.Flyout.prototype.GAP_Y = Blockly.Flyout.prototype.MARGIN;
/**
* Top/bottom padding between scrollbar and edge of flyout background.
* @type {number}
* @const
*/
Blockly.Flyout.prototype.SCROLLBAR_PADDING = 2;
/**
* Width of flyout.
* @type {number}
* @protected
*/
Blockly.Flyout.prototype.width_ = 0;
/**
* Height of flyout.
* @type {number}
* @protected
*/
Blockly.Flyout.prototype.height_ = 0;
/**
* Width of flyout contents.
* @type {number}
* @private
*/
Blockly.Flyout.prototype.contentWidth_ = 0;
/**
* Height of flyout contents.
* @type {number}
* @private
*/
Blockly.Flyout.prototype.contentHeight_ = 0;
/**
* Vertical offset of flyout.
* @type {number}
* @private
*/
Blockly.Flyout.prototype.verticalOffset_ = 0;
/**
* Range of a drag angle from a flyout considered "dragging toward workspace".
* Drags that are within the bounds of this many degrees from the orthogonal
* line to the flyout edge are considered to be "drags toward the workspace".
* Example:
* Flyout Edge Workspace
* [block] / <-within this angle, drags "toward workspace" |
* [block] ---- orthogonal to flyout boundary ---- |
* [block] \ |
* The angle is given in degrees from the orthogonal.
*
* This is used to know when to create a new block and when to scroll the
* flyout. Setting it to 360 means that all drags create a new block.
* @type {number}
* @protected
*/
Blockly.Flyout.prototype.dragAngleRange_ = 70;
/**
* The fraction of the distance to the scroll target to move the flyout on
* each animation frame, when auto-scrolling. Values closer to 1.0 will make
* the scroll animation complete faster. Use 1.0 for no animation.
* @type {number}
*/
Blockly.Flyout.prototype.scrollAnimationFraction = 0.3;
/**
* Whether to recycle blocks when refreshing the flyout. When false, do not allow
* anything to be recycled. The default is to recycle.
* @type {boolean}
* @private
*/
Blockly.Flyout.prototype.recyclingEnabled_ = true;
/**
* Creates the flyout's DOM. Only needs to be called once. The flyout can
* either exist as its own svg element or be a g element nested inside a
* separate svg element.
* @param {string} tagName The type of tag to put the flyout in. This
* should be <svg> or <g>.
* @return {!Element} The flyout's SVG group.
*/
Blockly.Flyout.prototype.createDom = function(tagName) {
/*
<svg | g>
<path class="blocklyFlyoutBackground"/>
<g class="blocklyFlyout"></g>
</ svg | g>
*/
// Setting style to display:none to start. The toolbox and flyout
// hide/show code will set up proper visibility and size later.
this.svgGroup_ = Blockly.utils.createSvgElement(tagName,
{'class': 'blocklyFlyout', 'style': 'display: none'}, null);
this.svgBackground_ = Blockly.utils.createSvgElement('path',
{'class': 'blocklyFlyoutBackground'}, this.svgGroup_);
this.svgGroup_.appendChild(this.workspace_.createDom());
return this.svgGroup_;
};
/**
* Initializes the flyout.
* @param {!Blockly.Workspace} targetWorkspace The workspace in which to create
* new blocks.
*/
Blockly.Flyout.prototype.init = function(targetWorkspace) {
this.targetWorkspace_ = targetWorkspace;
this.workspace_.targetWorkspace = targetWorkspace;
// Add scrollbar.
this.scrollbar_ = new Blockly.Scrollbar(this.workspace_,
this.horizontalLayout_, false, 'blocklyFlyoutScrollbar');
this.position();
Array.prototype.push.apply(this.eventWrappers_,
Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this, this.wheel_));
// Dragging the flyout up and down (or left and right).
Array.prototype.push.apply(this.eventWrappers_,
Blockly.bindEventWithChecks_(
this.svgGroup_, 'mousedown', this, this.onMouseDown_));
// A flyout connected to a workspace doesn't have its own current gesture.
this.workspace_.getGesture =
this.targetWorkspace_.getGesture.bind(this.targetWorkspace_);
// Get variables from the main workspace rather than the target workspace.
this.workspace_.variableMap_ = this.targetWorkspace_.getVariableMap();
this.workspace_.createPotentialVariableMap();
};
/**
* Dispose of this flyout.
* Unlink from all DOM elements to prevent memory leaks.
*/
Blockly.Flyout.prototype.dispose = function() {
this.hide();
Blockly.unbindEvent_(this.eventWrappers_);
if (this.scrollbar_) {
this.scrollbar_.dispose();
this.scrollbar_ = null;
}
if (this.workspace_) {
this.workspace_.targetWorkspace = null;
this.workspace_.dispose();
this.workspace_ = null;
}
if (this.svgGroup_) {
goog.dom.removeNode(this.svgGroup_);
this.svgGroup_ = null;
}
this.parentToolbox_ = null;
this.svgBackground_ = null;
this.targetWorkspace_ = null;
};
/**
* Set the parent toolbox of this flyout.
* @param {!Blockly.Toolbox} toolbox The toolbox that owns this flyout.
*/
Blockly.Flyout.prototype.setParentToolbox = function(toolbox) {
this.parentToolbox_ = toolbox;
};
/**
* Get the width of the flyout.
* @return {number} The width of the flyout.
*/
Blockly.Flyout.prototype.getWidth = function() {
return this.DEFAULT_WIDTH;
};
/**
* Get the height of the flyout.
* @return {number} The width of the flyout.
*/
Blockly.Flyout.prototype.getHeight = function() {
return this.height_;
};
/**
* Get the workspace inside the flyout.
* @return {!Blockly.WorkspaceSvg} The workspace inside the flyout.
* @package
*/
Blockly.Flyout.prototype.getWorkspace = function() {
return this.workspace_;
};
/**
* Is the flyout visible?
* @return {boolean} True if visible.
*/
Blockly.Flyout.prototype.isVisible = function() {
return this.isVisible_;
};
/**
* Set whether the flyout is visible. A value of true does not necessarily mean
* that the flyout is shown. It could be hidden because its container is hidden.
* @param {boolean} visible True if visible.
*/
Blockly.Flyout.prototype.setVisible = function(visible) {
var visibilityChanged = (visible != this.isVisible());
this.isVisible_ = visible;
if (visibilityChanged) {
this.updateDisplay_();
}
};
/**
* Set whether this flyout's container is visible.
* @param {boolean} visible Whether the container is visible.
*/
Blockly.Flyout.prototype.setContainerVisible = function(visible) {
var visibilityChanged = (visible != this.containerVisible_);
this.containerVisible_ = visible;
if (visibilityChanged) {
this.updateDisplay_();
}
};
/**
* Update the display property of the flyout based whether it thinks it should
* be visible and whether its containing workspace is visible.
* @private
*/
Blockly.Flyout.prototype.updateDisplay_ = function() {
var show = true;
if (!this.containerVisible_) {
show = false;
} else {
show = this.isVisible();
}
this.svgGroup_.style.display = show ? 'block' : 'none';
// Update the scrollbar's visiblity too since it should mimic the
// flyout's visibility.
this.scrollbar_.setContainerVisible(show);
};
/**
* Hide and empty the flyout.
*/
Blockly.Flyout.prototype.hide = function() {
if (!this.isVisible()) {
return;
}
this.setVisible(false);
// Delete all the event listeners.
for (var x = 0, listen; listen = this.listeners_[x]; x++) {
Blockly.unbindEvent_(listen);
}
this.listeners_.length = 0;
if (this.reflowWrapper_) {
this.workspace_.removeChangeListener(this.reflowWrapper_);
this.reflowWrapper_ = null;
}
// Do NOT delete the blocks here. Wait until Flyout.show.
// https://neil.fraser.name/news/2014/08/09/
};
/**
* Show and populate the flyout.
* @param {!Array|string} xmlList List of blocks to show.
* Variables and procedures have a custom set of blocks.
*/
Blockly.Flyout.prototype.show = function(xmlList) {
this.workspace_.setResizesEnabled(false);
this.hide();
this.clearOldBlocks_();
this.setVisible(true);
// Create the blocks to be shown in this flyout.
var contents = [];
var gaps = [];
this.permanentlyDisabled_.length = 0;
for (var i = 0, xml; xml = xmlList[i]; i++) {
// Handle dynamic categories, represented by a name instead of a list of XML.
// Look up the correct category generation function and call that to get a
// valid XML list.
if (typeof xml === 'string') {
var fnToApply = this.workspace_.targetWorkspace.getToolboxCategoryCallback(
xmlList[i]);
var newList = fnToApply(this.workspace_.targetWorkspace);
// Insert the new list of blocks in the middle of the list.
// We use splice to insert at index i, and remove a single element
// (the placeholder string). Because the spread operator (...) is not
// available, use apply and concat the array.
xmlList.splice.apply(xmlList, [i, 1].concat(newList));
xml = xmlList[i];
}
if (xml.tagName) {
var tagName = xml.tagName.toUpperCase();
var default_gap = this.horizontalLayout_ ? this.GAP_X : this.GAP_Y;
if (tagName == 'BLOCK') {
// We assume that in a flyout, the same block id (or type if missing id) means
// the same output BlockSVG.
// Look for a block that matches the id or type, our createBlock will assign
// id = type if none existed.
var id = xml.getAttribute('id') || xml.getAttribute('type');
var recycled = this.recycleBlocks_.findIndex(function(block) {
return block.id === id;
});
// If we found a recycled item, reuse the BlockSVG from last time.
// Otherwise, convert the XML block to a BlockSVG.
var curBlock;
if (recycled > -1) {
curBlock = this.recycleBlocks_.splice(recycled, 1)[0];
} else {
curBlock = Blockly.Xml.domToBlock(xml, this.workspace_);
}
if (curBlock.disabled) {
// Record blocks that were initially disabled.
// Do not enable these blocks as a result of capacity filtering.
this.permanentlyDisabled_.push(curBlock);
}
contents.push({type: 'block', block: curBlock});
var gap = parseInt(xml.getAttribute('gap'), 10);
gaps.push(isNaN(gap) ? default_gap : gap);
} else if (xml.tagName.toUpperCase() == 'SEP') {
// Change the gap between two blocks.
// <sep gap="36"></sep>
// The default gap is 24, can be set larger or smaller.
// This overwrites the gap attribute on the previous block.
// Note that a deprecated method is to add a gap to a block.
// <block type="math_arithmetic" gap="8"></block>
var newGap = parseInt(xml.getAttribute('gap'), 10);
// Ignore gaps before the first block.
if (!isNaN(newGap) && gaps.length > 0) {
gaps[gaps.length - 1] = newGap;
} else {
gaps.push(default_gap);
}
} else if ((tagName == 'LABEL') && (xml.getAttribute('showStatusButton') == 'true')) {
var curButton = new Blockly.FlyoutExtensionCategoryHeader(this.workspace_,
this.targetWorkspace_, xml);
contents.push({type: 'button', button: curButton});
gaps.push(default_gap);
} else if (tagName == 'BUTTON' || tagName == 'LABEL') {
// Labels behave the same as buttons, but are styled differently.
var isLabel = tagName == 'LABEL';
var curButton = new Blockly.FlyoutButton(this.workspace_,
this.targetWorkspace_, xml, isLabel);
contents.push({type: 'button', button: curButton});
gaps.push(default_gap);
}
}
}
this.emptyRecycleBlocks_();
this.layout_(contents, gaps);
// IE 11 is an incompetent browser that fails to fire mouseout events.
// When the mouse is over the background, deselect all blocks.
var deselectAll = function() {
var topBlocks = this.workspace_.getTopBlocks(false);
for (var i = 0, block; block = topBlocks[i]; i++) {
block.removeSelect();
}
};
this.listeners_.push(Blockly.bindEvent_(this.svgBackground_, 'mouseover',
this, deselectAll));
this.workspace_.setResizesEnabled(true);
this.reflow();
// Correctly position the flyout's scrollbar when it opens.
this.position();
this.reflowWrapper_ = this.reflow.bind(this);
this.workspace_.addChangeListener(this.reflowWrapper_);
this.recordCategoryScrollPositions_();
};
/**
* Empty out the recycled blocks, properly destroying everything.
* @private
*/
Blockly.Flyout.prototype.emptyRecycleBlocks_ = function() {
// Clean out the old recycle bin.
var oldBlocks = this.recycleBlocks_;
this.recycleBlocks_ = [];
for (var i = 0; i < oldBlocks.length; i++) {
oldBlocks[i].dispose(false, false);
}
};
/**
* Store an array of category names, ids, scrollbar positions, and category lengths.
* This is used when scrolling the flyout to cause a category to be selected.
* @private
*/
Blockly.Flyout.prototype.recordCategoryScrollPositions_ = function() {
this.categoryScrollPositions = [];
// Record category names and positions using the text label at the top of each one.
for (var i = 0; i < this.buttons_.length; i++) {
if (this.buttons_[i].getIsCategoryLabel()) {
var categoryLabel = this.buttons_[i];
this.categoryScrollPositions.push({
categoryName: categoryLabel.getText(),
position: this.horizontalLayout_ ?
categoryLabel.getPosition().x : categoryLabel.getPosition().y
});
}
}
// Record the length of each category, setting the final one to 0.
var numCategories = this.categoryScrollPositions.length;
if (numCategories > 0) {
for (var i = 0; i < numCategories - 1; i++) {
var currentPos = this.categoryScrollPositions[i].position;
var nextPos = this.categoryScrollPositions[i + 1].position;
var length = nextPos - currentPos;
this.categoryScrollPositions[i].length = length;
}
this.categoryScrollPositions[numCategories - 1].length = 0;
// Record the id of each category.
for (var i = 0; i < numCategories; i++) {
var category = this.parentToolbox_.getCategoryByIndex(i);
if (category && category.id_) {
this.categoryScrollPositions[i].categoryId = category.id_;
}
}
}
};
/**
* Select a category using the scroll position.
* @param {number} pos The scroll position in pixels.
* @package
*/
Blockly.Flyout.prototype.selectCategoryByScrollPosition = function(pos) {
// If we are currently auto-scrolling, due to selecting a category by clicking on it,
// do not update the category selection.
if (this.scrollTarget) {
return;
}
var workspacePos = Math.round(pos / this.workspace_.scale);
// Traverse the array of scroll positions in reverse, so we can select the furthest
// category that the scroll position is beyond.
for (var i = this.categoryScrollPositions.length - 1; i >= 0; i--) {
if (workspacePos >= this.categoryScrollPositions[i].position) {
this.parentToolbox_.selectCategoryById(this.categoryScrollPositions[i].categoryId);
return;
}
}
};
Blockly.Flyout.prototype.startScrollAnimation = function() {
this.scrollTime = -1;
this.scrollStart = this.horizontalLayout_ ? -this.workspace_.scrollX : -this.workspace_.scrollY;
requestAnimationFrame(this.stepScrollAnimation.bind(this));
};
/**
* Step the scrolling animation by scrolling a fraction of the way to
* a scroll target, and request the next frame if necessary.
* @param {number} time Time in milliseconds
* @package
*/
Blockly.Flyout.prototype.stepScrollAnimation = function(time) {
if (!this.scrollTarget) {
return;
}
if (this.scrollTime === -1) {
this.scrollTime = time;
}
var animationTime = (time - this.scrollTime) / 60 + 1;
var totalDistance = this.scrollTarget - this.scrollStart;
var scrollPos = this.scrollTarget - totalDistance * Math.pow(this.scrollAnimationFraction, animationTime);
var diff = this.scrollTarget - scrollPos;
if (Math.abs(diff) < 1) {
this.scrollbar_.set(this.scrollTarget);
this.scrollTarget = null;
return;
}
this.scrollbar_.set(scrollPos);
// Polyfilled by goog.dom.animationFrame.polyfill
requestAnimationFrame(this.stepScrollAnimation.bind(this));
};
/**
* Get the scaled scroll position.
* @return {number} The current scroll position.
*/
Blockly.Flyout.prototype.getScrollPos = function() {
var pos = this.horizontalLayout_ ?
-this.workspace_.scrollX : -this.workspace_.scrollY;
return pos / this.workspace_.scale;
};
/**
* Set the scroll position, scaling it.
* @param {number} pos The scroll position to set.
*/
Blockly.Flyout.prototype.setScrollPos = function(pos) {
this.scrollbar_.set(pos * this.workspace_.scale);
};
/**
* Set whether the flyout can recycle blocks. A value of true allows blocks to be recycled.
* @param {boolean} recycle True if recycling is possible.
*/
Blockly.Flyout.prototype.setRecyclingEnabled = function(recycle) {
this.recyclingEnabled_ = recycle;
};
/**
* Delete blocks and background buttons from a previous showing of the flyout.
* @private
*/
Blockly.Flyout.prototype.clearOldBlocks_ = function() {
// Delete any blocks from a previous showing.
var oldBlocks = this.workspace_.getTopBlocks(false);
for (var i = 0, block; block = oldBlocks[i]; i++) {
if (block.workspace == this.workspace_) {
if (this.recyclingEnabled_ &&
Blockly.scratchBlocksUtils.blockIsRecyclable(block)) {
this.recycleBlock_(block);
} else {
block.dispose(false, false);
}
}
}
// Delete any background buttons from a previous showing.
for (var j = 0; j < this.backgroundButtons_.length; j++) {
var rect = this.backgroundButtons_[j];
if (rect) goog.dom.removeNode(rect);
}
this.backgroundButtons_.length = 0;
for (var i = 0, button; button = this.buttons_[i]; i++) {
button.dispose();
}
this.buttons_.length = 0;
// Clear potential variables from the previous showing.
this.workspace_.getPotentialVariableMap().clear();
};
/**
* Add listeners to a block that has been added to the flyout.
* @param {!Element} root The root node of the SVG group the block is in.
* @param {!Blockly.Block} block The block to add listeners for.
* @param {!Element} rect The invisible rectangle under the block that acts as
* a button for that block.
* @private
*/
Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) {
this.listeners_.push(Blockly.bindEventWithChecks_(root, 'mousedown', null,
this.blockMouseDown_(block)));
this.listeners_.push(Blockly.bindEventWithChecks_(rect, 'mousedown', null,
this.blockMouseDown_(block)));
this.listeners_.push(Blockly.bindEvent_(root, 'mouseover', block,
block.addSelect));
this.listeners_.push(Blockly.bindEvent_(root, 'mouseout', block,
block.removeSelect));
this.listeners_.push(Blockly.bindEvent_(rect, 'mouseover', block,
block.addSelect));
this.listeners_.push(Blockly.bindEvent_(rect, 'mouseout', block,
block.removeSelect));
};
/**
* Handle a mouse-down on an SVG block in a non-closing flyout.
* @param {!Blockly.Block} block The flyout block to copy.
* @return {!Function} Function to call when block is clicked.
* @private
*/
Blockly.Flyout.prototype.blockMouseDown_ = function(block) {
var flyout = this;
return function(e) {
var gesture = flyout.targetWorkspace_.getGesture(e);
if (gesture) {
gesture.setStartBlock(block);
gesture.handleFlyoutStart(e, flyout);
}
};
};
/**
* Mouse down on the flyout background. Start a scroll drag.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Flyout.prototype.onMouseDown_ = function(e) {
var gesture = this.targetWorkspace_.getGesture(e);
if (gesture) {
gesture.handleFlyoutStart(e, this);
}
this.scrollTarget = null;
};
/**
* Create a copy of this block on the workspace.
* @param {!Blockly.BlockSvg} originalBlock The block to copy from the flyout.
* @return {Blockly.BlockSvg} The newly created block, or null if something
* went wrong with deserialization.
* @package
*/
Blockly.Flyout.prototype.createBlock = function(originalBlock) {
var newBlock = null;
Blockly.Events.disable();
var variablesBeforeCreation = this.targetWorkspace_.getAllVariables();
this.targetWorkspace_.setResizesEnabled(false);
try {
newBlock = this.placeNewBlock_(originalBlock);
// Close the flyout.
Blockly.hideChaff();
} finally {
Blockly.Events.enable();
}
var newVariables = Blockly.Variables.getAddedVariables(this.targetWorkspace_,
variablesBeforeCreation);
if (Blockly.Events.isEnabled()) {
Blockly.Events.setGroup(true);
Blockly.Events.fire(new Blockly.Events.Create(newBlock));
// Fire a VarCreate event for each (if any) new variable created.
for (var i = 0; i < newVariables.length; i++) {
var thisVariable = newVariables[i];
Blockly.Events.fire(new Blockly.Events.VarCreate(thisVariable));
}
}
if (this.autoClose) {
this.hide();
}
return newBlock;
};
/**
* Reflow blocks and their buttons.
*/
Blockly.Flyout.prototype.reflow = function() {
if (this.reflowWrapper_) {
this.workspace_.removeChangeListener(this.reflowWrapper_);
}
var blocks = this.workspace_.getTopBlocks(false);
this.reflowInternal_(blocks);
if (this.reflowWrapper_) {
this.workspace_.addChangeListener(this.reflowWrapper_);
}
};
/**
* @return {boolean} True if this flyout may be scrolled with a scrollbar or by
* dragging.
* @package
*/
Blockly.Flyout.prototype.isScrollable = function() {
return this.scrollbar_ ? this.scrollbar_.isVisible() : false;
};
/**
* Copy a block from the flyout to the workspace and position it correctly.
* @param {!Blockly.Block} oldBlock The flyout block to copy.
* @return {!Blockly.Block} The new block in the main workspace.
* @private
*/
Blockly.Flyout.prototype.placeNewBlock_ = function(oldBlock) {
var targetWorkspace = this.targetWorkspace_;
var svgRootOld = oldBlock.getSvgRoot();
if (!svgRootOld) {
throw 'oldBlock is not rendered.';
}
// Create the new block by cloning the block in the flyout (via XML).
var xml = Blockly.Xml.blockToDom(oldBlock);
// The target workspace would normally resize during domToBlock, which will
// lead to weird jumps. Save it for terminateDrag.
targetWorkspace.setResizesEnabled(false);
// Using domToBlock instead of domToWorkspace means that the new block will be
// placed at position (0, 0) in main workspace units.
var block = Blockly.Xml.domToBlock(xml, targetWorkspace);
var svgRootNew = block.getSvgRoot();
if (!svgRootNew) {
throw 'block is not rendered.';
}
// The offset in pixels between the main workspace's origin and the upper left
// corner of the injection div.
var mainOffsetPixels = targetWorkspace.getOriginOffsetInPixels();
// The offset in pixels between the flyout workspace's origin and the upper
// left corner of the injection div.
var flyoutOffsetPixels = this.workspace_.getOriginOffsetInPixels();
// The position of the old block in flyout workspace coordinates.
var oldBlockPosWs = oldBlock.getRelativeToSurfaceXY();
// The position of the old block in pixels relative to the flyout
// workspace's origin.
var oldBlockPosPixels = oldBlockPosWs.scale(this.workspace_.scale);
// The position of the old block in pixels relative to the upper left corner
// of the injection div.
var oldBlockOffsetPixels = goog.math.Coordinate.sum(flyoutOffsetPixels,
oldBlockPosPixels);
// The position of the old block in pixels relative to the origin of the
// main workspace.
var finalOffsetPixels = goog.math.Coordinate.difference(oldBlockOffsetPixels,
mainOffsetPixels);
// The position of the old block in main workspace coordinates.
var finalOffsetMainWs = finalOffsetPixels.scale(1 / targetWorkspace.scale);
block.moveBy(finalOffsetMainWs.x, finalOffsetMainWs.y);
return block;
};
/**
* Put a previously created block into the recycle bin, used during large
* workspace swaps to limit the number of new dom elements we need to create
*
* @param {!Blockly.BlockSvg} block The block to recycle.
* @private
*/
Blockly.Flyout.prototype.recycleBlock_ = function(block) {
var xy = block.getRelativeToSurfaceXY();
block.moveBy(-xy.x, -xy.y);
this.recycleBlocks_.push(block);
};

View File

@@ -0,0 +1,324 @@
/**
* @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 Class for a button in the flyout.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.FlyoutButton');
goog.require('goog.dom');
goog.require('goog.math.Coordinate');
/**
* Class for a button or label in the flyout. Labels behave the same as buttons,
* but are styled differently.
* @param {!Blockly.WorkspaceSvg} workspace The workspace in which to place this
* button.
* @param {!Blockly.WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {!Element} xml The XML specifying the label/button.
* @param {boolean} isLabel Whether this button should be styled as a label.
* @constructor
*/
Blockly.FlyoutButton = function(workspace, targetWorkspace, xml, isLabel) {
this.init(workspace, targetWorkspace, xml, isLabel);
/**
* Function to call when this button is clicked.
* @type {function(!Blockly.FlyoutButton)}
* @private
*/
this.callback_ = null;
var callbackKey = xml.getAttribute('callbackKey');
if (this.isLabel_ && callbackKey) {
console.warn('Labels should not have callbacks. Label text: ' + this.text_);
} else if (!this.isLabel_ &&
!(callbackKey && targetWorkspace.getButtonCallback(callbackKey))) {
console.warn('Buttons should have callbacks. Button text: ' + this.text_);
} else {
this.callback_ = targetWorkspace.getButtonCallback(callbackKey);
}
this.callbackData_ = xml.getAttribute('callbackData');
};
/**
* The margin around the text in the button.
*/
Blockly.FlyoutButton.MARGIN = 40;
/**
* The width of the button's rect.
* @type {number}
*/
Blockly.FlyoutButton.prototype.width = 0;
/**
* The height of the button's rect.
* @type {number}
*/
Blockly.FlyoutButton.prototype.height = 40; // Can't be computed like the width
/**
* Opaque data that can be passed to Blockly.unbindEvent_.
* @type {Array.<!Array>}
* @private
*/
Blockly.FlyoutButton.prototype.onMouseUpWrapper_ = null;
/**
* Initialize the button or label. This is a helper function to so that the
* constructor can be overridden.
* @param {!Blockly.WorkspaceSvg} workspace The workspace in which to place this
* button.
* @param {!Blockly.WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {!Element} xml The XML specifying the label/button.
* @param {boolean} isLabel Whether this button should be styled as a label.
*/
Blockly.FlyoutButton.prototype.init = function(
workspace, targetWorkspace, xml, isLabel) {
/**
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* @type {!Blockly.Workspace}
* @private
*/
this.targetWorkspace_ = targetWorkspace;
/**
* @type {string}
* @private
*/
this.text_ = xml.getAttribute('text');
/**
* @type {!goog.math.Coordinate}
* @private
*/
this.position_ = new goog.math.Coordinate(0, 0);
/**
* Whether this button should be styled as a label.
* @type {boolean}
* @private
*/
this.isLabel_ = isLabel;
/**
* Whether this button is a label at the top of a category.
* @type {boolean}
* @private
*/
this.isCategoryLabel_ = xml.getAttribute('category-label') === 'true';
/**
* If specified, a CSS class to add to this button.
* @type {?string}
* @private
*/
this.cssClass_ = xml.getAttribute('web-class') || null;
};
/**
* Create the button elements.
* @return {!Element} The button's SVG group.
*/
Blockly.FlyoutButton.prototype.createDom = function() {
var cssClass = this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton';
if (this.cssClass_) {
cssClass += ' ' + this.cssClass_;
}
this.svgGroup_ = Blockly.utils.createSvgElement('g', {'class': cssClass},
this.workspace_.getCanvas());
this.addTextSvg(this.isLabel_);
this.mouseUpWrapper_ = Blockly.bindEventWithChecks_(this.svgGroup_, 'mouseup',
this, this.onMouseUp_);
return this.svgGroup_;
};
/**
* Add the text element for the label or button.
* @param {boolean} isLabel True if this is a label and not button.
* @package
*/
Blockly.FlyoutButton.prototype.addTextSvg = function(isLabel) {
if (!isLabel) {
// Shadow rectangle (light source does not mirror in RTL).
var shadow = Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyFlyoutButtonShadow',
'rx': 4,
'ry': 4,
'x': 1,
'y': 1
},
this.svgGroup_);
}
// Background rectangle.
var rect = Blockly.utils.createSvgElement('rect',
{
'class': isLabel ?
'blocklyFlyoutLabelBackground' : 'blocklyFlyoutButtonBackground',
'rx': 4, 'ry': 4
},
this.svgGroup_);
var svgText = Blockly.utils.createSvgElement('text',
{
'class': isLabel ? 'blocklyFlyoutLabelText' : 'blocklyText',
'x': 0,
'y': 0,
'text-anchor': 'middle'
},
this.svgGroup_);
svgText.textContent = Blockly.utils.replaceMessageReferences(this.text_);
this.width = Blockly.Field.getCachedWidth(svgText);
if (!isLabel) {
this.width += 2 * Blockly.FlyoutButton.MARGIN;
shadow.setAttribute('width', this.width);
shadow.setAttribute('height', this.height);
}
rect.setAttribute('width', this.width);
rect.setAttribute('height', this.height);
svgText.setAttribute('text-anchor', 'middle');
svgText.setAttribute('dominant-baseline', 'central');
svgText.setAttribute('dy', goog.userAgent.EDGE_OR_IE ?
Blockly.Field.IE_TEXT_OFFSET : '0');
svgText.setAttribute('x', this.width / 2);
svgText.setAttribute('y', this.height / 2);
};
/**
* Correctly position the flyout button and make it visible.
*/
Blockly.FlyoutButton.prototype.show = function() {
this.updateTransform_();
this.svgGroup_.setAttribute('display', 'block');
};
/**
* Update SVG attributes to match internal state.
* @private
*/
Blockly.FlyoutButton.prototype.updateTransform_ = function() {
this.svgGroup_.setAttribute('transform',
'translate(' + this.position_.x + ',' + this.position_.y + ')');
};
/**
* Move the button to the given x, y coordinates.
* @param {number} x The new x coordinate.
* @param {number} y The new y coordinate.
*/
Blockly.FlyoutButton.prototype.moveTo = function(x, y) {
this.position_.x = x;
this.position_.y = y;
this.updateTransform_();
};
/**
* Get the button's target workspace.
* @return {!Blockly.WorkspaceSvg} The target workspace of the flyout where this
* button resides.
*/
Blockly.FlyoutButton.prototype.getTargetWorkspace = function() {
return this.targetWorkspace_;
};
/**
* Get whether this button is a label at the top of a category.
* @return {boolean} True if it is a category label.
* @package
*/
Blockly.FlyoutButton.prototype.getIsCategoryLabel = function() {
return this.isCategoryLabel_;
};
/**
* Get the text of this button.
* @return {string} The text on the button.
* @package
*/
Blockly.FlyoutButton.prototype.getText = function() {
return this.text_;
};
/**
* Get the position of this button.
* @return {!goog.math.Coordinate} The button position.
* @package
*/
Blockly.FlyoutButton.prototype.getPosition = function() {
return this.position_;
};
/**
* Dispose of this button.
*/
Blockly.FlyoutButton.prototype.dispose = function() {
if (this.onMouseUpWrapper_) {
Blockly.unbindEvent_(this.onMouseUpWrapper_);
}
if (this.svgGroup_) {
goog.dom.removeNode(this.svgGroup_);
this.svgGroup_ = null;
}
this.workspace_ = null;
this.targetWorkspace_ = null;
};
/**
* Do something when the button is clicked.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.FlyoutButton.prototype.onMouseUp_ = function(e) {
var gesture = this.targetWorkspace_.getGesture(e);
if (gesture) {
// If we're in the middle of dragging something (blocks, workspace, etc.) ignore the button.
// Otherwise, cancel the gesture.
if (gesture.isDragging()) {
return;
}
gesture.cancel();
}
// Call the callback registered to this button.
if (this.callback_) {
this.callback_(this);
}
};

View File

@@ -0,0 +1,83 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Methods for dragging a flyout visually.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.FlyoutDragger');
goog.require('Blockly.WorkspaceDragger');
goog.require('goog.asserts');
goog.require('goog.math.Coordinate');
/**
* Class for a flyout dragger. It moves a flyout 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.
* @param {!Blockly.Flyout} flyout The flyout to drag.
* @constructor
*/
Blockly.FlyoutDragger = function(flyout) {
Blockly.FlyoutDragger.superClass_.constructor.call(this,
flyout.getWorkspace());
/**
* The scrollbar to update to move the flyout.
* Unlike the main workspace, the flyout has only one scrollbar, in either the
* horizontal or the vertical direction.
* @type {!Blockly.Scrollbar}
* @private
*/
this.scrollbar_ = flyout.scrollbar_;
/**
* Whether the flyout scrolls horizontally. If false, the flyout scrolls
* vertically.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = flyout.horizontalLayout_;
};
goog.inherits(Blockly.FlyoutDragger, Blockly.WorkspaceDragger);
/**
* Move the appropriate scrollbar to drag the flyout.
* Since flyouts only scroll in one direction at a time, this will discard one
* of the calculated values.
* x and y are in pixels.
* @param {number} x The new x position to move the scrollbar to.
* @param {number} y The new y position to move the scrollbar to.
* @private
*/
Blockly.FlyoutDragger.prototype.updateScroll_ = function(x, y) {
// Move the scrollbar and the flyout will scroll automatically.
if (this.horizontalLayout_) {
this.scrollbar_.set(x);
} else {
this.scrollbar_.set(y);
}
};

View File

@@ -0,0 +1,159 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Class for a category header in the flyout for Scratch
* extensions which can display a textual label and a status button.
* @author ericr@media.mit.edu (Eric Rosenbaum)
*/
'use strict';
goog.provide('Blockly.FlyoutExtensionCategoryHeader');
goog.require('Blockly.FlyoutButton');
/**
* Class for a category header in the flyout for Scratch extensions which can
* display a textual label and a status button.
* @param {!Blockly.WorkspaceSvg} workspace The workspace in which to place this
* header.
* @param {!Blockly.WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {!Element} xml The XML specifying the header.
* @extends {Blockly.FlyoutButton}
* @constructor
*/
Blockly.FlyoutExtensionCategoryHeader = function(workspace, targetWorkspace, xml) {
this.init(workspace, targetWorkspace, xml, false);
/**
* @type {number}
* @private
*/
this.flyoutWidth_ = this.targetWorkspace_.getFlyout().getWidth();
/**
* @type {string}
*/
this.extensionId = xml.getAttribute('id');
/**
* Whether this is a label at the top of a category.
* @type {boolean}
* @private
*/
this.isCategoryLabel_ = true;
};
goog.inherits(Blockly.FlyoutExtensionCategoryHeader, Blockly.FlyoutButton);
/**
* Create the label and button elements.
* @return {!Element} The SVG group.
*/
Blockly.FlyoutExtensionCategoryHeader.prototype.createDom = function() {
var cssClass = 'blocklyFlyoutLabel';
this.svgGroup_ = Blockly.utils.createSvgElement('g', {'class': cssClass},
this.workspace_.getCanvas());
this.addTextSvg(true);
this.refreshStatus();
var statusButtonWidth = 30;
var marginX = 20;
var marginY = 5;
var touchPadding = 16;
var statusButtonX = this.workspace_.RTL ? (marginX - this.flyoutWidth_ + statusButtonWidth) :
(this.flyoutWidth_ - statusButtonWidth - marginX) / this.workspace_.scale;
if (this.imageSrc_) {
/** @type {SVGElement} */
this.imageElement_ = Blockly.utils.createSvgElement(
'image',
{
'class': 'blocklyFlyoutButton',
'height': statusButtonWidth + 'px',
'width': statusButtonWidth + 'px',
'x': statusButtonX + 'px',
'y': marginY + 'px'
},
this.svgGroup_);
this.imageElementBackground_ = Blockly.utils.createSvgElement(
'rect',
{
'class': 'blocklyTouchTargetBackground',
'height': statusButtonWidth + 2 * touchPadding + 'px',
'width': statusButtonWidth + 2 * touchPadding + 'px',
'x': (statusButtonX - touchPadding) + 'px',
'y': (marginY - touchPadding) + 'px'
},
this.svgGroup_);
this.setImageSrc(this.imageSrc_);
}
this.callback_ = Blockly.statusButtonCallback.bind(this, this.extensionId);
this.mouseUpWrapper_ = Blockly.bindEventWithChecks_(this.imageElementBackground_, 'mouseup',
this, this.onMouseUp_);
return this.svgGroup_;
};
/**
* Set the image on the status button using a status string.
*/
Blockly.FlyoutExtensionCategoryHeader.prototype.refreshStatus = function() {
var status = Blockly.FlyoutExtensionCategoryHeader.getExtensionState(this.extensionId);
var basePath = Blockly.mainWorkspace.options.pathToMedia;
if (status == Blockly.StatusButtonState.READY) {
this.setImageSrc(basePath + 'status-ready.svg');
}
if (status == Blockly.StatusButtonState.NOT_READY) {
this.setImageSrc(basePath + 'status-not-ready.svg');
}
};
/**
* Set the source URL of the image for the button.
* @param {?string} src New source.
* @package
*/
Blockly.FlyoutExtensionCategoryHeader.prototype.setImageSrc = function(src) {
if (src === null) {
// No change if null.
return;
}
this.imageSrc_ = src;
if (this.imageElement_) {
this.imageElement_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', this.imageSrc_ || '');
}
};
/**
* Gets the extension state. Overridden externally.
* @param {string} extensionId The ID of the extension in question.
* @return {Blockly.StatusButtonState} The state of the extension.
* @public
*/
Blockly.FlyoutExtensionCategoryHeader.getExtensionState = function(/* extensionId */) {
return Blockly.StatusButtonState.NOT_READY;
};

View File

@@ -0,0 +1,475 @@
/**
* @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 Flyout tray containing blocks which may be created.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.HorizontalFlyout');
goog.require('Blockly.Block');
goog.require('Blockly.Comment');
goog.require('Blockly.Events');
goog.require('Blockly.FlyoutButton');
goog.require('Blockly.Flyout');
goog.require('Blockly.WorkspaceSvg');
goog.require('goog.dom');
goog.require('goog.dom.animationFrame.polyfill');
goog.require('goog.events');
goog.require('goog.math.Rect');
goog.require('goog.userAgent');
/**
* Class for a flyout.
* @param {!Object} workspaceOptions Dictionary of options for the workspace.
* @extends {Blockly.Flyout}
* @constructor
*/
Blockly.HorizontalFlyout = function(workspaceOptions) {
workspaceOptions.getMetrics = this.getMetrics_.bind(this);
workspaceOptions.setMetrics = this.setMetrics_.bind(this);
Blockly.HorizontalFlyout.superClass_.constructor.call(this, workspaceOptions);
/**
* Flyout should be laid out horizontally vs vertically.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = true;
};
goog.inherits(Blockly.HorizontalFlyout, Blockly.Flyout);
/**
* Return an object with all the metrics required to size scrollbars for the
* flyout. The following properties are computed:
* .viewHeight: Height of the visible rectangle,
* .viewWidth: Width of the visible rectangle,
* .contentHeight: Height of the contents,
* .contentWidth: Width of the contents,
* .viewTop: Offset of top edge of visible rectangle from parent,
* .contentTop: Offset of the top-most content from the y=0 coordinate,
* .absoluteTop: Top-edge of view.
* .viewLeft: Offset of the left edge of visible rectangle from parent,
* .contentLeft: Offset of the left-most content from the x=0 coordinate,
* .absoluteLeft: Left-edge of view.
* @return {Object} Contains size and position metrics of the flyout.
* @private
*/
Blockly.HorizontalFlyout.prototype.getMetrics_ = function() {
if (!this.isVisible()) {
// Flyout is hidden.
return null;
}
try {
var optionBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
var optionBox = {height: 0, y: 0, width: 0, x: 0};
}
var absoluteTop = this.SCROLLBAR_PADDING;
var absoluteLeft = this.SCROLLBAR_PADDING;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
absoluteTop = 0;
}
var viewHeight = this.height_;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
viewHeight += this.MARGIN;
}
var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING;
var metrics = {
viewHeight: viewHeight,
viewWidth: viewWidth,
contentHeight: optionBox.height * this.workspace_.scale + 2 * this.MARGIN,
contentWidth: optionBox.width * this.workspace_.scale + 2 * this.MARGIN,
viewTop: -this.workspace_.scrollY,
viewLeft: -this.workspace_.scrollX,
contentTop: optionBox.y,
contentLeft: optionBox.x,
absoluteTop: absoluteTop,
absoluteLeft: absoluteLeft
};
return metrics;
};
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!Object} xyRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling and a
* similar x property.
* @private
*/
Blockly.HorizontalFlyout.prototype.setMetrics_ = function(xyRatio) {
var metrics = this.getMetrics_();
// This is a fix to an apparent race condition.
if (!metrics) {
return;
}
if (goog.isNumber(xyRatio.x)) {
this.workspace_.scrollX = -metrics.contentWidth * xyRatio.x;
}
this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft,
this.workspace_.scrollY + metrics.absoluteTop);
if (this.categoryScrollPositions) {
this.selectCategoryByScrollPosition(-this.workspace_.scrollX);
}
};
/**
* Move the flyout to the edge of the workspace.
*/
Blockly.HorizontalFlyout.prototype.position = function() {
if (!this.isVisible()) {
return;
}
var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics();
if (!targetWorkspaceMetrics) {
// Hidden components will return null.
return;
}
var edgeWidth = this.horizontalLayout_ ?
targetWorkspaceMetrics.viewWidth - 2 * this.CORNER_RADIUS :
this.width_ - this.CORNER_RADIUS;
var edgeHeight = this.horizontalLayout_ ?
this.height_ - this.CORNER_RADIUS :
targetWorkspaceMetrics.viewHeight - 2 * this.CORNER_RADIUS;
this.setBackgroundPath_(edgeWidth, edgeHeight);
var x = targetWorkspaceMetrics.absoluteLeft;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
x += targetWorkspaceMetrics.viewWidth;
x -= this.width_;
}
var y = targetWorkspaceMetrics.absoluteTop;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
y += targetWorkspaceMetrics.viewHeight;
y -= this.height_;
}
// Record the height for Blockly.Flyout.getMetrics_, or width if the layout is
// horizontal.
if (this.horizontalLayout_) {
this.width_ = targetWorkspaceMetrics.viewWidth;
} else {
this.height_ = targetWorkspaceMetrics.viewHeight;
}
this.svgGroup_.setAttribute("width", this.width_);
this.svgGroup_.setAttribute("height", this.height_);
var transform = 'translate(' + x + 'px,' + y + 'px)';
Blockly.utils.setCssTransform(this.svgGroup_, transform);
// Update the scrollbar (if one exists).
if (this.scrollbar_) {
// Set the scrollbars origin to be the top left of the flyout.
this.scrollbar_.setOrigin(x, y);
this.scrollbar_.resize();
}
// The blocks need to be visible in order to be laid out and measured correctly, but we don't
// want the flyout to show up until it's properly sized.
// Opacity is set to zero in show().
this.svgGroup_.style.opacity = 1;
};
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
Blockly.HorizontalFlyout.prototype.setBackgroundPath_ = function(width, height) {
var atTop = this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP;
// Start at top left.
var path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
if (atTop) {
// Top.
path.push('h', width + 2 * this.CORNER_RADIUS);
// Right.
path.push('v', height);
// Bottom.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('h', -1 * width);
// Left.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('z');
} else {
// Top.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('h', width);
// Right.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('v', height);
// Bottom.
path.push('h', -width - 2 * this.CORNER_RADIUS);
// Left.
path.push('z');
}
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Scroll the flyout to the top.
*/
Blockly.HorizontalFlyout.prototype.scrollToStart = function() {
this.scrollbar_.set(this.RTL ? Infinity : 0);
};
/**
* Scroll the flyout to a position.
* @param {number} pos The targeted scroll position.
* @package
*/
Blockly.HorizontalFlyout.prototype.scrollTo = function(pos) {
this.scrollTarget = pos * this.workspace_.scale;
// Make sure not to set the scroll target past the farthest point we can
// scroll to, i.e. the content width minus the view width
var metrics = this.workspace_.getMetrics();
var contentWidth = metrics.contentWidth;
var viewWidth = metrics.viewWidth;
this.scrollTarget = Math.min(this.scrollTarget, contentWidth - viewWidth);
this.startScrollAnimation();
};
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @private
*/
Blockly.HorizontalFlyout.prototype.wheel_ = function(e) {
// remove scrollTarget to stop auto scrolling in stepScrollAnimation
this.scrollTarget = null;
var delta = e.deltaX;
// If we're scrolling more vertically than horizontally, use the vertical
// scroll delta instead. This allows people using a mouse wheel (which can
// only scroll vertically) to scroll the horizontal flyout. It also allows
// trackpad users to scroll it by scrolling either horizontally or
// vertically.
if (Math.abs(e.deltaY) > Math.abs(delta)) {
delta = e.deltaY;
}
if (delta) {
// Firefox's mouse wheel deltas are a tenth that of Chrome/Safari.
// DeltaMode is 1 for a mouse wheel, but not for a trackpad scroll event
if (goog.userAgent.GECKO && (e.deltaMode === 1)) {
delta *= 10;
}
var metrics = this.getMetrics_();
var pos = metrics.viewLeft + delta;
var limit = metrics.contentWidth - metrics.viewWidth;
pos = Math.min(pos, limit);
pos = Math.max(pos, 0);
this.scrollbar_.set(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and DropDownDiv.
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
};
/**
* Lay out the blocks in the flyout.
* @param {!Array.<!Object>} contents The blocks and buttons to lay out.
* @param {!Array.<number>} gaps The visible gaps between blocks.
* @private
*/
Blockly.HorizontalFlyout.prototype.layout_ = function(contents, gaps) {
this.workspace_.scale = this.targetWorkspace_.scale;
var margin = this.MARGIN;
var cursorX = margin;
var cursorY = margin;
if (this.RTL) {
contents = contents.reverse();
}
for (var i = 0, item; item = contents[i]; i++) {
if (item.type == 'block') {
var block = item.block;
var allBlocks = block.getDescendants(false);
for (var j = 0, child; child = allBlocks[j]; j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such a
// block.
child.isInFlyout = true;
}
var root = block.getSvgRoot();
var blockHW = block.getHeightWidth();
var moveX = cursorX;
if (this.RTL) {
moveX += blockHW.width;
}
block.moveBy(moveX, cursorY);
cursorX += blockHW.width + gaps[i];
// Create an invisible rectangle under the block to act as a button. Just
// using the block as a button is poor, since blocks have holes in them.
var rect = Blockly.utils.createSvgElement('rect', {'fill-opacity': 0}, null);
rect.tooltip = block;
Blockly.Tooltip.bindMouseEvents(rect);
// Add the rectangles under the blocks, so that the blocks' tooltips work.
this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot());
block.flyoutRect_ = rect;
this.backgroundButtons_[i] = rect;
this.addBlockListeners_(root, block, rect);
} else if (item.type == 'button') {
var button = item.button;
var buttonSvg = button.createDom();
button.moveTo(cursorX, cursorY);
button.show();
// Clicking on a flyout button or label is a lot like clicking on the
// flyout background.
this.listeners_.push(Blockly.bindEventWithChecks_(buttonSvg, 'mousedown',
this, this.onMouseDown_));
this.buttons_.push(button);
cursorX += (button.width + gaps[i]);
}
}
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This to decide if a new block should be
* created or if the flyout should scroll.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} true if the drag is toward the workspace.
* @package
*/
Blockly.HorizontalFlyout.prototype.isDragTowardWorkspace = function(currentDragDeltaXY) {
var dx = currentDragDeltaXY.x;
var dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
var dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
var draggingTowardWorkspace = false;
var range = this.dragAngleRange_;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
// Horizontal at top.
if (dragDirection < 90 + range && dragDirection > 90 - range) {
draggingTowardWorkspace = true;
}
} else {
// Horizontal at bottom.
if (dragDirection > -90 - range && dragDirection < -90 + range) {
draggingTowardWorkspace = true;
}
}
return draggingTowardWorkspace;
};
/**
* Return the deletion rectangle for this flyout in viewport coordinates.
* @return {goog.math.Rect} Rectangle in which to delete.
*/
Blockly.HorizontalFlyout.prototype.getClientRect = function() {
if (!this.svgGroup_) {
return null;
}
var flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout
// area are still deleted. Must be larger than the largest screen size,
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
var BIG_NUM = 1000000000;
var y = flyoutRect.top;
var height = flyoutRect.height;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
return new goog.math.Rect(-BIG_NUM, y - BIG_NUM, BIG_NUM * 2,
BIG_NUM + height);
} else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
return new goog.math.Rect(-BIG_NUM, y, BIG_NUM * 2,
BIG_NUM + height);
}
};
/**
* Compute height of flyout. Position button under each block.
* For RTL: Lay out the blocks right-aligned.
* @param {!Array<!Blockly.Block>} blocks The blocks to reflow.
*/
Blockly.HorizontalFlyout.prototype.reflowInternal_ = function(blocks) {
this.workspace_.scale = this.targetWorkspace_.scale;
var flyoutHeight = 0;
for (var i = 0, block; block = blocks[i]; i++) {
flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height);
}
flyoutHeight += this.MARGIN * 1.5;
flyoutHeight *= this.workspace_.scale;
flyoutHeight += Blockly.Scrollbar.scrollbarThickness;
if (this.height_ != flyoutHeight) {
for (var i = 0, block; block = blocks[i]; i++) {
var blockHW = block.getHeightWidth();
if (block.flyoutRect_) {
block.flyoutRect_.setAttribute('width', blockHW.width);
block.flyoutRect_.setAttribute('height', blockHW.height);
// Rectangles behind blocks with output tabs are shifted a bit.
var blockXY = block.getRelativeToSurfaceXY();
block.flyoutRect_.setAttribute('y', blockXY.y);
block.flyoutRect_.setAttribute('x',
this.RTL ? blockXY.x - blockHW.width : blockXY.x);
// For hat blocks we want to shift them down by the hat height
// since the y coordinate is the corner, not the top of the hat.
var hatOffset =
block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0;
if (hatOffset) {
block.moveBy(0, hatOffset);
}
block.flyoutRect_.setAttribute('y', blockXY.y);
}
}
// Record the height for .getMetrics_ and .position.
this.height_ = flyoutHeight;
// Call this since it is possible the trash and zoom buttons need
// to move. e.g. on a bottom positioned flyout when zoom is clicked.
this.targetWorkspace_.resize();
}
};

View File

@@ -0,0 +1,770 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Layout code for a vertical variant of the flyout.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.VerticalFlyout');
goog.require('Blockly.Block');
goog.require('Blockly.Comment');
goog.require('Blockly.Events');
goog.require('Blockly.Flyout');
goog.require('Blockly.FlyoutButton');
goog.require('Blockly.utils');
goog.require('Blockly.WorkspaceSvg');
goog.require('goog.dom');
goog.require('goog.dom.animationFrame.polyfill');
goog.require('goog.events');
goog.require('goog.math.Rect');
goog.require('goog.userAgent');
/**
* Class for a flyout.
* @param {!Object} workspaceOptions Dictionary of options for the workspace.
* @extends {Blockly.Flyout}
* @constructor
*/
Blockly.VerticalFlyout = function(workspaceOptions) {
workspaceOptions.getMetrics = this.getMetrics_.bind(this);
workspaceOptions.setMetrics = this.setMetrics_.bind(this);
Blockly.VerticalFlyout.superClass_.constructor.call(this, workspaceOptions);
/**
* Flyout should be laid out vertically.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = false;
/**
* Map of checkboxes that correspond to monitored blocks.
* Each element is an object containing the SVG for the checkbox, a boolean
* for its checked state, and the block the checkbox is associated with.
* @type {!Object.<string, !Object>}
* @private
*/
this.checkboxes_ = {};
};
goog.inherits(Blockly.VerticalFlyout, Blockly.Flyout);
/**
* Does the flyout automatically close when a block is created?
* @type {boolean}
*/
Blockly.VerticalFlyout.prototype.autoClose = false;
/**
* The width of the flyout, if not otherwise specified.
* @type {number}
*/
Blockly.VerticalFlyout.prototype.DEFAULT_WIDTH = 250;
/**
* Size of a checkbox next to a variable reporter.
* @type {number}
* @const
*/
Blockly.VerticalFlyout.prototype.CHECKBOX_SIZE = 25;
/**
* Amount of touchable padding around reporter checkboxes.
* @type {number}
* @const
*/
Blockly.VerticalFlyout.prototype.CHECKBOX_TOUCH_PADDING = 12;
/**
* SVG path data for checkmark in checkbox.
* @type {string}
* @const
*/
Blockly.VerticalFlyout.prototype.CHECKMARK_PATH =
'M' + Blockly.VerticalFlyout.prototype.CHECKBOX_SIZE / 4 +
' ' + Blockly.VerticalFlyout.prototype.CHECKBOX_SIZE / 2 +
'L' + 5 * Blockly.VerticalFlyout.prototype.CHECKBOX_SIZE / 12 +
' ' + 2 * Blockly.VerticalFlyout.prototype.CHECKBOX_SIZE / 3 +
'L' + 3 * Blockly.VerticalFlyout.prototype.CHECKBOX_SIZE / 4 +
' ' + Blockly.VerticalFlyout.prototype.CHECKBOX_SIZE / 3;
/**
* Size of the checkbox corner radius
* @type {number}
* @const
*/
Blockly.VerticalFlyout.prototype.CHECKBOX_CORNER_RADIUS = 5;
/**
* Space above and around the checkbox.
* @type {number}
* @const
*/
Blockly.VerticalFlyout.prototype.CHECKBOX_MARGIN = Blockly.Flyout.prototype.MARGIN;
/**
* Total additional width of a row that contains a checkbox.
* @type {number}
* @const
*/
Blockly.VerticalFlyout.prototype.CHECKBOX_SPACE_X =
Blockly.VerticalFlyout.prototype.CHECKBOX_SIZE +
2 * Blockly.VerticalFlyout.prototype.CHECKBOX_MARGIN;
/**
* Initializes the flyout.
* @param {!Blockly.Workspace} targetWorkspace The workspace in which to create
* new blocks.
*/
Blockly.VerticalFlyout.prototype.init = function(targetWorkspace) {
Blockly.VerticalFlyout.superClass_.init.call(this, targetWorkspace);
this.workspace_.scale = targetWorkspace.scale;
};
/**
* Creates the flyout's DOM. Only needs to be called once.
* @param {string} tagName HTML element
* @return {!Element} The flyout's SVG group.
*/
Blockly.VerticalFlyout.prototype.createDom = function(tagName) {
Blockly.VerticalFlyout.superClass_.createDom.call(this, tagName);
/*
<defs>
<clipPath id="blocklyBlockMenuClipPath">
<rect id="blocklyBlockMenuClipRect" height="1147px"
width="248px" y="0" x="0">
</rect>
</clipPath>
</defs>
*/
this.defs_ = Blockly.utils.createSvgElement('defs', {}, this.svgGroup_);
var clipPath = Blockly.utils.createSvgElement('clipPath',
{'id':'blocklyBlockMenuClipPath'}, this.defs_);
this.clipRect_ = Blockly.utils.createSvgElement('rect',
{
'id': 'blocklyBlockMenuClipRect',
'height': '0',
'width': '0',
'y': '0',
'x': '0'
},
clipPath);
this.workspace_.svgGroup_.setAttribute(
'clip-path', 'url(#blocklyBlockMenuClipPath)');
return this.svgGroup_;
};
/**
* Calculate the bounding box of the flyout.
*
* @return {Object} Contains the position and size of the bounding
* box containing the elements (blocks, buttons, labels) in the flyout.
*/
Blockly.VerticalFlyout.prototype.getContentBoundingBox_ = function() {
var contentBounds = this.workspace_.getBlocksBoundingBox();
var bounds = {
xMin: contentBounds.x,
yMin: contentBounds.y,
xMax: contentBounds.x + contentBounds.width,
yMax: contentBounds.y + contentBounds.height
};
// Check if any of the buttons/labels are outside the blocks bounding box.
for (var i = 0; i < this.buttons_.length; i ++) {
var button = this.buttons_[i];
var buttonPosition = button.getPosition();
if (buttonPosition.x < bounds.xMin) {
bounds.xMin = buttonPosition.x;
}
if (buttonPosition.y < bounds.yMin) {
bounds.yMin = buttonPosition.y;
}
// Button extends past the bounding box to the right.
if (buttonPosition.x + button.width > bounds.xMax) {
bounds.xMax = buttonPosition.x + button.width;
}
// Button extends past the bounding box on the bottom
if (buttonPosition.y + button.height > bounds.yMax) {
bounds.yMax = buttonPosition.y + button.height;
}
}
return {
x: bounds.xMin,
y: bounds.yMin,
width: bounds.xMax - bounds.xMin,
height: bounds.yMax - bounds.yMin,
};
};
/**
* Return an object with all the metrics required to size scrollbars for the
* flyout. The following properties are computed:
* .viewHeight: Height of the visible rectangle,
* .viewWidth: Width of the visible rectangle,
* .contentHeight: Height of the contents,
* .contentWidth: Width of the contents,
* .viewTop: Offset of top edge of visible rectangle from parent,
* .contentTop: Offset of the top-most content from the y=0 coordinate,
* .absoluteTop: Top-edge of view.
* .viewLeft: Offset of the left edge of visible rectangle from parent,
* .contentLeft: Offset of the left-most content from the x=0 coordinate,
* .absoluteLeft: Left-edge of view.
* @return {Object} Contains size and position metrics of the flyout.
* @private
*/
Blockly.VerticalFlyout.prototype.getMetrics_ = function() {
if (!this.isVisible()) {
// Flyout is hidden.
return null;
}
var optionBox = this.getContentBoundingBox_();
// Padding for the end of the scrollbar.
var absoluteTop = this.SCROLLBAR_PADDING;
var absoluteLeft = 0;
var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING;
var viewWidth = this.getWidth() - this.SCROLLBAR_PADDING;
// Add padding to the bottom of the flyout, so we can scroll to the top of
// the last category.
var contentHeight = optionBox.height * this.workspace_.scale;
this.recordCategoryScrollPositions_();
var bottomPadding = this.MARGIN;
if (this.categoryScrollPositions.length > 0) {
var lastLabel = this.categoryScrollPositions[
this.categoryScrollPositions.length - 1];
var lastPos = lastLabel.position * this.workspace_.scale;
var lastCategoryHeight = contentHeight - lastPos;
if (lastCategoryHeight < viewHeight) {
bottomPadding = viewHeight - lastCategoryHeight;
}
}
var metrics = {
viewHeight: viewHeight,
viewWidth: viewWidth,
contentHeight: contentHeight + bottomPadding,
contentWidth: optionBox.width * this.workspace_.scale + 2 * this.MARGIN,
viewTop: -this.workspace_.scrollY + optionBox.y,
viewLeft: -this.workspace_.scrollX,
contentTop: optionBox.y,
contentLeft: optionBox.x,
absoluteTop: absoluteTop,
absoluteLeft: absoluteLeft
};
return metrics;
};
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!Object} xyRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling and a
* similar x property.
* @private
*/
Blockly.VerticalFlyout.prototype.setMetrics_ = function(xyRatio) {
var metrics = this.getMetrics_();
// This is a fix to an apparent race condition.
if (!metrics) {
return;
}
if (goog.isNumber(xyRatio.y)) {
this.workspace_.scrollY = -metrics.contentHeight * xyRatio.y;
}
this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft,
this.workspace_.scrollY + metrics.absoluteTop);
this.clipRect_.setAttribute('height', Math.max(0, metrics.viewHeight) + 'px');
this.clipRect_.setAttribute('width', metrics.viewWidth + 'px');
if (this.categoryScrollPositions) {
this.selectCategoryByScrollPosition(-this.workspace_.scrollY);
}
};
/**
* Move the flyout to the edge of the workspace.
*/
Blockly.VerticalFlyout.prototype.position = function() {
if (!this.isVisible()) {
return;
}
var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics();
if (!targetWorkspaceMetrics) {
// Hidden components will return null.
return;
}
// This version of the flyout does not change width to fit its contents.
// Instead it matches the width of its parent or uses a default value.
this.width_ = this.getWidth();
if (this.parentToolbox_) {
var toolboxWidth = this.parentToolbox_.getWidth();
var categoryWidth = toolboxWidth - this.width_;
var x = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT ?
targetWorkspaceMetrics.viewWidth : categoryWidth;
var y = 0;
} else {
var x = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT ?
targetWorkspaceMetrics.viewWidth - this.width_ : 0;
var y = 0;
}
// Record the height for Blockly.Flyout.getMetrics_
this.height_ = Math.max(0, targetWorkspaceMetrics.viewHeight - y);
this.setBackgroundPath_(this.width_, this.height_);
this.svgGroup_.setAttribute("width", this.width_);
this.svgGroup_.setAttribute("height", this.height_);
var transform = 'translate(' + x + 'px,' + y + 'px)';
Blockly.utils.setCssTransform(this.svgGroup_, transform);
// Update the scrollbar (if one exists).
if (this.scrollbar_) {
// Set the scrollbars origin to be the top left of the flyout.
this.scrollbar_.setOrigin(x, y);
this.scrollbar_.resize();
}
// The blocks need to be visible in order to be laid out and measured
// correctly, but we don't want the flyout to show up until it's properly
// sized. Opacity is set to zero in show().
this.svgGroup_.style.opacity = 1;
};
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
Blockly.VerticalFlyout.prototype.setBackgroundPath_ = function(width, height) {
var atRight = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT;
// Decide whether to start on the left or right.
var path = ['M ' + 0 + ',0'];
// Top.
path.push('h', width);
// Rounded corner.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
atRight ? 0 : 1,
atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS,
this.CORNER_RADIUS);
// Side closest to workspace.
path.push('v', Math.max(0, height - this.CORNER_RADIUS * 2));
// Rounded corner.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
atRight ? 0 : 1,
atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS,
this.CORNER_RADIUS);
// Bottom.
path.push('h', -width);
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Scroll the flyout to the top.
*/
Blockly.VerticalFlyout.prototype.scrollToStart = function() {
this.scrollbar_.set(0);
};
/**
* Scroll the flyout to a position.
* @param {number} pos The targeted scroll position in workspace coordinates.
* @package
*/
Blockly.VerticalFlyout.prototype.scrollTo = function(pos) {
this.scrollTarget = pos * this.workspace_.scale;
// Make sure not to set the scroll target below the lowest point we can
// scroll to, i.e. the content height minus the view height
var metrics = this.workspace_.getMetrics();
var contentHeight = metrics.contentHeight;
var viewHeight = metrics.viewHeight;
this.scrollTarget = Math.min(this.scrollTarget, contentHeight - viewHeight);
this.startScrollAnimation();
};
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @private
*/
Blockly.VerticalFlyout.prototype.wheel_ = function(e) {
// remove scrollTarget to stop auto scrolling in stepScrollAnimation
this.scrollTarget = null;
var delta = e.deltaY;
if (delta) {
// Firefox's mouse wheel deltas are a tenth that of Chrome/Safari.
// DeltaMode is 1 for a mouse wheel, but not for a trackpad scroll event
if (goog.userAgent.GECKO && (e.deltaMode === 1)) {
delta *= 10;
}
var metrics = this.getMetrics_();
var pos = (metrics.viewTop - metrics.contentTop) + delta;
var limit = metrics.contentHeight - metrics.viewHeight;
pos = Math.min(pos, limit);
pos = Math.max(pos, 0);
this.scrollbar_.set(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and DropDownDiv.
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
};
/**
* Delete blocks and background buttons from a previous showing of the flyout.
* @private
*/
Blockly.VerticalFlyout.prototype.clearOldBlocks_ = function() {
Blockly.VerticalFlyout.superClass_.clearOldBlocks_.call(this);
// Do the same for checkboxes.
for (var checkboxId in this.checkboxes_) {
if (!Object.prototype.hasOwnProperty.call(this.checkboxes_, checkboxId)) {
continue;
}
var checkbox = this.checkboxes_[checkboxId];
checkbox.block.flyoutCheckbox = null;
goog.dom.removeNode(checkbox.svgRoot);
}
this.checkboxes_ = {};
};
/**
* Add listeners to a block that has been added to the flyout.
* @param {Element} root The root node of the SVG group the block is in.
* @param {!Blockly.Block} block The block to add listeners for.
* @param {!Element} rect The invisible rectangle under the block that acts as
* a button for that block.
* @private
*/
Blockly.VerticalFlyout.prototype.addBlockListeners_ = function(root, block,
rect) {
Blockly.VerticalFlyout.superClass_.addBlockListeners_.call(this, root, block,
rect);
if (block.flyoutCheckbox) {
this.listeners_.push(Blockly.bindEvent_(block.flyoutCheckbox.svgRoot,
'mousedown', null, this.checkboxClicked_(block.flyoutCheckbox)));
}
};
/**
* Lay out the blocks in the flyout.
* @param {!Array.<!Object>} contents The blocks and buttons to lay out.
* @param {!Array.<number>} gaps The visible gaps between blocks.
* @private
*/
Blockly.VerticalFlyout.prototype.layout_ = function(contents, gaps) {
var margin = this.MARGIN;
var flyoutWidth = this.getWidth() / this.workspace_.scale;
var cursorX = margin;
var cursorY = margin;
for (var i = 0, item; item = contents[i]; i++) {
if (item.type == 'block') {
var block = item.block;
var allBlocks = block.getDescendants(false);
for (var j = 0, child; child = allBlocks[j]; j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such a
// block.
child.isInFlyout = true;
}
var root = block.getSvgRoot();
var blockHW = block.getHeightWidth();
// Figure out where the block goes, taking into account its size, whether
// we're in RTL mode, and whether it has a checkbox.
var oldX = block.getRelativeToSurfaceXY().x;
var newX = flyoutWidth - this.MARGIN;
var moveX = this.RTL ? newX - oldX : margin;
if (block.hasCheckboxInFlyout()) {
this.createCheckbox_(block, cursorX, cursorY, blockHW);
if (this.RTL) {
moveX -= (this.CHECKBOX_SIZE + this.CHECKBOX_MARGIN);
} else {
moveX += this.CHECKBOX_SIZE + this.CHECKBOX_MARGIN;
}
}
// The block moves a bit extra for the hat, but the block's rectangle
// doesn't. That's because the hat actually extends up from 0.
block.moveBy(moveX,
cursorY + (block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0));
var rect = this.createRect_(block, this.RTL ? moveX - blockHW.width : moveX, cursorY, blockHW, i);
this.addBlockListeners_(root, block, rect);
cursorY += blockHW.height + gaps[i] + (block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0);
} else if (item.type == 'button') {
var button = item.button;
var buttonSvg = button.createDom();
if (this.RTL) {
button.moveTo(flyoutWidth - this.MARGIN - button.width, cursorY);
} else {
button.moveTo(cursorX, cursorY);
}
button.show();
// Clicking on a flyout button or label is a lot like clicking on the
// flyout background.
this.listeners_.push(Blockly.bindEventWithChecks_(
buttonSvg, 'mousedown', this, this.onMouseDown_));
this.buttons_.push(button);
cursorY += button.height + gaps[i];
}
}
};
/**
* Create and place a rectangle corresponding to the given block.
* @param {!Blockly.Block} block The block to associate the rect to.
* @param {number} x The x position of the cursor during this layout pass.
* @param {number} y The y position of the cursor during this layout pass.
* @param {!{height: number, width: number}} blockHW The height and width of the
* block.
* @param {number} index The index into the background buttons list where this
* rect should be placed.
* @return {!SVGElement} Newly created SVG element for the rectangle behind the
* block.
* @private
*/
Blockly.VerticalFlyout.prototype.createRect_ = function(block, x, y,
blockHW, index) {
// Create an invisible rectangle under the block to act as a button. Just
// using the block as a button is poor, since blocks have holes in them.
var rect = Blockly.utils.createSvgElement('rect',
{
'fill-opacity': 0,
'x': x,
'y': y,
'height': blockHW.height,
'width': blockHW.width
}, null);
rect.tooltip = block;
Blockly.Tooltip.bindMouseEvents(rect);
// Add the rectangles under the blocks, so that the blocks' tooltips work.
this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot());
block.flyoutRect_ = rect;
this.backgroundButtons_[index] = rect;
return rect;
};
/**
* Create and place a checkbox corresponding to the given block.
* @param {!Blockly.Block} block The block to associate the checkbox to.
* @param {number} cursorX The x position of the cursor during this layout pass.
* @param {number} cursorY The y position of the cursor during this layout pass.
* @param {!{height: number, width: number}} blockHW The height and width of the
* block.
* @private
*/
Blockly.VerticalFlyout.prototype.createCheckbox_ = function(block, cursorX,
cursorY, blockHW) {
var checkboxState = Blockly.VerticalFlyout.getCheckboxState(block.id);
var svgRoot = block.getSvgRoot();
var extraSpace = this.CHECKBOX_SIZE + this.CHECKBOX_MARGIN;
var width = this.RTL ? this.getWidth() / this.workspace_.scale - extraSpace : cursorX;
var height = cursorY + blockHW.height / 2 - this.CHECKBOX_SIZE / 2;
var touchMargin = this.CHECKBOX_TOUCH_PADDING;
var checkboxGroup = Blockly.utils.createSvgElement('g',
{
'transform': 'translate(' + width + ', ' + height + ')'
}, null);
Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyFlyoutCheckbox',
'height': this.CHECKBOX_SIZE,
'width': this.CHECKBOX_SIZE,
'rx': this.CHECKBOX_CORNER_RADIUS,
'ry': this.CHECKBOX_CORNER_RADIUS
}, checkboxGroup);
Blockly.utils.createSvgElement('path',
{
'class': 'blocklyFlyoutCheckboxPath',
'd': this.CHECKMARK_PATH
}, checkboxGroup);
Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyTouchTargetBackground',
'x': -touchMargin + 'px',
'y': -touchMargin + 'px',
'height': this.CHECKBOX_SIZE + 2 * touchMargin,
'width': this.CHECKBOX_SIZE + 2 * touchMargin,
}, checkboxGroup);
var checkboxObj = {svgRoot: checkboxGroup, clicked: checkboxState, block: block};
if (checkboxState) {
Blockly.utils.addClass((checkboxObj.svgRoot), 'checked');
}
block.flyoutCheckbox = checkboxObj;
this.workspace_.getCanvas().insertBefore(checkboxGroup, svgRoot);
this.checkboxes_[block.id] = checkboxObj;
};
/**
* Respond to a click on a checkbox in the flyout.
* @param {!Object} checkboxObj An object containing the svg element of the
* checkbox, a boolean for the state of the checkbox, and the block the
* checkbox is associated with.
* @return {!Function} Function to call when checkbox is clicked.
* @private
*/
Blockly.VerticalFlyout.prototype.checkboxClicked_ = function(checkboxObj) {
return function(e) {
this.setCheckboxState(checkboxObj.block.id, !checkboxObj.clicked);
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
e.preventDefault();
}.bind(this);
};
/**
* Set the state of a checkbox by block ID.
* @param {string} blockId ID of the block whose checkbox should be set
* @param {boolean} value Value to set the checkbox to.
* @public
*/
Blockly.VerticalFlyout.prototype.setCheckboxState = function(blockId, value) {
var checkboxObj = this.checkboxes_[blockId];
if (!checkboxObj || checkboxObj.clicked === value) {
return;
}
var oldValue = checkboxObj.clicked;
checkboxObj.clicked = value;
if (checkboxObj.clicked) {
Blockly.utils.addClass(checkboxObj.svgRoot, 'checked');
} else {
Blockly.utils.removeClass(checkboxObj.svgRoot, 'checked');
}
Blockly.Events.fire(new Blockly.Events.Change(
checkboxObj.block, 'checkbox', null, oldValue, value));
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This to decide if a new block should be
* created or if the flyout should scroll.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} true if the drag is toward the workspace.
* @package
*/
Blockly.VerticalFlyout.prototype.isDragTowardWorkspace = function(currentDragDeltaXY) {
var dx = currentDragDeltaXY.x;
var dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
var dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
var draggingTowardWorkspace = false;
var range = this.dragAngleRange_;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
// Vertical at left.
if (dragDirection < range && dragDirection > -range) {
draggingTowardWorkspace = true;
}
} else {
// Vertical at right.
if (dragDirection < -180 + range || dragDirection > 180 - range) {
draggingTowardWorkspace = true;
}
}
return draggingTowardWorkspace;
};
/**
* Return the deletion rectangle for this flyout in viewport coordinates.
* Deletion area is the height of the flyout, but extends to the left (in LTR)
* by a lot in order to allow for deleting blocks when dragged beyond the left
* window edge. In RTL, the delete area extends off to the right.
* The top/bottom do not extend to allow dragging blocks outside of the workspace
* to be dropped (e.g. to the backpack).
* @return {goog.math.Rect} Rectangle in which to delete.
*/
Blockly.VerticalFlyout.prototype.getClientRect = function() {
if (!this.svgGroup_) {
return null;
}
var flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout
// area are still deleted. Must be larger than the largest screen size,
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
var BIG_NUM = 1000000000;
var x = flyoutRect.left;
var y = flyoutRect.top;
var width = flyoutRect.width;
var height = flyoutRect.height;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
return new goog.math.Rect(x - BIG_NUM, y, BIG_NUM + width, height);
} else { // Right
return new goog.math.Rect(x, y, BIG_NUM + width, height);
}
};
/**
* Compute width of flyout. Position button under each block.
* For RTL: Lay out the blocks right-aligned.
* @param {!Array<!Blockly.Block>} blocks The blocks to reflow.
*/
Blockly.VerticalFlyout.prototype.reflowInternal_ = function(/* blocks */) {
// This is a no-op because the flyout is a fixed size.
return;
};
/**
* Gets the checkbox state for a block
* @param {string} blockId The ID of the block in question.
* @return {boolean} Whether the block is checked.
* @public
*/
Blockly.VerticalFlyout.getCheckboxState = function(/* blockId */) {
return false;
};

View File

@@ -0,0 +1,426 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Utility functions for generating executable code from
* Blockly code.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Generator');
goog.require('Blockly.Block');
goog.require('goog.asserts');
/**
* Class for a code generator that translates the blocks into a language.
* @param {string} name Language name of this generator.
* @constructor
*/
Blockly.Generator = function(name) {
this.name_ = name;
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ =
new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g');
};
/**
* Category to separate generated function names from variables and procedures.
*/
Blockly.Generator.NAME_TYPE = 'generated_function';
/**
* Arbitrary code to inject into locations that risk causing infinite loops.
* Any instances of '%1' will be replaced by the block ID that failed.
* E.g. ' checkTimeout(%1);\n'
* @type {?string}
*/
Blockly.Generator.prototype.INFINITE_LOOP_TRAP = null;
/**
* Arbitrary code to inject before every statement.
* Any instances of '%1' will be replaced by the block ID of the statement.
* E.g. 'highlight(%1);\n'
* @type {?string}
*/
Blockly.Generator.prototype.STATEMENT_PREFIX = null;
/**
* The method of indenting. Defaults to two spaces, but language generators
* may override this to increase indent or change to tabs.
* @type {string}
*/
Blockly.Generator.prototype.INDENT = ' ';
/**
* Maximum length for a comment before wrapping. Does not account for
* indenting level.
* @type {number}
*/
Blockly.Generator.prototype.COMMENT_WRAP = 60;
/**
* List of outer-inner pairings that do NOT require parentheses.
* @type {!Array.<!Array.<number>>}
*/
Blockly.Generator.prototype.ORDER_OVERRIDES = [];
/**
* Generate code for all blocks in the workspace to the specified language.
* @param {Blockly.Workspace} workspace Workspace to generate code from.
* @return {string} Generated code.
*/
Blockly.Generator.prototype.workspaceToCode = function(workspace) {
if (!workspace) {
// Backwards compatibility from before there could be multiple workspaces.
console.warn('No workspace specified in workspaceToCode call. Guessing.');
workspace = Blockly.getMainWorkspace();
}
var code = [];
this.init(workspace);
var blocks = workspace.getTopBlocks(true);
for (var x = 0, block; block = blocks[x]; x++) {
var line = this.blockToCode(block);
if (goog.isArray(line)) {
// Value blocks return tuples of code and operator order.
// Top-level blocks don't care about operator order.
line = line[0];
}
if (line) {
if (block.outputConnection && this.scrubNakedValue) {
// This block is a naked value. Ask the language's code generator if
// it wants to append a semicolon, or something.
line = this.scrubNakedValue(line);
}
code.push(line);
}
}
code = code.join('\n'); // Blank line between each section.
code = this.finish(code);
// Final scrubbing of whitespace.
code = code.replace(/^\s+\n/, '');
code = code.replace(/\n\s+$/, '\n');
code = code.replace(/[ \t]+\n/g, '\n');
return code;
};
// The following are some helpful functions which can be used by multiple
// languages.
/**
* Prepend a common prefix onto each line of code.
* @param {string} text The lines of code.
* @param {string} prefix The common prefix.
* @return {string} The prefixed lines of code.
*/
Blockly.Generator.prototype.prefixLines = function(text, prefix) {
return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix);
};
/**
* Recursively spider a tree of blocks, returning all their comments.
* @param {!Blockly.Block} block The block from which to start spidering.
* @return {string} Concatenated list of comments.
*/
Blockly.Generator.prototype.allNestedComments = function(block) {
var comments = [];
var blocks = block.getDescendants(true);
for (var i = 0; i < blocks.length; i++) {
var comment = blocks[i].getCommentText();
if (comment) {
comments.push(comment);
}
}
// Append an empty string to create a trailing line break when joined.
if (comments.length) {
comments.push('');
}
return comments.join('\n');
};
/**
* Generate code for the specified block (and attached blocks).
* @param {Blockly.Block} block The block to generate code for.
* @return {string|!Array} For statement blocks, the generated code.
* For value blocks, an array containing the generated code and an
* operator order value. Returns '' if block is null.
*/
Blockly.Generator.prototype.blockToCode = function(block) {
if (!block) {
return '';
}
if (block.disabled) {
// Skip past this block if it is disabled.
return this.blockToCode(block.getNextBlock());
}
var func = this[block.type];
goog.asserts.assertFunction(func,
'Language "%s" does not know how to generate code for block type "%s".',
this.name_, block.type);
// First argument to func.call is the value of 'this' in the generator.
// Prior to 24 September 2013 'this' was the only way to access the block.
// The current prefered method of accessing the block is through the second
// argument to func.call, which becomes the first parameter to the generator.
var code = func.call(block, block);
if (goog.isArray(code)) {
// Value blocks return tuples of code and operator order.
goog.asserts.assert(block.outputConnection,
'Expecting string from statement block "%s".', block.type);
return [this.scrub_(block, code[0]), code[1]];
} else if (goog.isString(code)) {
var id = block.id.replace(/\$/g, '$$$$'); // Issue 251.
if (this.STATEMENT_PREFIX) {
code = this.STATEMENT_PREFIX.replace(/%1/g, '\'' + id + '\'') +
code;
}
return this.scrub_(block, code);
} else if (code === null) {
// Block has handled code generation itself.
return '';
} else {
goog.asserts.fail('Invalid code generated: %s', code);
}
};
/**
* Generate code representing the specified value input.
* @param {!Blockly.Block} block The block containing the input.
* @param {string} name The name of the input.
* @param {number} outerOrder The maximum binding strength (minimum order value)
* of any operators adjacent to "block".
* @return {string} Generated code or '' if no blocks are connected or the
* specified input does not exist.
*/
Blockly.Generator.prototype.valueToCode = function(block, name, outerOrder) {
if (isNaN(outerOrder)) {
goog.asserts.fail('Expecting valid order from block "%s".', block.type);
}
var targetBlock = block.getInputTargetBlock(name);
if (!targetBlock) {
return '';
}
var tuple = this.blockToCode(targetBlock);
if (tuple === '') {
// Disabled block.
return '';
}
// Value blocks must return code and order of operations info.
// Statement blocks must only return code.
goog.asserts.assertArray(tuple, 'Expecting tuple from value block "%s".',
targetBlock.type);
var code = tuple[0];
var innerOrder = tuple[1];
if (isNaN(innerOrder)) {
goog.asserts.fail('Expecting valid order from value block "%s".',
targetBlock.type);
}
if (!code) {
return '';
}
// Add parentheses if needed.
var parensNeeded = false;
var outerOrderClass = Math.floor(outerOrder);
var innerOrderClass = Math.floor(innerOrder);
if (outerOrderClass <= innerOrderClass) {
if (outerOrderClass == innerOrderClass &&
(outerOrderClass == 0 || outerOrderClass == 99)) {
// Don't generate parens around NONE-NONE and ATOMIC-ATOMIC pairs.
// 0 is the atomic order, 99 is the none order. No parentheses needed.
// In all known languages multiple such code blocks are not order
// sensitive. In fact in Python ('a' 'b') 'c' would fail.
} else {
// The operators outside this code are stronger than the operators
// inside this code. To prevent the code from being pulled apart,
// wrap the code in parentheses.
parensNeeded = true;
// Check for special exceptions.
for (var i = 0; i < this.ORDER_OVERRIDES.length; i++) {
if (this.ORDER_OVERRIDES[i][0] == outerOrder &&
this.ORDER_OVERRIDES[i][1] == innerOrder) {
parensNeeded = false;
break;
}
}
}
}
if (parensNeeded) {
// Technically, this should be handled on a language-by-language basis.
// However all known (sane) languages use parentheses for grouping.
code = '(' + code + ')';
}
return code;
};
/**
* Generate code representing the statement. Indent the code.
* @param {!Blockly.Block} block The block containing the input.
* @param {string} name The name of the input.
* @return {string} Generated code or '' if no blocks are connected.
*/
Blockly.Generator.prototype.statementToCode = function(block, name) {
var targetBlock = block.getInputTargetBlock(name);
var code = this.blockToCode(targetBlock);
// Value blocks must return code and order of operations info.
// Statement blocks must only return code.
goog.asserts.assertString(code, 'Expecting code from statement block "%s".',
targetBlock && targetBlock.type);
if (code) {
code = this.prefixLines(/** @type {string} */ (code), this.INDENT);
}
return code;
};
/**
* Add an infinite loop trap to the contents of a loop.
* If loop is empty, add a statment prefix for the loop block.
* @param {string} branch Code for loop contents.
* @param {string} id ID of enclosing block.
* @return {string} Loop contents, with infinite loop trap added.
*/
Blockly.Generator.prototype.addLoopTrap = function(branch, id) {
id = id.replace(/\$/g, '$$$$'); // Issue 251.
if (this.INFINITE_LOOP_TRAP) {
branch = this.INFINITE_LOOP_TRAP.replace(/%1/g, '\'' + id + '\'') + branch;
}
if (this.STATEMENT_PREFIX) {
branch += this.prefixLines(this.STATEMENT_PREFIX.replace(/%1/g,
'\'' + id + '\''), this.INDENT);
}
return branch;
};
/**
* Comma-separated list of reserved words.
* @type {string}
* @private
*/
Blockly.Generator.prototype.RESERVED_WORDS_ = '';
/**
* Add one or more words to the list of reserved words for this language.
* @param {string} words Comma-separated list of words to add to the list.
* No spaces. Duplicates are ok.
*/
Blockly.Generator.prototype.addReservedWords = function(words) {
this.RESERVED_WORDS_ += words + ',';
};
/**
* This is used as a placeholder in functions defined using
* Blockly.Generator.provideFunction_. It must not be legal code that could
* legitimately appear in a function definition (or comment), and it must
* not confuse the regular expression parser.
* @type {string}
* @private
*/
Blockly.Generator.prototype.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}';
/**
* Define a function to be included in the generated code.
* The first time this is called with a given desiredName, the code is
* saved and an actual name is generated. Subsequent calls with the
* same desiredName have no effect but have the same return value.
*
* It is up to the caller to make sure the same desiredName is not
* used for different code values.
*
* The code gets output when Blockly.Generator.finish() is called.
*
* @param {string} desiredName The desired name of the function (e.g., isPrime).
* @param {!Array.<string>} code A list of statements. Use ' ' for indents.
* @return {string} The actual name of the new function. This may differ
* from desiredName if the former has already been taken by the user.
* @private
*/
Blockly.Generator.prototype.provideFunction_ = function(desiredName, code) {
if (!this.definitions_[desiredName]) {
var functionName = this.variableDB_.getDistinctName(desiredName,
Blockly.Procedures.NAME_TYPE);
this.functionNames_[desiredName] = functionName;
var codeText = code.join('\n').replace(
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName);
// Change all ' ' indents into the desired indent.
// To avoid an infinite loop of replacements, change all indents to '\0'
// character first, then replace them all with the indent.
// We are assuming that no provided functions contain a literal null char.
var oldCodeText;
while (oldCodeText != codeText) {
oldCodeText = codeText;
codeText = codeText.replace(/^(( {2})*) {2}/gm, '$1\0');
}
codeText = codeText.replace(/\0/g, this.INDENT);
this.definitions_[desiredName] = codeText;
}
return this.functionNames_[desiredName];
};
/**
* Hook for code to run before code generation starts.
* Subclasses may override this, e.g. to initialise the database of variable
* names.
* @param {!Blockly.Workspace} _workspace Workspace to generate code from.
*/
Blockly.Generator.prototype.init = function(_workspace) {
// Optionally override
};
/**
* Common tasks for generating code from blocks. This is called from
* blockToCode and is called on every block, not just top level blocks.
* Subclasses may override this, e.g. to generate code for statements following
* the block, or to handle comments for the specified block and any connected
* value blocks.
* @param {!Blockly.Block} _block The current block.
* @param {string} code The JavaScript code created for this block.
* @return {string} JavaScript code with comments and subsequent blocks added.
* @private
*/
Blockly.Generator.prototype.scrub_ = function(_block, code) {
// Optionally override
return code;
};
/**
* Hook for code to run at end of code generation.
* Subclasses may override this, e.g. to prepend the generated code with the
* variable definitions.
* @param {string} code Generated code.
* @return {string} Completed code.
*/
Blockly.Generator.prototype.finish = function(code) {
// Optionally override
return code;
};
/**
* Naked values are top-level blocks with outputs that aren't plugged into
* anything.
* Subclasses may override this, e.g. if their language does not allow
* naked values.
* @param {string} line Line of generated code.
* @return {string} Legal line of code.
*/
Blockly.Generator.prototype.scrubNakedValue = function(line) {
// Optionally override
return line;
};

File diff suppressed because it is too large Load Diff

227
scratch-blocks/core/grid.js Normal file
View File

@@ -0,0 +1,227 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Object for configuring and updating a workspace grid in
* Blockly.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.Grid');
goog.require('Blockly.utils');
goog.require('goog.userAgent');
/**
* Class for a workspace's grid.
* @param {!SVGElement} pattern The grid's SVG pattern, created during injection.
* @param {!Object} options A dictionary of normalized options for the grid.
* See grid documentation:
* https://developers.google.com/blockly/guides/configure/web/grid
* @constructor
*/
Blockly.Grid = function(pattern, options) {
/**
* The grid's SVG pattern, created during injection.
* @type {!SVGElement}
* @private
*/
this.gridPattern_ = pattern;
/**
* The spacing of the grid lines (in px).
* @type {number}
* @private
*/
this.spacing_ = options['spacing'];
/**
* How long the grid lines should be (in px).
* @type {number}
* @private
*/
this.length_ = options['length'];
/**
* The horizontal grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line1_ = pattern.firstChild;
/**
* The vertical grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line2_ = this.line1_ && this.line1_.nextSibling;
/**
* Whether blocks should snap to the grid.
* @type {boolean}
* @private
*/
this.snapToGrid_ = options['snap'];
};
/**
* The scale of the grid, used to set stroke width on grid lines.
* This should always be the same as the workspace scale.
* @type {number}
* @private
*/
Blockly.Grid.prototype.scale_ = 1;
/**
* Dispose of this grid and unlink from the DOM.
* @package
*/
Blockly.Grid.prototype.dispose = function() {
this.gridPattern_ = null;
};
/**
* Whether blocks should snap to the grid, based on the initial configuration.
* @return {boolean} True if blocks should snap, false otherwise.
* @package
*/
Blockly.Grid.prototype.shouldSnap = function() {
return this.snapToGrid_;
};
/**
* Get the spacing of the grid points (in px).
* @return {number} The spacing of the grid points.
* @package
*/
Blockly.Grid.prototype.getSpacing = function() {
return this.spacing_;
};
/**
* Get the id of the pattern element, which should be randomized to avoid
* conflicts with other Blockly instances on the page.
* @return {string} The pattern ID.
* @package
*/
Blockly.Grid.prototype.getPatternId = function() {
return this.gridPattern_.id;
};
/**
* Update the grid with a new scale.
* @param {number} scale The new workspace scale.
* @package
*/
Blockly.Grid.prototype.update = function(scale) {
this.scale_ = scale;
// MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
var safeSpacing = (this.spacing_ * scale) || 100;
this.gridPattern_.setAttribute('width', safeSpacing);
this.gridPattern_.setAttribute('height', safeSpacing);
var half = Math.floor(this.spacing_ / 2) + 0.5;
var start = half - this.length_ / 2;
var end = half + this.length_ / 2;
half *= scale;
start *= scale;
end *= scale;
this.setLineAttributes_(this.line1_, scale, start, end, half, half);
this.setLineAttributes_(this.line2_, scale, half, half, start, end);
};
/**
* Set the attributes on one of the lines in the grid. Use this to update the
* length and stroke width of the grid lines.
* @param {!SVGElement} line Which line to update.
* @param {number} width The new stroke size (in px).
* @param {number} x1 The new x start position of the line (in px).
* @param {number} x2 The new x end position of the line (in px).
* @param {number} y1 The new y start position of the line (in px).
* @param {number} y2 The new y end position of the line (in px).
* @private
*/
Blockly.Grid.prototype.setLineAttributes_ = function(line, width, x1, x2, y1, y2) {
if (line) {
line.setAttribute('stroke-width', width);
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
}
};
/**
* Move the grid to a new x and y position, and make sure that change is visible.
* @param {number} x The new x position of the grid (in px).
* @param {number} y The new y position ofthe grid (in px).
* @package
*/
Blockly.Grid.prototype.moveTo = function(x, y) {
this.gridPattern_.setAttribute('x', x);
this.gridPattern_.setAttribute('y', y);
if (goog.userAgent.IE || goog.userAgent.EDGE) {
// IE/Edge doesn't notice that the x/y offsets have changed.
// Force an update.
this.update(this.scale_);
}
};
/**
* Create the DOM for the grid described by options.
* @param {string} rnd A random ID to append to the pattern's ID.
* @param {!Object} gridOptions The object containing grid configuration.
* @param {!SVGElement} defs The root SVG element for this workspace's defs.
* @return {!SVGElement} The SVG element for the grid pattern.
* @package
*/
Blockly.Grid.createDom = function(rnd, gridOptions, defs) {
/*
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
<rect stroke="#888" />
<rect stroke="#888" />
</pattern>
*/
var gridPattern = Blockly.utils.createSvgElement('pattern',
{
'id': 'blocklyGridPattern' + rnd,
'patternUnits': 'userSpaceOnUse'
}, defs);
if (gridOptions['length'] > 0 && gridOptions['spacing'] > 0) {
Blockly.utils.createSvgElement('line',
{'stroke': gridOptions['colour']}, gridPattern);
if (gridOptions['length'] > 1) {
Blockly.utils.createSvgElement('line',
{'stroke': gridOptions['colour']}, gridPattern);
}
// x1, y1, x1, x2 properties will be set later in update.
} else {
// Edge 16 doesn't handle empty patterns
Blockly.utils.createSvgElement('line', {}, gridPattern);
}
return gridPattern;
};

205
scratch-blocks/core/icon.js Normal file
View File

@@ -0,0 +1,205 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2013 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 Object representing an icon on a block.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Icon');
goog.require('goog.dom');
goog.require('goog.math.Coordinate');
/**
* Class for an icon.
* @param {Blockly.Block} block The block associated with this icon.
* @constructor
*/
Blockly.Icon = function(block) {
this.block_ = block;
};
/**
* Does this icon get hidden when the block is collapsed.
*/
Blockly.Icon.prototype.collapseHidden = true;
/**
* Height and width of icons.
*/
Blockly.Icon.prototype.SIZE = 17;
/**
* Bubble UI (if visible).
* @type {Blockly.Bubble}
* @protected
*/
Blockly.Icon.prototype.bubble_ = null;
/**
* Absolute coordinate of icon's center.
* @type {goog.math.Coordinate}
* @protected
*/
Blockly.Icon.prototype.iconXY_ = null;
/**
* Create the icon on the block.
*/
Blockly.Icon.prototype.createIcon = function() {
if (this.iconGroup_) {
// Icon already exists.
return;
}
/* Here's the markup that will be generated:
<g class="blocklyIconGroup">
...
</g>
*/
this.iconGroup_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyIconGroup'}, null);
if (this.block_.isInFlyout) {
Blockly.utils.addClass(
/** @type {!Element} */ (this.iconGroup_), 'blocklyIconGroupReadonly');
}
this.drawIcon_(this.iconGroup_);
this.block_.getSvgRoot().appendChild(this.iconGroup_);
Blockly.bindEventWithChecks_(
this.iconGroup_, 'mouseup', this, this.iconClick_);
this.updateEditable();
};
/**
* Dispose of this icon.
*/
Blockly.Icon.prototype.dispose = function() {
// Dispose of and unlink the icon.
goog.dom.removeNode(this.iconGroup_);
this.iconGroup_ = null;
// Dispose of and unlink the bubble.
this.setVisible(false);
this.block_ = null;
};
/**
* Add or remove the UI indicating if this icon may be clicked or not.
*/
Blockly.Icon.prototype.updateEditable = function() {
};
/**
* Is the associated bubble visible?
* @return {boolean} True if the bubble is visible.
*/
Blockly.Icon.prototype.isVisible = function() {
return !!this.bubble_;
};
/**
* Clicking on the icon toggles if the bubble is visible.
* @param {!Event} e Mouse click event.
* @protected
*/
Blockly.Icon.prototype.iconClick_ = function(e) {
if (this.block_.workspace.isDragging()) {
// Drag operation is concluding. Don't open the editor.
return;
}
if (!this.block_.isInFlyout && !Blockly.utils.isRightButton(e)) {
this.setVisible(!this.isVisible());
}
};
/**
* Change the colour of the associated bubble to match its block.
*/
Blockly.Icon.prototype.updateColour = function() {
if (this.isVisible()) {
this.bubble_.setColour(this.block_.getColour());
}
};
/**
* Render the icon.
* @param {number} cursorX Horizontal offset at which to position the icon.
* @return {number} Horizontal offset for next item to draw.
*/
Blockly.Icon.prototype.renderIcon = function(cursorX) {
if (this.collapseHidden && this.block_.isCollapsed()) {
this.iconGroup_.setAttribute('display', 'none');
return cursorX;
}
this.iconGroup_.setAttribute('display', 'block');
var TOP_MARGIN = 5;
var width = this.SIZE;
if (this.block_.RTL) {
cursorX -= width;
}
this.iconGroup_.setAttribute('transform',
'translate(' + cursorX + ',' + TOP_MARGIN + ')');
this.computeIconLocation();
if (this.block_.RTL) {
cursorX -= Blockly.BlockSvg.SEP_SPACE_X;
} else {
cursorX += width + Blockly.BlockSvg.SEP_SPACE_X;
}
return cursorX;
};
/**
* Notification that the icon has moved. Update the arrow accordingly.
* @param {!goog.math.Coordinate} xy Absolute location in workspace coordinates.
*/
Blockly.Icon.prototype.setIconLocation = function(xy) {
this.iconXY_ = xy;
if (this.isVisible()) {
this.bubble_.setAnchorLocation(xy);
}
};
/**
* Notification that the icon has moved, but we don't really know where.
* Recompute the icon's location from scratch.
*/
Blockly.Icon.prototype.computeIconLocation = function() {
// Find coordinates for the centre of the icon and update the arrow.
var blockXY = this.block_.getRelativeToSurfaceXY();
var iconXY = Blockly.utils.getRelativeXY(this.iconGroup_);
var newXY = new goog.math.Coordinate(
blockXY.x + iconXY.x + this.SIZE / 2,
blockXY.y + iconXY.y + this.SIZE / 2);
if (!goog.math.Coordinate.equals(this.getIconLocation(), newXY)) {
this.setIconLocation(newXY);
}
};
/**
* Returns the center of the block's icon relative to the surface.
* @return {!goog.math.Coordinate} Object with x and y properties in workspace
* coordinates.
*/
Blockly.Icon.prototype.getIconLocation = function() {
return this.iconXY_;
};

View File

@@ -0,0 +1,496 @@
/**
* @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 Functions for injecting Blockly into a web page.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.inject');
goog.require('Blockly.BlockDragSurfaceSvg');
goog.require('Blockly.Css');
goog.require('Blockly.constants');
goog.require('Blockly.DropDownDiv');
goog.require('Blockly.Grid');
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');
/**
* Inject a Blockly editor into the specified container element (usually a div).
* @param {!Element|string} container Containing element, or its ID,
* or a CSS selector.
* @param {Object=} opt_options Optional dictionary of options.
* @return {!Blockly.Workspace} Newly created main workspace.
*/
Blockly.inject = function(container, opt_options) {
if (goog.isString(container)) {
container = document.getElementById(container) ||
document.querySelector(container);
}
// Verify that the container is in document.
if (!goog.dom.contains(document, container)) {
throw 'Error: container is not in current document.';
}
var options = new Blockly.Options(opt_options || {});
var subContainer = goog.dom.createDom('div', 'injectionDiv');
container.appendChild(subContainer);
// Open the Field text cache and leave it open. See this issue for more information
// https://github.com/LLK/scratch-blocks/issues/1004
Blockly.Field.startCache();
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);
var workspaceDragSurface = null;
var workspace = Blockly.createMainWorkspace_(svg, options, blockDragSurface,
workspaceDragSurface);
Blockly.init_(workspace);
Blockly.mainWorkspace = workspace;
Blockly.svgResize(workspace);
return workspace;
};
/**
* Create the SVG image.
* @param {!Element} container Containing element.
* @param {!Blockly.Options} options Dictionary of options.
* @return {!Element} Newly created SVG image.
* @private
*/
Blockly.createDom_ = function(container, options) {
// Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying
// out content in RTL mode. Therefore Blockly forces the use of LTR,
// then manually positions content in RTL as needed.
container.setAttribute('dir', 'LTR');
// Closure can be trusted to create HTML widgets with the proper direction.
goog.ui.Component.setDefaultRightToLeft(options.RTL);
// Load CSS.
Blockly.Css.inject(options.hasCss, options.pathToMedia);
// Build the SVG DOM.
/*
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
class="blocklySvg">
...
</svg>
*/
var svg = Blockly.utils.createSvgElement('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:html': 'http://www.w3.org/1999/xhtml',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
'version': '1.1',
'class': 'blocklySvg'
}, container);
/*
<defs>
... filters go here ...
</defs>
*/
var defs = Blockly.utils.createSvgElement('defs', {}, svg);
// Each filter/pattern needs a unique ID for the case of multiple Blockly
// instances on a page. Browser behaviour becomes undefined otherwise.
// https://neil.fraser.name/news/2015/11/01/
// TODO (tmickel): Look into whether block highlighting still works.
// Reference commit:
// https://github.com/google/blockly/commit/144be4d49f36fdba260a26edbd170ae75bbc37a6
var rnd = String(Math.random()).substring(2);
// Using a dilate distorts the block shape.
// Instead use a gaussian blur, and then set all alpha to 1 with a transfer.
var stackGlowFilter = Blockly.utils.createSvgElement('filter',
{
'id': 'blocklyStackGlowFilter' + rnd,
'height': '160%',
'width': '180%',
y: '-30%',
x: '-40%'
},
defs);
options.stackGlowBlur = Blockly.utils.createSvgElement('feGaussianBlur',
{
'in': 'SourceGraphic',
'stdDeviation': Blockly.Colours.stackGlowSize
},
stackGlowFilter);
// Set all gaussian blur pixels to 1 opacity before applying flood
var componentTransfer = Blockly.utils.createSvgElement('feComponentTransfer', {'result': 'outBlur'}, stackGlowFilter);
Blockly.utils.createSvgElement('feFuncA',
{
'type': 'table',
'tableValues': '0' + goog.string.repeat(' 1', 16)
},
componentTransfer);
// Color the highlight
Blockly.utils.createSvgElement('feFlood',
{
'flood-color': Blockly.Colours.stackGlow,
'flood-opacity': Blockly.Colours.stackGlowOpacity,
'result': 'outColor'
},
stackGlowFilter);
Blockly.utils.createSvgElement('feComposite',
{
'in': 'outColor',
'in2': 'outBlur',
'operator': 'in',
'result': 'outGlow'
},
stackGlowFilter);
Blockly.utils.createSvgElement('feComposite',
{
'in': 'SourceGraphic',
'in2': 'outGlow',
'operator': 'over'
},
stackGlowFilter);
// Filter for replacement marker
var replacementGlowFilter = Blockly.utils.createSvgElement('filter',
{
'id': 'blocklyReplacementGlowFilter' + rnd,
'height': '160%',
'width': '180%',
y: '-30%',
x: '-40%'
},
defs);
Blockly.utils.createSvgElement('feGaussianBlur',
{
'in': 'SourceGraphic',
'stdDeviation': Blockly.Colours.replacementGlowSize
},
replacementGlowFilter);
// Set all gaussian blur pixels to 1 opacity before applying flood
var componentTransfer = Blockly.utils.createSvgElement('feComponentTransfer',
{'result': 'outBlur'}, replacementGlowFilter);
Blockly.utils.createSvgElement('feFuncA',
{
'type': 'table',
'tableValues': '0' + goog.string.repeat(' 1', 16)
},
componentTransfer);
// Color the highlight
Blockly.utils.createSvgElement('feFlood',
{
'flood-color': Blockly.Colours.replacementGlow,
'flood-opacity': Blockly.Colours.replacementGlowOpacity,
'result': 'outColor'
},
replacementGlowFilter);
Blockly.utils.createSvgElement('feComposite',
{
'in': 'outColor',
'in2': 'outBlur',
'operator': 'in',
'result': 'outGlow'
},
replacementGlowFilter);
Blockly.utils.createSvgElement('feComposite',
{
'in': 'SourceGraphic',
'in2': 'outGlow',
'operator': 'over'
},
replacementGlowFilter);
/*
<pattern id="blocklyDisabledPattern837493" patternUnits="userSpaceOnUse"
width="10" height="10">
<rect width="10" height="10" fill="#aaa" />
<path d="M 0 0 L 10 10 M 10 0 L 0 10" stroke="#cc0" />
</pattern>
*/
var disabledPattern = Blockly.utils.createSvgElement('pattern',
{
'id': 'blocklyDisabledPattern' + rnd,
'patternUnits': 'userSpaceOnUse',
'width': 10,
'height': 10
},
defs);
Blockly.utils.createSvgElement('rect',
{
'width': 10,
'height': 10,
'fill': '#aaa'
},
disabledPattern);
Blockly.utils.createSvgElement('path',
{
'd': 'M 0 0 L 10 10 M 10 0 L 0 10',
'stroke': '#cc0'
},
disabledPattern);
options.stackGlowFilterId = stackGlowFilter.id;
options.replacementGlowFilterId = replacementGlowFilter.id;
options.disabledPatternId = disabledPattern.id;
options.gridPattern = Blockly.Grid.createDom(rnd, options.gridOptions, defs);
return svg;
};
/**
* 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 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, workspaceDragSurface) {
options.parentWorkspace = null;
var mainWorkspace = new Blockly.WorkspaceSvg(options, blockDragSurface, workspaceDragSurface);
mainWorkspace.scale = options.zoomOptions.startScale;
svg.appendChild(mainWorkspace.createDom('blocklyMainBackground'));
if (!options.hasCategories && options.languageTree) {
// Add flyout as an <svg> that is a sibling of the workspace svg.
var flyout = mainWorkspace.addFlyout_('svg');
Blockly.utils.insertAfter(flyout, svg);
}
// A null translation will also apply the correct initial scale.
mainWorkspace.translate(0, 0);
Blockly.mainWorkspace = mainWorkspace;
if (!options.readOnly && !options.hasScrollbars) {
var workspaceChanged = function() {
if (!mainWorkspace.isDragging()) {
var metrics = mainWorkspace.getMetrics();
var edgeLeft = metrics.viewLeft + metrics.absoluteLeft;
var edgeTop = metrics.viewTop + metrics.absoluteTop;
if (metrics.contentTop < edgeTop ||
metrics.contentTop + metrics.contentHeight >
metrics.viewHeight + edgeTop ||
metrics.contentLeft <
(options.RTL ? metrics.viewLeft : edgeLeft) ||
metrics.contentLeft + metrics.contentWidth > (options.RTL ?
metrics.viewWidth : metrics.viewWidth + edgeLeft)) {
// One or more blocks may be out of bounds. Bump them back in.
var MARGIN = 25;
var blocks = mainWorkspace.getTopBlocks(false);
for (var b = 0, block; block = blocks[b]; b++) {
var blockXY = block.getRelativeToSurfaceXY();
var blockHW = block.getHeightWidth();
// Bump any block that's above the top back inside.
var overflowTop = edgeTop + MARGIN - blockHW.height - blockXY.y;
if (overflowTop > 0) {
block.moveBy(0, overflowTop);
}
// Bump any block that's below the bottom back inside.
var overflowBottom =
edgeTop + metrics.viewHeight - MARGIN - blockXY.y;
if (overflowBottom < 0) {
block.moveBy(0, overflowBottom);
}
// Bump any block that's off the left back inside.
var overflowLeft = MARGIN + edgeLeft -
blockXY.x - (options.RTL ? 0 : blockHW.width);
if (overflowLeft > 0) {
block.moveBy(overflowLeft, 0);
}
// Bump any block that's off the right back inside.
var overflowRight = edgeLeft + metrics.viewWidth - MARGIN -
blockXY.x + (options.RTL ? blockHW.width : 0);
if (overflowRight < 0) {
block.moveBy(overflowRight, 0);
}
}
}
}
};
mainWorkspace.addChangeListener(workspaceChanged);
}
// The SVG is now fully assembled.
Blockly.svgResize(mainWorkspace);
Blockly.WidgetDiv.createDom();
Blockly.DropDownDiv.createDom();
Blockly.Tooltip.createDom();
return mainWorkspace;
};
/**
* Initialize Blockly with various handlers.
* @param {!Blockly.Workspace} mainWorkspace Newly created main workspace.
* @private
*/
Blockly.init_ = function(mainWorkspace) {
var options = mainWorkspace.options;
var svg = mainWorkspace.getParentSvg();
// This fixes wheel events in Safari.
// This makes no sense, but it really does work.
// https://bugs.webkit.org/show_bug.cgi?id=226683#c4
svg.parentNode.addEventListener('wheel', function() {});
// Suppress the browser's context menu.
Blockly.bindEventWithChecks_(svg.parentNode, 'contextmenu', null,
function(e) {
if (!Blockly.utils.isTargetInput(e)) {
e.preventDefault();
}
});
var workspaceResizeHandler = Blockly.bindEventWithChecks_(window, 'resize',
null,
function() {
Blockly.hideChaffOnResize(true);
Blockly.svgResize(mainWorkspace);
});
mainWorkspace.setResizeHandlerWrapper(workspaceResizeHandler);
Blockly.inject.bindDocumentEvents_();
if (options.languageTree) {
if (mainWorkspace.toolbox_) {
mainWorkspace.toolbox_.init(mainWorkspace);
} else if (mainWorkspace.flyout_) {
// Build a fixed flyout with the root blocks.
mainWorkspace.flyout_.init(mainWorkspace);
mainWorkspace.flyout_.show(options.languageTree.childNodes);
mainWorkspace.flyout_.scrollToStart();
// Translate the workspace to avoid the fixed flyout.
if (options.horizontalLayout) {
mainWorkspace.scrollY = mainWorkspace.flyout_.height_;
if (options.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
mainWorkspace.scrollY *= -1;
}
} else {
mainWorkspace.scrollX = mainWorkspace.flyout_.width_;
if (options.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
mainWorkspace.scrollX *= -1;
}
}
mainWorkspace.translate(mainWorkspace.scrollX, mainWorkspace.scrollY);
}
}
if (options.hasScrollbars) {
mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace);
mainWorkspace.scrollbar.resize();
}
// Load the sounds.
if (options.hasSounds) {
Blockly.inject.loadSounds_(options.pathToMedia, mainWorkspace);
}
};
/**
* Bind document events, but only once. Destroying and reinjecting Blockly
* should not bind again.
* Bind events for scrolling the workspace.
* Most of these events should be bound to the SVG's surface.
* However, 'mouseup' has to be on the whole document so that a block dragged
* out of bounds and released will know that it has been released.
* Also, 'keydown' has to be on the whole document since the browser doesn't
* understand a concept of focus on the SVG image.
* @private
*/
Blockly.inject.bindDocumentEvents_ = function() {
if (!Blockly.documentEventsBound_) {
Blockly.bindEventWithChecks_(document, 'keydown', null, Blockly.onKeyDown_);
// longStop needs to run to stop the context menu from showing up. It
// should run regardless of what other touch event handlers have run.
Blockly.bindEvent_(document, 'touchend', null, Blockly.longStop_);
Blockly.bindEvent_(document, 'touchcancel', null, Blockly.longStop_);
// Some iPad versions don't fire resize after portrait to landscape change.
if (goog.userAgent.IPAD) {
Blockly.bindEventWithChecks_(window, 'orientationchange', document,
function() {
// TODO(#397): Fix for multiple blockly workspaces.
Blockly.svgResize(Blockly.getMainWorkspace());
});
}
}
Blockly.documentEventsBound_ = true;
};
/**
* Load sounds for the given workspace.
* @param {string} pathToMedia The path to the media directory.
* @param {!Blockly.Workspace} workspace The workspace to load sounds for.
* @private
*/
Blockly.inject.loadSounds_ = function(pathToMedia, workspace) {
var audioMgr = workspace.getAudioManager();
audioMgr.load(
[
pathToMedia + 'click.mp3',
pathToMedia + 'click.wav',
pathToMedia + 'click.ogg'
],
'click');
audioMgr.load(
[
pathToMedia + 'delete.mp3',
pathToMedia + 'delete.ogg',
pathToMedia + 'delete.wav'
],
'delete');
// Bind temporary hooks that preload the sounds.
var soundBinds = [];
var unbindSounds = function() {
while (soundBinds.length) {
Blockly.unbindEvent_(soundBinds.pop());
}
audioMgr.preload();
};
// opt_noCaptureIdentifier is true because this is an action to take on a
// click, not a drag.
// Android ignores any sound not loaded as a result of a user action.
soundBinds.push(
Blockly.bindEventWithChecks_(document, 'mousemove', null, unbindSounds,
/* opt_noCaptureIdentifier */ true));
soundBinds.push(
Blockly.bindEventWithChecks_(document, 'touchstart', null, unbindSounds,
/* opt_noCaptureIdentifier */ true));
};
/**
* Modify the block tree on the existing toolbox.
* @param {Node|string} tree DOM tree of blocks, or text representation of same.
* @deprecated April 2015
*/
Blockly.updateToolbox = function(tree) {
console.warn('Deprecated call to Blockly.updateToolbox, ' +
'use workspace.updateToolbox instead.');
Blockly.getMainWorkspace().updateToolbox(tree);
};

View File

@@ -0,0 +1,285 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Object representing an input (value, statement, or dummy).
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Input');
goog.require('Blockly.Connection');
goog.require('Blockly.FieldLabel');
goog.require('goog.asserts');
/**
* Class for an input with an optional field.
* @param {number} type The type of the input.
* @param {string} name Language-neutral identifier which may used to find this
* input again.
* @param {!Blockly.Block} block The block containing this input.
* @param {Blockly.Connection} connection Optional connection for this input.
* @constructor
*/
Blockly.Input = function(type, name, block, connection) {
if (type != Blockly.DUMMY_INPUT && !name) {
throw 'Value inputs and statement inputs must have non-empty name.';
}
/** @type {number} */
this.type = type;
/** @type {string} */
this.name = name;
/**
* @type {!Blockly.Block}
* @private
*/
this.sourceBlock_ = block;
/** @type {Blockly.Connection} */
this.connection = connection;
/** @type {!Array.<!Blockly.Field>} */
this.fieldRow = [];
/**
* The shape that is displayed when this input is rendered but not filled.
* @type {SVGElement}
* @package
*/
this.outlinePath = null;
};
/**
* Alignment of input's fields (left, right or centre).
* @type {number}
*/
Blockly.Input.prototype.align = Blockly.ALIGN_LEFT;
/**
* Is the input visible?
* @type {boolean}
* @private
*/
Blockly.Input.prototype.visible_ = true;
/**
* Add a field (or label from string), and all prefix and suffix fields, to the
* end of the input's field row.
* @param {string|!Blockly.Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to find
* this field again. Should be unique to the host block.
* @return {!Blockly.Input} The input being append to (to allow chaining).
*/
Blockly.Input.prototype.appendField = function(field, opt_name) {
this.insertFieldAt(this.fieldRow.length, field, opt_name);
return this;
};
/**
* Inserts a field (or label from string), and all prefix and suffix fields, at
* the location of the input's field row.
* @param {number} index The index at which to insert field.
* @param {string|!Blockly.Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to find
* this field again. Should be unique to the host block.
* @return {number} The index following the last inserted field.
*/
Blockly.Input.prototype.insertFieldAt = function(index, field, opt_name) {
if (index < 0 || index > this.fieldRow.length) {
throw new Error('index ' + index + ' out of bounds.');
}
// Empty string, Null or undefined generates no field, unless field is named.
if (!field && !opt_name) {
return this;
}
// Generate a FieldLabel when given a plain text field.
if (goog.isString(field)) {
field = new Blockly.FieldLabel(/** @type {string} */ (field));
}
field.setSourceBlock(this.sourceBlock_);
if (this.sourceBlock_.rendered) {
field.init();
}
field.name = opt_name;
if (field.prefixField) {
// Add any prefix.
index = this.insertFieldAt(index, field.prefixField);
}
// Add the field to the field row.
this.fieldRow.splice(index, 0, field);
++index;
if (field.suffixField) {
// Add any suffix.
index = this.insertFieldAt(index, field.suffixField);
}
if (this.sourceBlock_.rendered) {
this.sourceBlock_.render();
// Adding a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours_();
}
return index;
};
/**
* Remove a field from this input.
* @param {string} name The name of the field.
* @throws {goog.asserts.AssertionError} if the field is not present.
*/
Blockly.Input.prototype.removeField = function(name) {
for (var i = 0, field; field = this.fieldRow[i]; i++) {
if (field.name === name) {
field.dispose();
this.fieldRow.splice(i, 1);
if (this.sourceBlock_.rendered) {
this.sourceBlock_.render();
// Removing a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours_();
}
return;
}
}
goog.asserts.fail('Field "%s" not found.', name);
};
/**
* Gets whether this input is visible or not.
* @return {boolean} True if visible.
*/
Blockly.Input.prototype.isVisible = function() {
return this.visible_;
};
/**
* Sets whether this input is visible or not.
* Used to collapse/uncollapse a block.
* @param {boolean} visible True if visible.
* @return {!Array.<!Blockly.Block>} List of blocks to render.
*/
Blockly.Input.prototype.setVisible = function(visible) {
var renderList = [];
if (this.visible_ == visible) {
return renderList;
}
this.visible_ = visible;
var display = visible ? 'block' : 'none';
for (var y = 0, field; field = this.fieldRow[y]; y++) {
field.setVisible(visible);
}
if (this.connection) {
// Has a connection.
if (visible) {
renderList = this.connection.unhideAll();
} else {
this.connection.hideAll();
}
var child = this.connection.targetBlock();
if (child) {
child.getSvgRoot().style.display = display;
if (!visible) {
child.rendered = false;
}
}
}
return renderList;
};
/**
* Change a connection's compatibility.
* @param {string|Array.<string>|null} check Compatible value type or
* list of value types. Null if all types are compatible.
* @return {!Blockly.Input} The input being modified (to allow chaining).
*/
Blockly.Input.prototype.setCheck = function(check) {
if (!this.connection) {
throw 'This input does not have a connection.';
}
this.connection.setCheck(check);
return this;
};
/**
* Change the alignment of the connection's field(s).
* @param {number} align One of Blockly.ALIGN_LEFT, ALIGN_CENTRE, ALIGN_RIGHT.
* In RTL mode directions are reversed, and ALIGN_RIGHT aligns to the left.
* @return {!Blockly.Input} The input being modified (to allow chaining).
*/
Blockly.Input.prototype.setAlign = function(align) {
this.align = align;
if (this.sourceBlock_.rendered) {
this.sourceBlock_.render();
}
return this;
};
/**
* Initialize the fields on this input.
*/
Blockly.Input.prototype.init = function() {
if (!this.sourceBlock_.workspace.rendered) {
return; // Headless blocks don't need fields initialized.
}
for (var i = 0; i < this.fieldRow.length; i++) {
this.fieldRow[i].init(this.sourceBlock_);
}
};
/**
* Sever all links to this input.
*/
Blockly.Input.prototype.dispose = function() {
if (this.outlinePath) {
goog.dom.removeNode(this.outlinePath);
}
for (var i = 0, field; field = this.fieldRow[i]; i++) {
field.dispose();
}
if (this.connection) {
this.connection.dispose();
}
this.sourceBlock_ = null;
};
/**
* Create the input shape path element and attach it to the given SVG element.
* @param {!SVGElement} svgRoot The parent on which ot append the new element.
* @package
*/
Blockly.Input.prototype.initOutlinePath = function(svgRoot) {
if (!this.sourceBlock_.workspace.rendered) {
return; // Headless blocks don't need field outlines.
}
if (this.outlinePath) {
return;
}
if (this.type == Blockly.INPUT_VALUE) {
this.outlinePath = Blockly.utils.createSvgElement(
'path',
{
'class': 'blocklyPath',
'style': 'visibility: hidden', // Hide by default - shown when not connected.
'd': '' // IE doesn't like paths without the data definition, set an empty default
},
svgRoot);
}
};

View File

@@ -0,0 +1,678 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Class that controls updates to connections during drags.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.InsertionMarkerManager');
goog.require('Blockly.BlockAnimations');
goog.require('Blockly.Events.BlockMove');
goog.require('Blockly.RenderedConnection');
goog.require('goog.math.Coordinate');
/**
* Class that controls updates to connections during drags. It is primarily
* responsible for finding the closest eligible connection and highlighting or
* unhiglighting it as needed during a drag.
* @param {!Blockly.BlockSvg} block The top block in the stack being dragged.
* @constructor
*/
Blockly.InsertionMarkerManager = function(block) {
Blockly.selected = block;
/**
* The top block in the stack being dragged.
* Does not change during a drag.
* @type {!Blockly.Block}
* @private
*/
this.topBlock_ = block;
/**
* The workspace on which these connections are being dragged.
* Does not change during a drag.
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = block.workspace;
/**
* The last connection on the stack, if it's not the last connection on the
* first block.
* Set in initAvailableConnections, if at all.
* @type {Blockly.RenderedConnection}
* @private
*/
this.lastOnStack_ = null;
/**
* The insertion marker corresponding to the last block in the stack, if
* that's not the same as the first block in the stack.
* Set in initAvailableConnections, if at all
* @type {Blockly.BlockSvg}
* @private
*/
this.lastMarker_ = null;
/**
* The insertion marker that shows up between blocks to show where a block
* would go if dropped immediately.
* This is the scratch-blocks equivalent of connection highlighting.
* @type {Blockly.BlockSvg}
* @private
*/
this.firstMarker_ = this.createMarkerBlock_(this.topBlock_);
/**
* The connection that this block would connect to if released immediately.
* Updated on every mouse move.
* This is not on any of the blocks that are being dragged.
* @type {Blockly.RenderedConnection}
* @private
*/
this.closestConnection_ = null;
/**
* The connection that would connect to this.closestConnection_ if this block
* were released immediately.
* Updated on every mouse move.
* This is on the top block that is being dragged or the last block in the
* dragging stack.
* @type {Blockly.RenderedConnection}
* @private
*/
this.localConnection_ = null;
/**
* Whether the block would be deleted if it were dropped immediately.
* Updated on every mouse move.
* @type {boolean}
* @private
*/
this.wouldDeleteBlock_ = false;
/**
* Connection on the insertion marker block that corresponds to
* this.localConnection_ on the currently dragged block.
* This is part of the scratch-blocks equivalent of connection highlighting.
* @type {Blockly.RenderedConnection}
* @private
*/
this.markerConnection_ = null;
/**
* Whether we are currently highlighting the block (shadow or real) that would
* be replaced if the drag were released immediately.
* @type {boolean}
* @private
*/
this.highlightingBlock_ = false;
/**
* The block that is being highlighted for replacement, or null.
* @type {Blockly.BlockSvg}
* @private
*/
this.highlightedBlock_ = null;
/**
* The connections on the dragging blocks that are available to connect to
* other blocks. This includes all open connections on the top block, as well
* as the last connection on the block stack.
* Does not change during a drag.
* @type {!Array.<!Blockly.RenderedConnection>}
* @private
*/
this.availableConnections_ = this.initAvailableConnections_();
};
/**
* Sever all links from this object.
* @package
*/
Blockly.InsertionMarkerManager.prototype.dispose = function() {
this.topBlock_ = null;
this.workspace_ = null;
this.availableConnections_.length = 0;
this.closestConnection_ = null;
this.localConnection_ = null;
Blockly.Events.disable();
try {
if (this.firstMarker_) {
this.firstMarker_.dispose();
this.firstMarker_ = null;
}
if (this.lastMarker_) {
this.lastMarker_.dispose();
this.lastMarker_ = null;
}
} finally {
Blockly.Events.enable();
}
this.highlightedBlock_ = null;
};
/**
* Return whether the block would be deleted if dropped immediately, based on
* information from the most recent move event.
* @return {boolean} true if the block would be deleted if dropped immediately.
* @package
*/
Blockly.InsertionMarkerManager.prototype.wouldDeleteBlock = function() {
return this.wouldDeleteBlock_;
};
/**
* Return whether the block would be connected if dropped immediately, based on
* information from the most recent move event.
* @return {boolean} True if the block would be connected if dropped
* immediately.
* @package
*/
Blockly.InsertionMarkerManager.prototype.wouldConnectBlock = function() {
return !!this.closestConnection_;
};
/**
* Connect to the closest connection and render the results.
* This should be called at the end of a drag.
* @package
*/
Blockly.InsertionMarkerManager.prototype.applyConnections = function() {
if (this.closestConnection_) {
// Don't fire events for insertion markers.
Blockly.Events.disable();
this.hidePreview_();
Blockly.Events.enable();
// Connect two blocks together.
this.localConnection_.connect(this.closestConnection_);
if (this.topBlock_.rendered) {
// Trigger a connection animation.
// Determine which connection is inferior (lower in the source stack).
var inferiorConnection = this.localConnection_.isSuperior() ?
this.closestConnection_ : this.localConnection_;
Blockly.BlockAnimations.connectionUiEffect(
inferiorConnection.getSourceBlock());
// Bring the just-edited stack to the front.
var rootBlock = this.topBlock_.getRootBlock();
rootBlock.bringToFront();
}
}
};
/**
* Update highlighted connections based on the most recent move location.
* @param {!goog.math.Coordinate} dxy Position relative to drag start,
* in workspace units.
* @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @package
*/
Blockly.InsertionMarkerManager.prototype.update = function(dxy, deleteArea) {
var candidate = this.getCandidate_(dxy);
this.wouldDeleteBlock_ = this.shouldDelete_(candidate, deleteArea);
var shouldUpdate = this.wouldDeleteBlock_ ||
this.shouldUpdatePreviews_(candidate, dxy);
if (shouldUpdate) {
// Don't fire events for insertion marker creation or movement.
Blockly.Events.disable();
this.maybeHidePreview_(candidate);
this.maybeShowPreview_(candidate);
Blockly.Events.enable();
}
};
/**** Begin initialization functions ****/
/**
* Create an insertion marker that represents the given block.
* @param {!Blockly.BlockSvg} sourceBlock The block that the insertion marker
* will represent.
* @return {!Blockly.BlockSvg} The insertion marker that represents the given
* block.
* @private
*/
Blockly.InsertionMarkerManager.prototype.createMarkerBlock_ = function(sourceBlock) {
var imType = sourceBlock.type;
Blockly.Events.disable();
try {
var result = this.workspace_.newBlock(imType);
result.setInsertionMarker(true, sourceBlock.width);
if (sourceBlock.mutationToDom) {
var oldMutationDom = sourceBlock.mutationToDom();
if (oldMutationDom) {
result.domToMutation(oldMutationDom);
}
}
result.initSvg();
} finally {
Blockly.Events.enable();
}
return result;
};
/**
* Populate the list of available connections on this block stack. This should
* only be called once, at the beginning of a drag.
* If the stack has more than one block, this function will populate
* lastOnStack_ and create the corresponding insertion marker.
* @return {!Array.<!Blockly.RenderedConnection>} a list of available
* connections.
* @private
*/
Blockly.InsertionMarkerManager.prototype.initAvailableConnections_ = function() {
var available = this.topBlock_.getConnections_(false);
// Also check the last connection on this stack
var lastOnStack = this.topBlock_.lastConnectionInStack();
if (lastOnStack && lastOnStack != this.topBlock_.nextConnection) {
available.push(lastOnStack);
this.lastOnStack_ = lastOnStack;
this.lastMarker_ = this.createMarkerBlock_(lastOnStack.sourceBlock_);
}
return available;
};
/**** End initialization functions ****/
/**
* Whether the previews (insertion marker and replacement marker) should be
* updated based on the closest candidate and the current drag distance.
* @param {!Object} candidate An object containing a local connection, a closest
* connection, and a radius. Returned by getCandidate_.
* @param {!goog.math.Coordinate} dxy Position relative to drag start,
* in workspace units.
* @return {boolean} whether the preview should be updated.
* @private
*/
Blockly.InsertionMarkerManager.prototype.shouldUpdatePreviews_ = function(
candidate, dxy) {
var candidateLocal = candidate.local;
var candidateClosest = candidate.closest;
var radius = candidate.radius;
// Found a connection!
if (candidateLocal && candidateClosest) {
if (candidateLocal.type == Blockly.OUTPUT_VALUE) {
// Always update previews for output connections.
return true;
}
// We're already showing an insertion marker.
// Decide whether the new connection has higher priority.
if (this.localConnection_ && this.closestConnection_) {
// The connection was the same as the current connection.
if (this.closestConnection_ == candidateClosest) {
return false;
}
var xDiff = this.localConnection_.x_ + dxy.x - this.closestConnection_.x_;
var yDiff = this.localConnection_.y_ + dxy.y - this.closestConnection_.y_;
var curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
// Slightly prefer the existing preview over a new preview.
return !(candidateClosest && radius > curDistance -
Blockly.CURRENT_CONNECTION_PREFERENCE);
} else if (!this.localConnection_ && !this.closestConnection_) {
// We weren't showing a preview before, but we should now.
return true;
} else {
console.error('Only one of localConnection_ and closestConnection_ was set.');
}
} else { // No connection found.
// Only need to update if we were showing a preview before.
return !!(this.localConnection_ && this.closestConnection_);
}
console.error('Returning true from shouldUpdatePreviews, but it\'s not clear why.');
return true;
};
/**
* Find the nearest valid connection, which may be the same as the current
* closest connection.
* @param {!goog.math.Coordinate} dxy Position relative to drag start,
* in workspace units.
* @return {!Object} candidate An object containing a local connection, a closest
* connection, and a radius.
*/
Blockly.InsertionMarkerManager.prototype.getCandidate_ = function(dxy) {
var radius = this.getStartRadius_();
var candidateClosest = null;
var candidateLocal = null;
for (var i = 0; i < this.availableConnections_.length; i++) {
var myConnection = this.availableConnections_[i];
var neighbour = myConnection.closest(radius, dxy);
if (neighbour.connection) {
candidateClosest = neighbour.connection;
candidateLocal = myConnection;
radius = neighbour.radius;
}
}
return {
closest: candidateClosest,
local: candidateLocal,
radius: radius
};
};
/**
* Decide the radius at which to start searching for the closest connection.
* @return {number} The radius at which to start the search for the closest
* connection.
* @private
*/
Blockly.InsertionMarkerManager.prototype.getStartRadius_ = function() {
// If there is already a connection highlighted,
// increase the radius we check for making new connections.
// Why? When a connection is highlighted, blocks move around when the insertion
// marker is created, which could cause the connection became out of range.
// By increasing radiusConnection when a connection already exists,
// we never "lose" the connection from the offset.
if (this.closestConnection_ && this.localConnection_) {
return Blockly.CONNECTING_SNAP_RADIUS;
}
return Blockly.SNAP_RADIUS;
};
/**
* Whether ending the drag would replace a block or insert a block.
* @return {boolean} True if dropping the block immediately would replace
* another block. False if dropping the block immediately would result in
* the block being inserted in a block stack.
* @private
*/
Blockly.InsertionMarkerManager.prototype.shouldReplace_ = function() {
var closest = this.closestConnection_;
var local = this.localConnection_;
// Dragging a block over an existing block in an input should replace the
// existing block and bump it out.
if (local.type == Blockly.OUTPUT_VALUE) {
return true; // Replace.
}
// Connecting to a statement input of c-block is an insertion, even if that
// c-block is terminal (e.g. forever).
if (local == local.sourceBlock_.getFirstStatementConnection()) {
return false; // Insert.
}
// Dragging a terminal block over another (connected) terminal block will
// replace, not insert.
var isTerminalBlock = !this.topBlock_.nextConnection;
var isConnectedTerminal = isTerminalBlock &&
local.type == Blockly.PREVIOUS_STATEMENT && closest.isConnected();
if (isConnectedTerminal) {
return true; // Replace.
}
// Otherwise it's an insertion.
return false;
};
/**
* Whether ending the drag would delete the block.
* @param {!Object} candidate An object containing a local connection, a closest
* connection, and a radius.
* @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @return {boolean} True if dropping the block immediately would replace
* delete the block. False otherwise.
* @private
*/
Blockly.InsertionMarkerManager.prototype.shouldDelete_ = function(candidate,
deleteArea) {
// Prefer connecting over dropping into the trash can, but prefer dragging to
// the toolbox over connecting to other blocks.
var wouldConnect = candidate && !!candidate.closest &&
deleteArea != Blockly.DELETE_AREA_TOOLBOX;
var wouldDelete = !!deleteArea && !this.topBlock_.getParent() &&
this.topBlock_.isDeletable();
return wouldDelete && !wouldConnect;
};
/**** Begin preview visibility functions ****/
/**
* Show an insertion marker or replacement highlighting during a drag, if
* needed.
* At the beginning of this function, this.localConnection_ and
* this.closestConnection_ should both be null.
* @param {!Object} candidate An object containing a local connection, a closest
* connection, and a radius.
* @private
*/
Blockly.InsertionMarkerManager.prototype.maybeShowPreview_ = function(candidate) {
// Nope, don't add a marker.
if (this.wouldDeleteBlock_) {
return;
}
var closest = candidate.closest;
var local = candidate.local;
// Nothing to connect to.
if (!closest) {
return;
}
// Something went wrong and we're trying to connect to an invalid connection.
if (closest == this.closestConnection_ ||
closest.sourceBlock_.isInsertionMarker()) {
return;
}
// Add an insertion marker or replacement marker.
this.closestConnection_ = closest;
this.localConnection_ = local;
this.showPreview_();
};
/**
* A preview should be shown. This function figures out if it should be a block
* highlight or an insertion marker, and shows the appropriate one.
* @private
*/
Blockly.InsertionMarkerManager.prototype.showPreview_ = function() {
if (this.shouldReplace_()) {
this.highlightBlock_();
} else { // Should insert
this.connectMarker_();
}
};
/**
* Show an insertion marker or replacement highlighting during a drag, if
* needed.
* At the end of this function, this.localConnection_ and
* this.closestConnection_ should both be null.
* @param {!Object} candidate An object containing a local connection, a closest
* connection, and a radius.
* @private
*/
Blockly.InsertionMarkerManager.prototype.maybeHidePreview_ = function(candidate) {
// If there's no new preview, remove the old one but don't bother deleting it.
// We might need it later, and this saves disposing of it and recreating it.
if (!candidate.closest) {
this.hidePreview_();
}
// If there's a new preview and there was an preview before, and either
// connection has changed, remove the old preview.
var hadPreview = this.closestConnection_ && this.localConnection_;
var closestChanged = this.closestConnection_ != candidate.closest;
var localChanged = this.localConnection_ != candidate.local;
// Also hide if we had a preview before but now we're going to delete instead.
if (hadPreview && (closestChanged || localChanged || this.wouldDeleteBlock_)) {
this.hidePreview_();
}
// Either way, clear out old state.
this.markerConnection_ = null;
this.closestConnection_ = null;
this.localConnection_ = null;
};
/**
* A preview should be hidden. This function figures out if it is a block
* highlight or an insertion marker, and hides the appropriate one.
* @private
*/
Blockly.InsertionMarkerManager.prototype.hidePreview_ = function() {
if (this.highlightingBlock_) {
this.unhighlightBlock_();
} else if (this.markerConnection_) {
this.disconnectMarker_();
}
};
/**** End preview visibility functions ****/
/**** Begin block highlighting functions ****/
/**
* Add highlighting showing which block will be replaced.
* Scratch-specific code, where "highlighting" applies to a block rather than
* a connection.
*/
Blockly.InsertionMarkerManager.prototype.highlightBlock_ = function() {
var closest = this.closestConnection_;
var local = this.localConnection_;
if (closest.targetBlock()) {
this.highlightedBlock_ = closest.targetBlock();
closest.targetBlock().highlightForReplacement(true);
} else if(local.type == Blockly.OUTPUT_VALUE) {
this.highlightedBlock_ = closest.sourceBlock_;
closest.sourceBlock_.highlightShapeForInput(closest, true);
}
this.highlightingBlock_ = true;
};
/**
* Get rid of the highlighting marking the block that will be replaced.
* Scratch-specific code, where "highlighting" applies to a block rather than
* a connection.
*/
Blockly.InsertionMarkerManager.prototype.unhighlightBlock_ = function() {
var closest = this.closestConnection_;
// If there's no block in place, but we're still connecting to a value input,
// then we must have been highlighting an input shape.
if (closest.type == Blockly.INPUT_VALUE && !closest.isConnected()) {
this.highlightedBlock_.highlightShapeForInput(closest, false);
} else {
this.highlightedBlock_.highlightForReplacement(false);
}
this.highlightedBlock_ = null;
this.highlightingBlock_ = false;
};
/**** End block highlighting functions ****/
/**** Begin insertion marker display functions ****/
/**
* Disconnect the insertion marker block in a manner that returns the stack to
* original state.
* @private
*/
Blockly.InsertionMarkerManager.prototype.disconnectMarker_ = function() {
if (!this.markerConnection_) {
console.log('No insertion marker connection to disconnect');
return;
}
var imConn = this.markerConnection_;
var imBlock = imConn.sourceBlock_;
var markerNext = imBlock.nextConnection;
var markerPrev = imBlock.previousConnection;
// The insertion marker is the first block in a stack, either because it
// doesn't have a previous connection or because the previous connection is
// not connected. Unplug won't do anything in that case. Instead, unplug the
// following block.
if (imConn == markerNext && !(markerPrev && markerPrev.targetConnection)) {
imConn.targetBlock().unplug(false);
}
// Inside of a C-block, first statement connection.
else if (imConn.type == Blockly.NEXT_STATEMENT && imConn != markerNext) {
var innerConnection = imConn.targetConnection;
innerConnection.sourceBlock_.unplug(false);
var previousBlockNextConnection =
markerPrev ? markerPrev.targetConnection : null;
imBlock.unplug(true);
if (previousBlockNextConnection) {
previousBlockNextConnection.connect(innerConnection);
}
} else {
imBlock.unplug(true /* healStack */);
}
if (imConn.targetConnection) {
throw 'markerConnection_ still connected at the end of disconnectInsertionMarker';
}
this.markerConnection_ = null;
imBlock.getSvgRoot().setAttribute('visibility', 'hidden');
};
/**
* Add an insertion marker connected to the appropriate blocks.
* @private
*/
Blockly.InsertionMarkerManager.prototype.connectMarker_ = function() {
var local = this.localConnection_;
var closest = this.closestConnection_;
var isLastInStack = this.lastOnStack_ && local == this.lastOnStack_;
var imBlock = isLastInStack ? this.lastMarker_ : this.firstMarker_;
var imConn = imBlock.getMatchingConnection(local.sourceBlock_, local);
goog.asserts.assert(imConn != this.markerConnection_,
'Made it to connectMarker_ even though the marker isn\'t changing');
// Render disconnected from everything else so that we have a valid
// connection location.
imBlock.render();
imBlock.rendered = true;
imBlock.getSvgRoot().setAttribute('visibility', 'visible');
// TODO: positionNewBlock should be on Blockly.BlockSvg, not prototype,
// because it doesn't rely on anything in the block it's called on.
imBlock.positionNewBlock(imBlock, imConn, closest);
// Connect() also renders the insertion marker.
imConn.connect(closest);
this.markerConnection_ = imConn;
};
/**** End insertion marker display functions ****/

View File

@@ -0,0 +1,102 @@
'use strict';
goog.provide('Blockly.IntersectionObserver');
Blockly.IntersectionObserver = function(workspace) {
this.workspace = workspace;
this.observing = [];
this.intersectionCheckQueued = false;
this.checkForIntersections = this.checkForIntersections.bind(this);
};
Blockly.IntersectionObserver.prototype.observe = function(block) {
var index = this.observing.indexOf(block);
if (index === -1) {
this.observing.push(block);
}
};
Blockly.IntersectionObserver.prototype.unobserve = function(block) {
var index = this.observing.indexOf(block);
if (index !== -1) {
this.observing = this.observing.filter(function(i) {
return i !== block;
});
}
};
Blockly.IntersectionObserver.prototype.dispose = function() {
this.observing = [];
this.workspace = null;
};
Blockly.IntersectionObserver.prototype.queueIntersectionCheck = function() {
if (this.intersectionCheckQueued) {
return;
}
this.intersectionCheckQueued = true;
// Check for intersections on the next microtick
// Prefer to use the native method when available, otherwise fallback to a Promise-based polyfill
if (window.queueMicrotask) {
window.queueMicrotask(this.checkForIntersections);
} else {
// eslint-disable-next-line no-undef
Promise.resolve().then(this.checkForIntersections);
}
};
Blockly.IntersectionObserver.prototype.checkForIntersections = function() {
this.intersectionCheckQueued = false;
if (!this.workspace) {
return;
}
var workspace = this.workspace;
var workspaceScale = workspace.scale;
var RTL = workspace.RTL;
var workspaceHeight = workspace.getParentSvg().height.baseVal.value;
var workspaceWidth = workspace.getParentSvg().width.baseVal.value;
if (workspace.isDragSurfaceActive_) {
var canvasPos = Blockly.utils.getRelativeXY(workspace.workspaceDragSurface_.SVG_);
} else {
var canvasPos = Blockly.utils.getRelativeXY(workspace.getCanvas());
}
// Allow blocks to go slightly offscreen so that effects such as glow do not get cut off.
var margin = 12 * workspaceScale;
for (var i = 0; i < this.observing.length; i++) {
var block = this.observing[i];
var blockPos = block.getRelativeToSurfaceXY();
var blockSize = null;
if (RTL) {
blockSize = block.getHeightWidth();
blockPos.x -= blockSize.width;
blockSize.width *= workspaceScale;
blockSize.height *= workspaceScale;
}
blockPos.x *= workspaceScale;
blockPos.y *= workspaceScale;
var visible = true;
if (canvasPos.y + blockPos.y - margin > workspaceHeight) {
visible = false;
} else if (canvasPos.x + blockPos.x - margin > workspaceWidth) {
visible = false;
} else {
if (!blockSize) {
blockSize = block.getHeightWidth();
blockSize.width *= workspaceScale;
blockSize.height *= workspaceScale;
}
if (canvasPos.x + blockPos.x + blockSize.width + margin < 0) {
visible = false;
} else if (canvasPos.y + blockPos.y + blockSize.height + margin < 0) {
visible = false;
}
}
block.setIntersects(visible);
}
};

View File

@@ -0,0 +1,62 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2013 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 Empty name space for the Message singleton.
* @author scr@google.com (Sheridan Rawlins)
*/
'use strict';
/**
* Name space for the Msg singleton.
* Msg gets populated in the message files.
*/
goog.provide('Blockly.Msg');
/**
* Back up original getMsg function.
* @type {!Function}
*/
goog.getMsgOrig = goog.getMsg;
/**
* Gets a localized message.
* Overrides the default Closure function to check for a Blockly.Msg first.
* Used infrequently, only known case is TODAY button in date picker.
* @param {string} str Translatable string, places holders in the form {$foo}.
* @param {Object<string, string>=} opt_values Maps place holder name to value.
* @return {string} message with placeholders filled.
* @suppress {duplicate}
*/
goog.getMsg = function(str, opt_values) {
var key = goog.getMsg.blocklyMsgMap[str];
if (key) {
str = Blockly.Msg[key];
}
return goog.getMsgOrig(str, opt_values);
};
/**
* Mapping of Closure messages to Blockly.Msg names.
*/
goog.getMsg.blocklyMsgMap = {
'Today': 'TODAY'
};

View File

@@ -0,0 +1,426 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Object representing a mutator dialog. A mutator allows the
* user to change the shape of a block using a nested blocks editor.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Mutator');
goog.require('Blockly.Bubble');
goog.require('Blockly.Events.BlockChange');
goog.require('Blockly.Events.Ui');
goog.require('Blockly.Icon');
goog.require('Blockly.WorkspaceSvg');
goog.require('goog.dom');
/**
* Class for a mutator dialog.
* @param {!Array.<string>} quarkNames List of names of sub-blocks for flyout.
* @extends {Blockly.Icon}
* @constructor
*/
Blockly.Mutator = function(quarkNames) {
Blockly.Mutator.superClass_.constructor.call(this, null);
this.quarkNames_ = quarkNames;
};
goog.inherits(Blockly.Mutator, Blockly.Icon);
/**
* Width of workspace.
* @private
*/
Blockly.Mutator.prototype.workspaceWidth_ = 0;
/**
* Height of workspace.
* @private
*/
Blockly.Mutator.prototype.workspaceHeight_ = 0;
/**
* Draw the mutator icon.
* @param {!Element} group The icon group.
* @private
*/
Blockly.Mutator.prototype.drawIcon_ = function(group) {
// Square with rounded corners.
Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyIconShape',
'rx': '4',
'ry': '4',
'height': '16',
'width': '16'
},
group);
// Gear teeth.
Blockly.utils.createSvgElement('path',
{
'class': 'blocklyIconSymbol',
'd': 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,' +
'0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,' +
'-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,' +
'-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,' +
'-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 ' +
'-0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,' +
'0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z'
},
group);
// Axle hole.
Blockly.utils.createSvgElement(
'circle',
{
'class': 'blocklyIconShape',
'r': '2.7',
'cx': '8',
'cy': '8'
},
group);
};
/**
* Clicking on the icon toggles if the mutator bubble is visible.
* Disable if block is uneditable.
* @param {!Event} e Mouse click event.
* @private
* @override
*/
Blockly.Mutator.prototype.iconClick_ = function(e) {
if (this.block_.isEditable()) {
Blockly.Icon.prototype.iconClick_.call(this, e);
}
};
/**
* Create the editor for the mutator's bubble.
* @return {!Element} The top-level node of the editor.
* @private
*/
Blockly.Mutator.prototype.createEditor_ = function() {
/* Create the editor. Here's the markup that will be generated:
<svg>
[Workspace]
</svg>
*/
this.svgDialog_ = Blockly.utils.createSvgElement('svg',
{'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH},
null);
// Convert the list of names into a list of XML objects for the flyout.
if (this.quarkNames_.length) {
var quarkXml = goog.dom.createDom('xml');
for (var i = 0, quarkName; quarkName = this.quarkNames_[i]; i++) {
quarkXml.appendChild(goog.dom.createDom('block', {'type': quarkName}));
}
} else {
var quarkXml = null;
}
var workspaceOptions = {
languageTree: quarkXml,
parentWorkspace: this.block_.workspace,
pathToMedia: this.block_.workspace.options.pathToMedia,
RTL: this.block_.RTL,
toolboxPosition: this.block_.RTL ? Blockly.TOOLBOX_AT_RIGHT :
Blockly.TOOLBOX_AT_LEFT,
horizontalLayout: false,
getMetrics: this.getFlyoutMetrics_.bind(this),
setMetrics: null
};
this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions, this.block_.workspace.dragSurface);
this.workspace_.isMutator = true;
// Mutator flyouts go inside the mutator workspace's <g> rather than in
// a top level svg. Instead of handling scale themselves, mutators
// inherit scale from the parent workspace.
// To fix this, scale needs to be applied at a different level in the dom.
var flyoutSvg = this.workspace_.addFlyout_('g');
var background = this.workspace_.createDom('blocklyMutatorBackground');
// Insert the flyout after the <rect> but before the block canvas so that
// the flyout is underneath in z-order. This makes blocks layering during
// dragging work properly.
background.insertBefore(flyoutSvg, this.workspace_.svgBlockCanvas_);
this.svgDialog_.appendChild(background);
return this.svgDialog_;
};
/**
* Add or remove the UI indicating if this icon may be clicked or not.
*/
Blockly.Mutator.prototype.updateEditable = function() {
if (!this.block_.isInFlyout) {
if (this.block_.isEditable()) {
if (this.iconGroup_) {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.iconGroup_),
'blocklyIconGroupReadonly');
}
} else {
// Close any mutator bubble. Icon is not clickable.
this.setVisible(false);
if (this.iconGroup_) {
Blockly.utils.addClass(
/** @type {!Element} */ (this.iconGroup_),
'blocklyIconGroupReadonly');
}
}
}
// Default behaviour for an icon.
Blockly.Icon.prototype.updateEditable.call(this);
};
/**
* Callback function triggered when the bubble has resized.
* Resize the workspace accordingly.
* @private
*/
Blockly.Mutator.prototype.resizeBubble_ = function() {
var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
var workspaceSize = this.workspace_.getCanvas().getBBox();
var width;
if (this.block_.RTL) {
width = -workspaceSize.x;
} else {
width = workspaceSize.width + workspaceSize.x;
}
var height = workspaceSize.height + doubleBorderWidth * 3;
if (this.workspace_.flyout_) {
var flyoutMetrics = this.workspace_.flyout_.getMetrics_();
height = Math.max(height, flyoutMetrics.contentHeight + 20);
}
width += doubleBorderWidth * 3;
// Only resize if the size difference is significant. Eliminates shuddering.
if (Math.abs(this.workspaceWidth_ - width) > doubleBorderWidth ||
Math.abs(this.workspaceHeight_ - height) > doubleBorderWidth) {
// Record some layout information for getFlyoutMetrics_.
this.workspaceWidth_ = width;
this.workspaceHeight_ = height;
// Resize the bubble.
this.bubble_.setBubbleSize(
width + doubleBorderWidth, height + doubleBorderWidth);
this.svgDialog_.setAttribute('width', this.workspaceWidth_);
this.svgDialog_.setAttribute('height', this.workspaceHeight_);
}
if (this.block_.RTL) {
// Scroll the workspace to always left-align.
var translation = 'translate(' + this.workspaceWidth_ + ',0)';
this.workspace_.getCanvas().setAttribute('transform', translation);
}
this.workspace_.resize();
};
/**
* Show or hide the mutator bubble.
* @param {boolean} visible True if the bubble should be visible.
*/
Blockly.Mutator.prototype.setVisible = function(visible) {
if (visible == this.isVisible()) {
// No change.
return;
}
Blockly.Events.fire(
new Blockly.Events.Ui(this.block_, 'mutatorOpen', !visible, visible));
if (visible) {
// Create the bubble.
this.bubble_ = new Blockly.Bubble(
/** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
this.createEditor_(), this.block_.svgPath_, this.iconXY_, null, null);
var tree = this.workspace_.options.languageTree;
if (tree) {
this.workspace_.flyout_.init(this.workspace_);
this.workspace_.flyout_.show(tree.childNodes);
}
this.rootBlock_ = this.block_.decompose(this.workspace_);
var blocks = this.rootBlock_.getDescendants(false);
for (var i = 0, child; child = blocks[i]; i++) {
child.render();
}
// The root block should not be dragable or deletable.
this.rootBlock_.setMovable(false);
this.rootBlock_.setDeletable(false);
if (this.workspace_.flyout_) {
var margin = this.workspace_.flyout_.CORNER_RADIUS * 2;
var x = this.workspace_.flyout_.width_ + margin;
} else {
var margin = 16;
var x = margin;
}
if (this.block_.RTL) {
x = -x;
}
this.rootBlock_.moveBy(x, margin);
// Save the initial connections, then listen for further changes.
if (this.block_.saveConnections) {
var thisMutator = this;
this.block_.saveConnections(this.rootBlock_);
this.sourceListener_ = function() {
thisMutator.block_.saveConnections(thisMutator.rootBlock_);
};
this.block_.workspace.addChangeListener(this.sourceListener_);
}
this.resizeBubble_();
// When the mutator's workspace changes, update the source block.
this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));
this.updateColour();
} else {
// Dispose of the bubble.
this.svgDialog_ = null;
this.workspace_.dispose();
this.workspace_ = null;
this.rootBlock_ = null;
this.bubble_.dispose();
this.bubble_ = null;
this.workspaceWidth_ = 0;
this.workspaceHeight_ = 0;
if (this.sourceListener_) {
this.block_.workspace.removeChangeListener(this.sourceListener_);
this.sourceListener_ = null;
}
}
};
/**
* Update the source block when the mutator's blocks are changed.
* Bump down any block that's too high.
* Fired whenever a change is made to the mutator's workspace.
* @private
*/
Blockly.Mutator.prototype.workspaceChanged_ = function() {
if (!this.workspace_.isDragging()) {
var blocks = this.workspace_.getTopBlocks(false);
var MARGIN = 20;
for (var b = 0, block; block = blocks[b]; b++) {
var blockXY = block.getRelativeToSurfaceXY();
var blockHW = block.getHeightWidth();
if (blockXY.y + blockHW.height < MARGIN) {
// Bump any block that's above the top back inside.
block.moveBy(0, MARGIN - blockHW.height - blockXY.y);
}
}
}
// When the mutator's workspace changes, update the source block.
if (this.rootBlock_.workspace == this.workspace_) {
Blockly.Events.setGroup(true);
var block = this.block_;
var oldMutationDom = block.mutationToDom();
var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
// Switch off rendering while the source block is rebuilt.
var savedRendered = block.rendered;
block.rendered = false;
// Allow the source block to rebuild itself.
block.compose(this.rootBlock_);
// Restore rendering and show the changes.
block.rendered = savedRendered;
// Mutation may have added some elements that need initializing.
block.initSvg();
var newMutationDom = block.mutationToDom();
var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
if (oldMutation != newMutation) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
block, 'mutation', null, oldMutation, newMutation));
// Ensure that any bump is part of this mutation's event group.
var group = Blockly.Events.getGroup();
setTimeout(function() {
Blockly.Events.setGroup(group);
block.bumpNeighbours_();
Blockly.Events.setGroup(false);
}, Blockly.BUMP_DELAY);
}
if (block.rendered) {
block.render();
}
// Don't update the bubble until the drag has ended, to avoid moving blocks
// under the cursor.
if (!this.workspace_.isDragging()) {
this.resizeBubble_();
}
Blockly.Events.setGroup(false);
}
};
/**
* Return an object with all the metrics required to size scrollbars for the
* mutator flyout. The following properties are computed:
* .viewHeight: Height of the visible rectangle,
* .viewWidth: Width of the visible rectangle,
* .absoluteTop: Top-edge of view.
* .absoluteLeft: Left-edge of view.
* @return {!Object} Contains size and position metrics of mutator dialog's
* workspace.
* @private
*/
Blockly.Mutator.prototype.getFlyoutMetrics_ = function() {
return {
viewHeight: this.workspaceHeight_,
viewWidth: this.workspaceWidth_,
absoluteTop: 0,
absoluteLeft: 0
};
};
/**
* Dispose of this mutator.
*/
Blockly.Mutator.prototype.dispose = function() {
this.block_.mutator = null;
Blockly.Icon.prototype.dispose.call(this);
};
/**
* Reconnect an block to a mutated input.
* @param {Blockly.Connection} connectionChild Connection on child block.
* @param {!Blockly.Block} block Parent block.
* @param {string} inputName Name of input on parent block.
* @return {boolean} True iff a reconnection was made, false otherwise.
*/
Blockly.Mutator.reconnect = function(connectionChild, block, inputName) {
if (!connectionChild || !connectionChild.getSourceBlock().workspace) {
return false; // No connection or block has been deleted.
}
var connectionParent = block.getInput(inputName).connection;
var currentParent = connectionChild.targetBlock();
if ((!currentParent || currentParent == block) &&
connectionParent.targetConnection != connectionChild) {
if (connectionParent.isConnected()) {
// There's already something connected here. Get rid of it.
connectionParent.disconnect();
}
connectionParent.connect(connectionChild);
return true;
}
return false;
};
// Export symbols that would otherwise be renamed by Closure compiler.
if (!goog.global['Blockly']) {
goog.global['Blockly'] = {};
}
if (!goog.global['Blockly']['Mutator']) {
goog.global['Blockly']['Mutator'] = {};
}
goog.global['Blockly']['Mutator']['reconnect'] = Blockly.Mutator.reconnect;

View File

@@ -0,0 +1,198 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Utility functions for handling variables and procedure names.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Names');
/**
* Class for a database of entity names (variables, functions, etc).
* @param {string} reservedWords A comma-separated string of words that are
* illegal for use as names in a language (e.g. 'new,if,this,...').
* @param {string=} opt_variablePrefix Some languages need a '$' or a namespace
* before all variable names.
* @constructor
*/
Blockly.Names = function(reservedWords, opt_variablePrefix) {
this.variablePrefix_ = opt_variablePrefix || '';
this.reservedDict_ = Object.create(null);
if (reservedWords) {
var splitWords = reservedWords.split(',');
for (var i = 0; i < splitWords.length; i++) {
this.reservedDict_[splitWords[i]] = true;
}
}
this.reset();
};
/**
* Constant to separate developer variable names from user-defined variable
* names when running generators.
* A developer variable will be declared as a global in the generated code, but
* will never be shown to the user in the workspace or stored in the variable
* map.
*/
Blockly.Names.DEVELOPER_VARIABLE_TYPE = 'DEVELOPER_VARIABLE';
/**
* When JavaScript (or most other languages) is generated, variable 'foo' and
* procedure 'foo' would collide. However, Blockly has no such problems since
* variable get 'foo' and procedure call 'foo' are unambiguous.
* Therefore, Blockly keeps a separate type name to disambiguate.
* getName('foo', 'variable') -> 'foo'
* getName('foo', 'procedure') -> 'foo2'
*/
/**
* Empty the database and start from scratch. The reserved words are kept.
*/
Blockly.Names.prototype.reset = function() {
this.db_ = Object.create(null);
this.dbReverse_ = Object.create(null);
this.variableMap_ = null;
};
/**
* Set the variable map that maps from variable name to variable object.
* @param {!Blockly.VariableMap} map The map to track.
* @package
*/
Blockly.Names.prototype.setVariableMap = function(map) {
this.variableMap_ = map;
};
/**
* Get the name for a user-defined variable, based on its ID.
* This should only be used for variables of type Blockly.Variables.NAME_TYPE.
* @param {string} id The ID to look up in the variable map.
* @return {?string} The name of the referenced variable, or null if there was
* no variable map or the variable was not found in the map.
* @private
*/
Blockly.Names.prototype.getNameForUserVariable_ = function(id) {
if (!this.variableMap_) {
console.log('Deprecated call to Blockly.Names.prototype.getName without ' +
'defining a variable map. To fix, add the folowing code in your ' +
'generator\'s init() function:\n' +
'Blockly.YourGeneratorName.variableDB_.setVariableMap(' +
'workspace.getVariableMap());');
return null;
}
var variable = this.variableMap_.getVariableById(id);
if (variable) {
return variable.name;
} else {
return null;
}
};
/**
* Convert a Blockly entity name to a legal exportable entity name.
* @param {string} name The Blockly entity name (no constraints).
* @param {string} type The type of entity in Blockly
* ('VARIABLE', 'PROCEDURE', 'BUILTIN', etc...).
* @return {string} An entity name that is legal in the exported language.
*/
Blockly.Names.prototype.getName = function(name, type) {
if (type == Blockly.Variables.NAME_TYPE) {
var varName = this.getNameForUserVariable_(name);
if (varName) {
name = varName;
}
}
var normalized = name.toLowerCase() + '_' + type;
var isVarType = type == Blockly.Variables.NAME_TYPE ||
type == Blockly.Names.DEVELOPER_VARIABLE_TYPE;
var prefix = isVarType ? this.variablePrefix_ : '';
if (normalized in this.db_) {
return prefix + this.db_[normalized];
}
var safeName = this.getDistinctName(name, type);
this.db_[normalized] = safeName.substr(prefix.length);
return safeName;
};
/**
* Convert a Blockly entity name to a legal exportable entity name.
* Ensure that this is a new name not overlapping any previously defined name.
* Also check against list of reserved words for the current language and
* ensure name doesn't collide.
* @param {string} name The Blockly entity name (no constraints).
* @param {string} type The type of entity in Blockly
* ('VARIABLE', 'PROCEDURE', 'BUILTIN', etc...).
* @return {string} An entity name that is legal in the exported language.
*/
Blockly.Names.prototype.getDistinctName = function(name, type) {
var safeName = this.safeName_(name);
var i = '';
while (this.dbReverse_[safeName + i] ||
(safeName + i) in this.reservedDict_) {
// Collision with existing name. Create a unique name.
i = i ? i + 1 : 2;
}
safeName += i;
this.dbReverse_[safeName] = true;
var isVarType = type == Blockly.Variables.NAME_TYPE ||
type == Blockly.Names.DEVELOPER_VARIABLE_TYPE;
var prefix = isVarType ? this.variablePrefix_ : '';
return prefix + safeName;
};
/**
* Given a proposed entity name, generate a name that conforms to the
* [_A-Za-z][_A-Za-z0-9]* format that most languages consider legal for
* variables.
* @param {string} name Potentially illegal entity name.
* @return {string} Safe entity name.
* @private
*/
Blockly.Names.prototype.safeName_ = function(name) {
if (!name) {
name = 'unnamed';
} else {
// Unfortunately names in non-latin characters will look like
// _E9_9F_B3_E4_B9_90 which is pretty meaningless.
// https://github.com/google/blockly/issues/1654
name = encodeURI(name.replace(/ /g, '_')).replace(/[^\w]/g, '_');
// Most languages don't allow names with leading numbers.
if ('0123456789'.indexOf(name[0]) != -1) {
name = 'my_' + name;
}
}
return name;
};
/**
* Do the given two entity names refer to the same entity?
* Blockly names are case-insensitive.
* @param {string} name1 First name.
* @param {string} name2 Second name.
* @return {boolean} True if names are the same.
*/
Blockly.Names.equals = function(name1, name2) {
return name1.toLowerCase() == name2.toLowerCase();
};

View File

@@ -0,0 +1,244 @@
/**
* @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 Object that controls settings for the workspace.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.Options');
goog.require('Blockly.Colours');
/**
* Parse the user-specified options, using reasonable defaults where behaviour
* is unspecified.
* @param {!Object} options Dictionary of options. Specification:
* https://developers.google.com/blockly/guides/get-started/web#configuration
* @constructor
*/
Blockly.Options = function(options) {
var readOnly = !!options['readOnly'];
if (readOnly) {
var languageTree = null;
var hasCategories = false;
var hasTrashcan = false;
var hasCollapse = false;
var hasComments = false;
var hasDisable = false;
var hasSounds = false;
} else {
if (!options['toolbox'] && Blockly.Blocks.defaultToolbox) {
var oParser = new DOMParser();
var dom = oParser.parseFromString(Blockly.Blocks.defaultToolbox, 'text/xml');
options['toolbox'] = dom.documentElement;
}
var languageTree = Blockly.Options.parseToolboxTree(options['toolbox']);
var hasCategories = Boolean(languageTree &&
languageTree.getElementsByTagName('category').length);
var hasTrashcan = options['trashcan'];
if (hasTrashcan === undefined) {
hasTrashcan = false;
}
var hasCollapse = options['collapse'];
if (hasCollapse === undefined) {
hasCollapse = hasCategories;
}
var hasComments = options['comments'];
if (hasComments === undefined) {
hasComments = hasCategories;
}
var hasDisable = options['disable'];
if (hasDisable === undefined) {
hasDisable = hasCategories;
}
var hasSounds = options['sounds'];
if (hasSounds === undefined) {
hasSounds = true;
}
}
var rtl = !!options['rtl'];
var horizontalLayout = options['horizontalLayout'];
if (horizontalLayout === undefined) {
horizontalLayout = false;
}
var toolboxAtStart = options['toolboxPosition'];
if (toolboxAtStart === 'end') {
toolboxAtStart = false;
} else {
toolboxAtStart = true;
}
if (horizontalLayout) {
var toolboxPosition = toolboxAtStart ?
Blockly.TOOLBOX_AT_TOP : Blockly.TOOLBOX_AT_BOTTOM;
} else {
var toolboxPosition = (toolboxAtStart == rtl) ?
Blockly.TOOLBOX_AT_RIGHT : Blockly.TOOLBOX_AT_LEFT;
}
var hasScrollbars = options['scrollbars'];
if (hasScrollbars === undefined) {
hasScrollbars = hasCategories;
}
var hasCss = options['css'];
if (hasCss === undefined) {
hasCss = true;
}
var pathToMedia = 'https://blockly-demo.appspot.com/static/media/';
if (options['media']) {
pathToMedia = options['media'];
} else if (options['path']) {
// 'path' is a deprecated option which has been replaced by 'media'.
pathToMedia = options['path'] + 'media/';
}
if (options['oneBasedIndex'] === undefined) {
var oneBasedIndex = true;
} else {
var oneBasedIndex = !!options['oneBasedIndex'];
}
Blockly.Colours.overrideColours(options['colours']);
this.RTL = rtl;
this.oneBasedIndex = oneBasedIndex;
this.collapse = hasCollapse;
this.comments = hasComments;
this.disable = hasDisable;
this.readOnly = readOnly;
this.pathToMedia = pathToMedia;
this.hasCategories = hasCategories;
this.hasScrollbars = hasScrollbars;
this.hasTrashcan = hasTrashcan;
this.hasSounds = hasSounds;
this.hasCss = hasCss;
this.horizontalLayout = horizontalLayout;
this.languageTree = languageTree;
this.gridOptions = Blockly.Options.parseGridOptions_(options);
this.zoomOptions = Blockly.Options.parseZoomOptions_(options);
this.toolboxPosition = toolboxPosition;
};
/**
* The parent of the current workspace, or null if there is no parent workspace.
* @type {Blockly.Workspace}
**/
Blockly.Options.prototype.parentWorkspace = null;
/**
* If set, sets the translation of the workspace to match the scrollbars.
*/
Blockly.Options.prototype.setMetrics = null;
/**
* Return an object with the metrics required to size the workspace.
* @return {Object} Contains size and position metrics, or null.
*/
Blockly.Options.prototype.getMetrics = null;
/**
* Parse the user-specified zoom options, using reasonable defaults where
* behaviour is unspecified. See zoom documentation:
* https://developers.google.com/blockly/guides/configure/web/zoom
* @param {!Object} options Dictionary of options.
* @return {!Object} A dictionary of normalized options.
* @private
*/
Blockly.Options.parseZoomOptions_ = function(options) {
var zoom = options['zoom'] || {};
var zoomOptions = {};
if (zoom['controls'] === undefined) {
zoomOptions.controls = false;
} else {
zoomOptions.controls = !!zoom['controls'];
}
if (zoom['wheel'] === undefined) {
zoomOptions.wheel = false;
} else {
zoomOptions.wheel = !!zoom['wheel'];
}
if (zoom['startScale'] === undefined) {
zoomOptions.startScale = 1;
} else {
zoomOptions.startScale = parseFloat(zoom['startScale']);
}
if (zoom['maxScale'] === undefined) {
zoomOptions.maxScale = 3;
} else {
zoomOptions.maxScale = parseFloat(zoom['maxScale']);
}
if (zoom['minScale'] === undefined) {
zoomOptions.minScale = 0.3;
} else {
zoomOptions.minScale = parseFloat(zoom['minScale']);
}
if (zoom['scaleSpeed'] === undefined) {
zoomOptions.scaleSpeed = 1.2;
} else {
zoomOptions.scaleSpeed = parseFloat(zoom['scaleSpeed']);
}
return zoomOptions;
};
/**
* Parse the user-specified grid options, using reasonable defaults where
* behaviour is unspecified. See grid documentation:
* https://developers.google.com/blockly/guides/configure/web/grid
* @param {!Object} options Dictionary of options.
* @return {!Object} A dictionary of normalized options.
* @private
*/
Blockly.Options.parseGridOptions_ = function(options) {
var grid = options['grid'] || {};
var gridOptions = {};
gridOptions.spacing = parseFloat(grid['spacing']) || 0;
gridOptions.colour = grid['colour'] || '#888';
gridOptions.length = parseFloat(grid['length']) || 1;
gridOptions.snap = gridOptions.spacing > 0 && !!grid['snap'];
return gridOptions;
};
/**
* Parse the provided toolbox tree into a consistent DOM format.
* @param {Node|string} tree DOM tree of blocks, or text representation of same.
* @return {Node} DOM tree of blocks, or null.
*/
Blockly.Options.parseToolboxTree = function(tree) {
if (tree) {
if (typeof tree != 'string') {
if (typeof XSLTProcessor == 'undefined' && tree.outerHTML) {
// In this case the tree will not have been properly built by the
// browser. The HTML will be contained in the element, but it will
// not have the proper DOM structure since the browser doesn't support
// XSLTProcessor (XML -> HTML). This is the case in IE 9+.
tree = tree.outerHTML;
} else if (!(tree instanceof Element)) {
tree = null;
}
}
if (typeof tree == 'string') {
tree = Blockly.Xml.textToDom(tree);
}
} else {
tree = null;
}
return tree;
};

View File

@@ -0,0 +1,739 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Utility functions for handling procedures.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* @name Blockly.Procedures
* @namespace
**/
goog.provide('Blockly.Procedures');
goog.require('Blockly.Blocks');
goog.require('Blockly.constants');
goog.require('Blockly.Events.BlockChange');
goog.require('Blockly.Field');
goog.require('Blockly.Names');
goog.require('Blockly.Workspace');
/**
* Constant to separate procedure names from variables and generated functions
* when running generators.
* @deprecated Use Blockly.PROCEDURE_CATEGORY_NAME
*/
Blockly.Procedures.NAME_TYPE = Blockly.PROCEDURE_CATEGORY_NAME;
/**
* Find all user-created procedure definitions in a workspace.
* @param {!Blockly.Workspace} root Root workspace.
* @return {!Array.<!Array.<!Array>>} Pair of arrays, the
* first contains procedures without return variables, the second with.
* Each procedure is defined by a three-element list of name, parameter
* list, and return value boolean.
*/
Blockly.Procedures.allProcedures = function(root) {
var blocks = root.getAllBlocks();
var proceduresReturn = [];
var proceduresNoReturn = [];
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].getProcedureDef) {
var tuple = blocks[i].getProcedureDef();
if (tuple) {
if (tuple[2]) {
proceduresReturn.push(tuple);
} else {
proceduresNoReturn.push(tuple);
}
}
}
}
proceduresNoReturn.sort(Blockly.Procedures.procTupleComparator_);
proceduresReturn.sort(Blockly.Procedures.procTupleComparator_);
return [proceduresNoReturn, proceduresReturn];
};
/**
* Find all user-created procedure definition mutations in a workspace.
* @param {!Blockly.Workspace} root Root workspace.
* @return {!Array.<Element>} Array of mutation xml elements.
* @package
*/
Blockly.Procedures.allProcedureMutations = function(root) {
var blocks = root.getAllBlocks();
var mutations = [];
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].type == Blockly.PROCEDURES_PROTOTYPE_BLOCK_TYPE) {
var mutation = blocks[i].mutationToDom(/* opt_generateShadows */ true);
if (mutation) {
mutations.push(mutation);
}
}
}
return mutations;
};
/**
* Sorts an array of procedure definition mutations alphabetically.
* (Does not mutate the given array.)
* @param {!Array.<Element>} mutations Array of mutation xml elements.
* @return {!Array.<Element>} Sorted array of mutation xml elements.
* @private
*/
Blockly.Procedures.sortProcedureMutations_ = function(mutations) {
var newMutations = mutations.slice();
newMutations.sort(function(a, b) {
var procCodeA = a.getAttribute('proccode');
var procCodeB = b.getAttribute('proccode');
return Blockly.scratchBlocksUtils.compareStrings(procCodeA, procCodeB);
});
return newMutations;
};
/**
* Comparison function for case-insensitive sorting of the first element of
* a tuple.
* @param {!Array} ta First tuple.
* @param {!Array} tb Second tuple.
* @return {number} -1, 0, or 1 to signify greater than, equality, or less than.
* @private
*/
Blockly.Procedures.procTupleComparator_ = function(ta, tb) {
return Blockly.scratchBlocksUtils.compareStrings(ta[0], tb[0]);
};
/**
* Ensure two identically-named procedures don't exist.
* @param {string} name Proposed procedure name.
* @param {!Blockly.Block} block Block to disambiguate.
* @return {string} Non-colliding name.
*/
Blockly.Procedures.findLegalName = function(name, block) {
if (block.isInFlyout) {
// Flyouts can have multiple procedures called 'do something'.
return name;
}
while (!Blockly.Procedures.isLegalName_(name, block.workspace, block)) {
// Collision with another procedure.
var r = name.match(/^(.*?)(\d+)$/);
if (!r) {
name += '2';
} else {
name = r[1] + (parseInt(r[2], 10) + 1);
}
}
return name;
};
/**
* Does this procedure have a legal name? Illegal names include names of
* procedures already defined.
* @param {string} name The questionable name.
* @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
* @param {Blockly.Block=} opt_exclude Optional block to exclude from
* comparisons (one doesn't want to collide with oneself).
* @return {boolean} True if the name is legal.
* @private
*/
Blockly.Procedures.isLegalName_ = function(name, workspace, opt_exclude) {
return !Blockly.Procedures.isNameUsed(name, workspace, opt_exclude);
};
/**
* Return if the given name is already a procedure name.
* @param {string} name The questionable name.
* @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
* @param {Blockly.Block=} opt_exclude Optional block to exclude from
* comparisons (one doesn't want to collide with oneself).
* @return {boolean} True if the name is used, otherwise return false.
*/
Blockly.Procedures.isNameUsed = function(name, workspace, opt_exclude) {
var blocks = workspace.getAllBlocks();
// Iterate through every block and check the name.
for (var i = 0; i < blocks.length; i++) {
if (blocks[i] == opt_exclude) {
continue;
}
if (blocks[i].getProcedureDef) {
var procName = blocks[i].getProcedureDef();
if (Blockly.Names.equals(procName[0], name)) {
return false;
}
}
}
return true;
};
/**
* Rename a procedure. Called by the editable field.
* @param {string} name The proposed new name.
* @return {string} The accepted name.
* @this {Blockly.Field}
*/
Blockly.Procedures.rename = function(name) {
// Strip leading and trailing whitespace. Beyond this, all names are legal.
name = name.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
// Ensure two identically-named procedures don't exist.
var legalName = Blockly.Procedures.findLegalName(name, this.sourceBlock_);
var oldName = this.text_;
if (oldName != name && oldName != legalName) {
// Rename any callers.
var blocks = this.sourceBlock_.workspace.getAllBlocks();
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].renameProcedure) {
blocks[i].renameProcedure(oldName, legalName);
}
}
}
return legalName;
};
/**
* Construct the blocks required by the flyout for the procedure category.
* @param {!Blockly.Workspace} workspace The workspace contianing procedures.
* @return {!Array.<!Element>} Array of XML block elements.
*/
Blockly.Procedures.flyoutCategory = function(workspace) {
var xmlList = [];
Blockly.Procedures.addCreateButton_(workspace, xmlList);
// Create call blocks for each procedure defined in the workspace
var mutations = Blockly.Procedures.allProcedureMutations(workspace);
mutations = Blockly.Procedures.sortProcedureMutations_(mutations);
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i].cloneNode(false);
var procCode = mutation.getAttribute('proccode');
var returnType = Blockly.Procedures.getProcedureReturnType(procCode, workspace);
if (returnType !== Blockly.PROCEDURES_CALL_TYPE_STATEMENT) {
mutation.setAttribute('return', returnType);
}
// <block type="procedures_call">
// <mutation ...></mutation>
// </block>
var block = goog.dom.createDom('block');
block.setAttribute('type', 'procedures_call');
block.setAttribute('gap', 12);
block.appendChild(mutation);
xmlList.push(block);
}
var showReturn = (
Blockly.Procedures.DEFAULT_ENABLE_RETURNS ?
mutations.length > 0 :
workspace.procedureReturnsEnabled
);
if (showReturn) {
var returnBlock = goog.dom.createDom('block');
returnBlock.setAttribute('type', Blockly.PROCEDURES_RETURN_BLOCK_TYPE);
returnBlock.setAttribute('gap', 12);
var returnBlockValue = goog.dom.createDom('value');
returnBlockValue.setAttribute('name', 'VALUE');
var returnBlockShadow = goog.dom.createDom('shadow');
returnBlockShadow.setAttribute('type', 'text');
var returnBlockField = goog.dom.createDom('field');
returnBlockField.setAttribute('name', 'TEXT');
returnBlockShadow.appendChild(returnBlockField);
returnBlockValue.appendChild(returnBlockShadow);
returnBlock.appendChild(returnBlockValue);
xmlList.unshift(returnBlock);
var returnDocsButton = goog.dom.createDom('button');
returnDocsButton.setAttribute('callbackkey', 'OPEN_RETURN_DOCS');
returnDocsButton.setAttribute('text', Blockly.Msg.PROCEDURES_DOCS);
xmlList.unshift(returnDocsButton);
}
return xmlList;
};
/**
* Create the "Make a Block..." button.
* @param {!Blockly.Workspace} workspace The workspace contianing procedures.
* @param {!Array.<!Element>} xmlList Array of XML block elements to add to.
* @private
*/
Blockly.Procedures.addCreateButton_ = function(workspace, xmlList) {
var button = goog.dom.createDom('button');
var msg = Blockly.Msg.NEW_PROCEDURE;
var callbackKey = 'CREATE_PROCEDURE';
var callback = function() {
Blockly.Procedures.createProcedureDefCallback_(workspace);
};
button.setAttribute('text', msg);
button.setAttribute('callbackKey', callbackKey);
workspace.registerButtonCallback(callbackKey, callback);
xmlList.push(button);
};
/**
* Find all callers of a named procedure.
* @param {string} name Name of procedure (procCode in scratch-blocks).
* @param {!Blockly.Workspace} ws The workspace to find callers in.
* @param {!Blockly.Block} definitionRoot The root of the stack where the
* procedure is defined.
* @param {boolean} allowRecursive True if the search should include recursive
* procedure calls. False if the search should ignore the stack starting
* with definitionRoot.
* @return {!Array.<!Blockly.Block>} Array of caller blocks.
* @package
*/
Blockly.Procedures.getCallers = function(name, ws, definitionRoot,
allowRecursive) {
var allBlocks = [];
var topBlocks = ws.getTopBlocks();
// Start by deciding which stacks to investigate.
for (var i = 0; i < topBlocks.length; i++) {
var block = topBlocks[i];
if (block.id == definitionRoot.id && !allowRecursive) {
continue;
}
allBlocks.push.apply(allBlocks, block.getDescendants(false));
}
var callers = [];
for (var i = 0; i < allBlocks.length; i++) {
var block = allBlocks[i];
if (block.type == Blockly.PROCEDURES_CALL_BLOCK_TYPE ) {
var procCode = block.getProcCode();
if (procCode && procCode == name) {
callers.push(block);
}
}
}
return callers;
};
/**
* Find and edit all callers with a procCode using a new mutation.
* @param {string} name Name of procedure (procCode in scratch-blocks).
* @param {!Blockly.Workspace} ws The workspace to find callers in.
* @param {!Element} mutation New mutation for the callers.
* @package
*/
Blockly.Procedures.mutateCallersAndPrototype = function(name, ws, mutation) {
var defineBlock = Blockly.Procedures.getDefineBlock(name, ws);
var prototypeBlock = Blockly.Procedures.getPrototypeBlock(name, ws);
if (defineBlock && prototypeBlock) {
var callers = Blockly.Procedures.getCallers(name,
defineBlock.workspace, defineBlock, true /* allowRecursive */);
callers.push(prototypeBlock);
Blockly.Events.setGroup(true);
for (var i = 0, caller; caller = callers[i]; i++) {
var oldMutationDom = caller.mutationToDom();
var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
// Preserve the block's existing shape
var mutationToReplaceWith = mutation.cloneNode(false);
mutationToReplaceWith.setAttribute('return', oldMutationDom.getAttribute('return'));
caller.domToMutation(mutationToReplaceWith);
var newMutationDom = caller.mutationToDom();
var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
if (oldMutation != newMutation) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
caller, 'mutation', null, oldMutation, newMutation));
}
}
Blockly.Events.setGroup(false);
} else {
alert('No define block on workspace'); // TODO decide what to do about this.
}
};
/**
* Find the definition block for the named procedure.
* @param {string} procCode The identifier of the procedure.
* @param {!Blockly.Workspace} workspace The workspace to search.
* @return {Blockly.Block} The procedure definition block, or null not found.
* @package
*/
Blockly.Procedures.getDefineBlock = function(procCode, workspace) {
// Assume that a procedure definition is a top block.
var blocks = workspace.getTopBlocks(false);
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE) {
var prototypeBlock = blocks[i].getInput('custom_block').connection.targetBlock();
if (prototypeBlock.getProcCode && prototypeBlock.getProcCode() == procCode) {
return blocks[i];
}
}
}
return null;
};
/**
* Find the prototype block for the named procedure.
* @param {string} procCode The identifier of the procedure.
* @param {!Blockly.Workspace} workspace The workspace to search.
* @return {Blockly.Block} The procedure prototype block, or null not found.
* @package
*/
Blockly.Procedures.getPrototypeBlock = function(procCode, workspace) {
var defineBlock = Blockly.Procedures.getDefineBlock(procCode, workspace);
if (defineBlock) {
return defineBlock.getInput('custom_block').connection.targetBlock();
}
return null;
};
/**
* Create a mutation for a brand new custom procedure.
* @return {Element} The mutation for a new custom procedure
* @package
*/
Blockly.Procedures.newProcedureMutation = function() {
var mutationText = '<xml>' +
'<mutation' +
' proccode="' + Blockly.Msg['PROCEDURE_DEFAULT_NAME'] + '"' +
' argumentids="[]"' +
' argumentnames="[]"' +
' argumentdefaults="[]"' +
' warp="false">' +
'</mutation>' +
'</xml>';
return Blockly.Xml.textToDom(mutationText).firstChild;
};
/**
* Callback to create a new procedure custom command block.
* @param {!Blockly.Workspace} workspace The workspace to create the new procedure on.
* @private
*/
Blockly.Procedures.createProcedureDefCallback_ = function(workspace) {
Blockly.Procedures.externalProcedureDefCallback(
Blockly.Procedures.newProcedureMutation(),
Blockly.Procedures.createProcedureCallbackFactory_(workspace)
);
};
/**
* Callback factory for adding a new custom procedure from a mutation.
* @param {!Blockly.Workspace} workspace The workspace to create the new procedure on.
* @return {function(?Element)} callback for creating the new custom procedure.
* @private
*/
Blockly.Procedures.createProcedureCallbackFactory_ = function(workspace) {
return function(mutation) {
if (mutation) {
var blockText = '<xml>' +
'<block type="procedures_definition">' +
'<statement name="custom_block">' +
'<shadow type="procedures_prototype">' +
Blockly.Xml.domToText(mutation) +
'</shadow>' +
'</statement>' +
'</block>' +
'</xml>';
var blockDom = Blockly.Xml.textToDom(blockText).firstChild;
Blockly.Events.setGroup(true);
var block = Blockly.Xml.domToBlock(blockDom, workspace);
var scale = workspace.scale; // To convert from pixel units to workspace units
// Position the block so that it is at the top left of the visible workspace,
// padded from the edge by 30 units. Position in the top right if RTL.
var posX = -workspace.scrollX;
if (workspace.RTL) {
posX += workspace.getMetrics().contentWidth - 30;
} else {
posX += 30;
}
block.moveBy(posX / scale, (-workspace.scrollY + 30) / scale);
block.scheduleSnapAndBump();
Blockly.Events.setGroup(false);
}
};
};
/**
* Callback to open the modal for editing custom procedures.
* @param {!Blockly.Block} block The block that was right-clicked.
* @private
*/
Blockly.Procedures.editProcedureCallback_ = function(block) {
// Edit can come from one of three block types (call, define, prototype)
// Normalize by setting the block to the prototype block for the procedure.
if (block.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE) {
var input = block.getInput('custom_block');
if (!input) {
alert('Bad input'); // TODO: Decide what to do about this.
return;
}
var conn = input.connection;
if (!conn) {
alert('Bad connection'); // TODO: Decide what to do about this.
return;
}
var innerBlock = conn.targetBlock();
if (!innerBlock ||
!innerBlock.type == Blockly.PROCEDURES_PROTOTYPE_BLOCK_TYPE) {
alert('Bad inner block'); // TODO: Decide what to do about this.
return;
}
block = innerBlock;
} else if (block.type == Blockly.PROCEDURES_CALL_BLOCK_TYPE) {
// This is a call block, find the prototype corresponding to the procCode.
// Make sure to search the correct workspace, call block can be in flyout.
var workspaceToSearch = block.workspace.isFlyout ?
block.workspace.targetWorkspace : block.workspace;
block = Blockly.Procedures.getPrototypeBlock(
block.getProcCode(), workspaceToSearch);
}
// Block now refers to the procedure prototype block, it is safe to proceed.
Blockly.Procedures.externalProcedureDefCallback(
block.mutationToDom(),
Blockly.Procedures.editProcedureCallbackFactory_(block)
);
};
/**
* Callback factory for editing an existing custom procedure.
* @param {!Blockly.Block} block The procedure prototype block being edited.
* @return {function(?Element)} Callback for editing the custom procedure.
* @private
*/
Blockly.Procedures.editProcedureCallbackFactory_ = function(block) {
return function(mutation) {
if (mutation) {
Blockly.Procedures.mutateCallersAndPrototype(block.getProcCode(),
block.workspace, mutation);
}
};
};
/**
* Callback to create a new procedure custom command block.
* @public
*/
Blockly.Procedures.externalProcedureDefCallback = function(/** mutator, callback */) {
alert('External procedure editor must be override Blockly.Procedures.externalProcedureDefCallback');
};
/**
* Make a context menu option for editing a custom procedure.
* This appears in the context menu for procedure definitions and procedure
* calls.
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.Procedures.makeEditOption = function(block) {
var editOption = {
enabled: true,
text: Blockly.Msg.EDIT_PROCEDURE,
callback: function() {
Blockly.Procedures.editProcedureCallback_(block);
}
};
return editOption;
};
Blockly.Procedures.makeChangeTypeOption = function(block) {
var isStatement = block.getReturn() === Blockly.PROCEDURES_CALL_TYPE_STATEMENT;
var option = {
enabled: true,
text: isStatement ? Blockly.Msg.PROCEDURES_TO_REPORTER : Blockly.Msg.PROCEDURES_TO_STATEMENT,
callback: function() {
var newType;
if (isStatement) {
var procCode = block.getProcCode();
var workspace = block.workspace;
var actualReturnType = Blockly.Procedures.getProcedureReturnType(procCode, workspace);
// If the definition is boolean-shaped, then the reporter should be boolean-shaped,
// otherwise normal reporter shaped.
newType = (
actualReturnType === Blockly.PROCEDURES_CALL_TYPE_BOOLEAN ?
actualReturnType :
Blockly.PROCEDURES_CALL_TYPE_REPORTER
);
} else {
newType = Blockly.PROCEDURES_CALL_TYPE_STATEMENT;
}
Blockly.Events.setGroup(true);
try {
Blockly.Procedures.changeReturnType(block, newType);
} finally {
Blockly.Events.setGroup(false);
}
}
};
return option;
};
Blockly.Procedures.changeReturnType = function(block, returnType) {
block.unplug(true);
var workspace = block.workspace;
var xml = Blockly.Xml.blockToDom(block);
var xy = block.getRelativeToSurfaceXY();
block.dispose();
var mutation = xml.querySelector('mutation');
mutation.setAttribute('return', returnType);
var newBlock = Blockly.Xml.domToBlock(xml, workspace);
newBlock.moveBy(xy.x, xy.y);
};
/**
* Callback to show the procedure definition corresponding to a custom command
* block.
* TODO(#1136): Implement.
* @param {!Blockly.Block} block The block that was right-clicked.
* @private
*/
Blockly.Procedures.showProcedureDefCallback_ = function(block) {
alert('TODO(#1136): implement showing procedure definition (procCode was "' +
block.procCode_ + '")');
};
/**
* Make a context menu option for showing the definition for a custom procedure,
* based on a right-click on a custom command block.
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.Procedures.makeShowDefinitionOption = function(block) {
var option = {
enabled: true,
text: Blockly.Msg.SHOW_PROCEDURE_DEFINITION,
callback: function() {
Blockly.Procedures.showProcedureDefCallback_(block);
}
};
return option;
};
/**
* Callback to try to delete a custom block definitions.
* @param {string} procCode The identifier of the procedure to delete.
* @param {!Blockly.Block} definitionRoot The root block of the stack that
* defines the custom procedure.
* @return {boolean} True if the custom procedure was deleted, false otherwise.
* @package
*/
Blockly.Procedures.deleteProcedureDefCallback = function(procCode,
definitionRoot) {
var callers = Blockly.Procedures.getCallers(procCode,
definitionRoot.workspace, definitionRoot, false /* allowRecursive */);
if (callers.length > 0) {
return false;
}
var workspace = definitionRoot.workspace;
// Delete the whole stack.
Blockly.Events.setGroup(true);
definitionRoot.dispose();
Blockly.Events.setGroup(false);
// TODO (#1354) Update this function when '_' is removed
// Refresh toolbox, so caller doesn't appear there anymore
workspace.refreshToolboxSelection_();
return true;
};
/**
* If true, the user will be able to manually override the shape of procedure call blocks.
*/
Blockly.Procedures.USER_CAN_CHANGE_CALL_TYPE = true;
/**
* If false, a round procedure call reporter can be dropped into any input, including boolean ones.
*/
Blockly.Procedures.ENFORCE_TYPES = false;
/**
* If true, the return block will always be available. If false, either create a block that requires
* returns or call workspace.enableProcedureReturns() to enable return blocks.
*/
Blockly.Procedures.DEFAULT_ENABLE_RETURNS = false;
/**
* @param {string} procCode The procedure code
* @param {Blockly.Workspace} workspace The workspace
* @returns {number} The type of the return block
*/
Blockly.Procedures.getProcedureReturnType = function(procCode, workspace) {
var defineBlock = Blockly.Procedures.getDefineBlock(procCode, workspace);
if (!defineBlock) {
return Blockly.PROCEDURES_CALL_TYPE_STATEMENT;
}
return Blockly.Procedures.getBlockReturnType(defineBlock);
};
/**
* @param {Blockly.Workspace} workspace The workspace
* @returns {Record<string, number>} The return type of each procedure in the workspace.
*/
Blockly.Procedures.getAllProcedureReturnTypes = function(workspace) {
var result = Object.create(null);
var blocks = workspace.getTopBlocks(false);
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (block.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE && !block.isInsertionMarker()) {
var procCode = block.getInput('custom_block').connection.targetBlock().getProcCode();
// To match behavior of getDefineBlock, if multiple instances of this procedure are
// defined, only use the first one.
if (!Object.prototype.hasOwnProperty.call(result, procCode)) {
result[procCode] = Blockly.Procedures.getBlockReturnType(block);
}
}
}
return result;
};
/**
* @param {Blockly.Block} block The block
* @returns {number} The type of the return block
*/
Blockly.Procedures.getBlockReturnType = function(block) {
var hasSeenBooleanReturn = false;
/** @type {Blockly.Block[]} */
var descendants = block.getDescendants();
for (var i = 0; i < descendants.length; i++) {
if (descendants[i].type === Blockly.PROCEDURES_RETURN_BLOCK_TYPE) {
// The block at i + 1 should be the block inside of the return block.
// Even if the return block is missing its input, this will still be fine, because the
// next block should a stacked block which won't be hexagon-shaped.
if (i + 1 < descendants.length && descendants[i + 1].outputShape_ === Blockly.OUTPUT_SHAPE_HEXAGONAL) {
// keep searching, because there may be other, non-boolean returns in this function definition.
hasSeenBooleanReturn = true;
} else {
return Blockly.PROCEDURES_CALL_TYPE_REPORTER;
}
}
}
if (hasSeenBooleanReturn) {
return Blockly.PROCEDURES_CALL_TYPE_BOOLEAN;
} else {
return Blockly.PROCEDURES_CALL_TYPE_STATEMENT;
}
};

View File

@@ -0,0 +1,417 @@
/**
* @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 Components for creating connections between blocks.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.RenderedConnection');
goog.require('Blockly.Connection');
/**
* Class for a connection between blocks that may be rendered on screen.
* @param {!Blockly.Block} source The block establishing this connection.
* @param {number} type The type of the connection.
* @extends {Blockly.Connection}
* @constructor
*/
Blockly.RenderedConnection = function(source, type) {
Blockly.RenderedConnection.superClass_.constructor.call(this, source, type);
/**
* Workspace units, (0, 0) is top left of block.
* @type {!goog.math.Coordinate}
* @private
*/
this.offsetInBlock_ = new goog.math.Coordinate(0, 0);
};
goog.inherits(Blockly.RenderedConnection, Blockly.Connection);
/**
* Returns the distance between this connection and another connection in
* workspace units.
* @param {!Blockly.Connection} otherConnection The other connection to measure
* the distance to.
* @return {number} The distance between connections, in workspace units.
*/
Blockly.RenderedConnection.prototype.distanceFrom = function(otherConnection) {
var xDiff = this.x_ - otherConnection.x_;
var yDiff = this.y_ - otherConnection.y_;
return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
};
/**
* Move the block(s) belonging to the connection to a point where they don't
* visually interfere with the specified connection.
* @param {!Blockly.Connection} staticConnection The connection to move away
* from.
* @private
*/
Blockly.RenderedConnection.prototype.bumpAwayFrom_ = function(staticConnection) {
if (this.sourceBlock_.workspace.isDragging()) {
// Don't move blocks around while the user is doing the same.
return;
}
// Move the root block.
var rootBlock = this.sourceBlock_.getRootBlock();
if (rootBlock.isInFlyout) {
// Don't move blocks around in a flyout.
return;
}
var reverse = false;
if (!rootBlock.isMovable()) {
// Can't bump an uneditable block away.
// Check to see if the other block is movable.
rootBlock = staticConnection.getSourceBlock().getRootBlock();
if (!rootBlock.isMovable()) {
return;
}
// Swap the connections and move the 'static' connection instead.
staticConnection = this;
reverse = true;
}
// Raise it to the top for extra visibility.
var selected = Blockly.selected == rootBlock;
selected || rootBlock.addSelect();
var dx = (staticConnection.x_ + Blockly.SNAP_RADIUS) - this.x_;
var dy = (staticConnection.y_ + Blockly.SNAP_RADIUS) - this.y_;
if (reverse) {
// When reversing a bump due to an uneditable block, bump up.
dy = -dy;
}
if (rootBlock.RTL) {
dx = -dx;
}
rootBlock.moveBy(dx, dy);
selected || rootBlock.removeSelect();
};
/**
* Change the connection's coordinates.
* @param {number} x New absolute x coordinate, in workspace coordinates.
* @param {number} y New absolute y coordinate, in workspace coordinates.
*/
Blockly.RenderedConnection.prototype.moveTo = function(x, y) {
// Remove it from its old location in the database (if already present)
if (this.inDB_) {
this.db_.removeConnection_(this);
}
this.x_ = x;
this.y_ = y;
// Insert it into its new location in the database.
if (!this.hidden_) {
this.db_.addConnection(this);
}
};
/**
* Change the connection's coordinates.
* @param {number} dx Change to x coordinate, in workspace units.
* @param {number} dy Change to y coordinate, in workspace units.
*/
Blockly.RenderedConnection.prototype.moveBy = function(dx, dy) {
this.moveTo(this.x_ + dx, this.y_ + dy);
};
/**
* Move this connection to the location given by its offset within the block and
* the location of the block's top left corner.
* @param {!goog.math.Coordinate} blockTL The location of the top left corner
* of the block, in workspace coordinates.
*/
Blockly.RenderedConnection.prototype.moveToOffset = function(blockTL) {
this.moveTo(blockTL.x + this.offsetInBlock_.x,
blockTL.y + this.offsetInBlock_.y);
};
/**
* Set the offset of this connection relative to the top left of its block.
* @param {number} x The new relative x, in workspace units.
* @param {number} y The new relative y, in workspace units.
*/
Blockly.RenderedConnection.prototype.setOffsetInBlock = function(x, y) {
this.offsetInBlock_.x = x;
this.offsetInBlock_.y = y;
};
/**
* Move the blocks on either side of this connection right next to each other.
* @private
*/
Blockly.RenderedConnection.prototype.tighten_ = function() {
var dx = this.targetConnection.x_ - this.x_;
var dy = this.targetConnection.y_ - this.y_;
if (dx != 0 || dy != 0) {
var block = this.targetBlock();
var svgRoot = block.getSvgRoot();
if (!svgRoot) {
throw 'block is not rendered.';
}
// Workspace coordinates.
var xy = Blockly.utils.getRelativeXY(svgRoot);
block.getSvgRoot().setAttribute('transform',
'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')');
block.moveConnections_(-dx, -dy);
}
};
/**
* Find the closest compatible connection to this connection.
* All parameters are in workspace units.
* @param {number} maxLimit The maximum radius to another connection.
* @param {!goog.math.Coordinate} dxy Offset between this connection's location
* in the database and the current location (as a result of dragging).
* @return {!{connection: ?Blockly.Connection, radius: number}} Contains two
* properties: 'connection' which is either another connection or null,
* and 'radius' which is the distance.
*/
Blockly.RenderedConnection.prototype.closest = function(maxLimit, dxy) {
return this.dbOpposite_.searchForClosest(this, maxLimit, dxy);
};
/**
* Add highlighting around this connection.
*/
Blockly.RenderedConnection.prototype.highlight = function() {
var steps;
steps = 'm -20,0 h 5 ' + Blockly.BlockSvg.NOTCH_PATH_LEFT + ' h 5';
var xy = this.sourceBlock_.getRelativeToSurfaceXY();
var x = this.x_ - xy.x;
var y = this.y_ - xy.y;
Blockly.Connection.highlightedPath_ = Blockly.utils.createSvgElement(
'path',
{
'class': 'blocklyHighlightedConnectionPath',
'd': steps,
transform: 'translate(' + x + ',' + y + ')' +
(this.sourceBlock_.RTL ? ' scale(-1 1)' : '')
},
this.sourceBlock_.getSvgRoot());
};
/**
* Unhide this connection, as well as all down-stream connections on any block
* attached to this connection. This happens when a block is expanded.
* Also unhides down-stream comments.
* @return {!Array.<!Blockly.Block>} List of blocks to render.
*/
Blockly.RenderedConnection.prototype.unhideAll = function() {
this.setHidden(false);
// All blocks that need unhiding must be unhidden before any rendering takes
// place, since rendering requires knowing the dimensions of lower blocks.
// Also, since rendering a block renders all its parents, we only need to
// render the leaf nodes.
var renderList = [];
if (this.type != Blockly.INPUT_VALUE && this.type != Blockly.NEXT_STATEMENT) {
// Only spider down.
return renderList;
}
var block = this.targetBlock();
if (block) {
var connections;
if (block.isCollapsed()) {
// This block should only be partially revealed since it is collapsed.
connections = [];
block.outputConnection && connections.push(block.outputConnection);
block.nextConnection && connections.push(block.nextConnection);
block.previousConnection && connections.push(block.previousConnection);
} else {
// Show all connections of this block.
connections = block.getConnections_(true);
}
for (var i = 0; i < connections.length; i++) {
renderList.push.apply(renderList, connections[i].unhideAll());
}
if (!renderList.length) {
// Leaf block.
renderList[0] = block;
}
}
return renderList;
};
/**
* Remove the highlighting around this connection.
*/
Blockly.RenderedConnection.prototype.unhighlight = function() {
goog.dom.removeNode(Blockly.Connection.highlightedPath_);
delete Blockly.Connection.highlightedPath_;
};
/**
* Set whether this connections is hidden (not tracked in a database) or not.
* @param {boolean} hidden True if connection is hidden.
*/
Blockly.RenderedConnection.prototype.setHidden = function(hidden) {
this.hidden_ = hidden;
if (hidden && this.inDB_) {
this.db_.removeConnection_(this);
} else if (!hidden && !this.inDB_) {
this.db_.addConnection(this);
}
};
/**
* Hide this connection, as well as all down-stream connections on any block
* attached to this connection. This happens when a block is collapsed.
* Also hides down-stream comments.
*/
Blockly.RenderedConnection.prototype.hideAll = function() {
this.setHidden(true);
if (this.targetConnection) {
var blocks = this.targetBlock().getDescendants(false);
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
// Hide all connections of all children.
var connections = block.getConnections_(true);
for (var j = 0; j < connections.length; j++) {
connections[j].setHidden(true);
}
// Close all bubbles of all children.
var icons = block.getIcons();
for (var j = 0; j < icons.length; j++) {
icons[j].setVisible(false);
}
}
}
};
/**
* Check if the two connections can be dragged to connect to each other.
* @param {!Blockly.Connection} candidate A nearby connection to check.
* @param {number} maxRadius The maximum radius allowed for connections, in
* workspace units.
* @return {boolean} True if the connection is allowed, false otherwise.
*/
Blockly.RenderedConnection.prototype.isConnectionAllowed = function(candidate,
maxRadius) {
if (this.distanceFrom(candidate) > maxRadius) {
return false;
}
return Blockly.RenderedConnection.superClass_.isConnectionAllowed.call(this,
candidate);
};
/**
* Disconnect two blocks that are connected by this connection.
* @param {!Blockly.Block} parentBlock The superior block.
* @param {!Blockly.Block} childBlock The inferior block.
* @private
*/
Blockly.RenderedConnection.prototype.disconnectInternal_ = function(parentBlock,
childBlock) {
Blockly.RenderedConnection.superClass_.disconnectInternal_.call(this,
parentBlock, childBlock);
// Rerender the parent so that it may reflow.
if (parentBlock.rendered) {
parentBlock.render();
}
if (childBlock.rendered) {
childBlock.updateDisabled();
childBlock.render();
}
};
/**
* Respawn the shadow block if there was one connected to the this connection.
* Render/rerender blocks as needed.
* @private
*/
Blockly.RenderedConnection.prototype.respawnShadow_ = function() {
var parentBlock = this.getSourceBlock();
// Respawn the shadow block if there is one.
var shadow = this.getShadowDom();
if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) {
Blockly.RenderedConnection.superClass_.respawnShadow_.call(this);
var blockShadow = this.targetBlock();
if (!blockShadow) {
throw 'Couldn\'t respawn the shadow block that should exist here.';
}
blockShadow.initSvg();
blockShadow.render(false);
if (parentBlock.rendered) {
parentBlock.render();
}
}
};
/**
* Find all nearby compatible connections to this connection.
* Type checking does not apply, since this function is used for bumping.
* @param {number} maxLimit The maximum radius to another connection, in
* workspace units.
* @return {!Array.<!Blockly.Connection>} List of connections.
* @private
*/
Blockly.RenderedConnection.prototype.neighbours_ = function(maxLimit) {
return this.dbOpposite_.getNeighbours(this, maxLimit);
};
/**
* Connect two connections together. This is the connection on the superior
* block. Rerender blocks as needed.
* @param {!Blockly.Connection} childConnection Connection on inferior block.
* @private
*/
Blockly.RenderedConnection.prototype.connect_ = function(childConnection) {
Blockly.RenderedConnection.superClass_.connect_.call(this, childConnection);
var parentConnection = this;
var parentBlock = parentConnection.getSourceBlock();
var childBlock = childConnection.getSourceBlock();
if (parentBlock.rendered) {
parentBlock.updateDisabled();
}
if (childBlock.rendered) {
childBlock.updateDisabled();
}
if (parentBlock.rendered && childBlock.rendered) {
if (parentConnection.type == Blockly.NEXT_STATEMENT ||
parentConnection.type == Blockly.PREVIOUS_STATEMENT) {
// Child block may need to square off its corners if it is in a stack.
// Rendering a child will render its parent.
childBlock.render();
} else {
// Child block does not change shape. Rendering the parent node will
// move its connected children into position.
parentBlock.render();
}
}
};
/**
* Function to be called when this connection's compatible types have changed.
* @private
*/
Blockly.RenderedConnection.prototype.onCheckChanged_ = function() {
// The new value type may not be compatible with the existing connection.
if (this.isConnected() && !this.checkType_(this.targetConnection)) {
var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
child.unplug();
// Bump away.
this.sourceBlock_.bumpNeighbours_();
}
};

View File

@@ -0,0 +1,646 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Object representing a code comment.
* @author kchadha@scratch.mit.edu (Karishma Chadha)
*/
'use strict';
goog.provide('Blockly.ScratchBlockComment');
goog.require('Blockly.Comment');
goog.require('Blockly.Events.BlockChange');
goog.require('Blockly.Events.Ui');
goog.require('Blockly.Icon');
goog.require('Blockly.ScratchBubble');
goog.require('goog.math.Coordinate');
goog.require('goog.userAgent');
/**
* Class for a comment.
* @param {!Blockly.Block} block The block associated with this comment.
* @param {string} text The text content of this comment.
* @param {string=} id Optional uid for comment; a new one will be generated if
* not provided.
* @param {number=} x Initial x position for comment, in workspace coordinates.
* @param {number=} y Initial y position for comment, in workspace coordinates.
* @param {boolean=} minimized Whether or not this comment is minimized
* (only the top bar displays), defaults to false.
* @extends {Blockly.Comment}
* @constructor
*/
Blockly.ScratchBlockComment = function(block, text, id, x, y, minimized) {
Blockly.ScratchBlockComment.superClass_.constructor.call(this, block);
/**
* The text content of this comment.
* @type {string}
* @private
*/
this.text_ = text;
var xIsValidNumber = typeof x == 'number' && !isNaN(x);
var yIsValidNumber = typeof y == 'number' && !isNaN(y);
/**
* Whether this comment needs to be auto-positioned (based on provided values
* for x and y position).
* @type {boolean}
* @private
*/
this.needsAutoPositioning_ = !xIsValidNumber && !yIsValidNumber;
// If both of the given x and y params are invalid, this comment needs to be auto positioned.
/**
* The x position of this comment in workspace coordinates. Default to 0 if
* x position is not provided or is not a valid number.
* @type {number}
* @private
*/
this.x_ = xIsValidNumber ? x : 0;
/**
* The y position of this comment in workspace coordinates. Default to 0 if
* y position is not provided or is not a valid number.
* @type {number}
* @private
*/
this.y_ = yIsValidNumber ? y : 0;
/**
* Whether this comment is minimized.
* @type {boolean}
* @private
*/
this.isMinimized_ = minimized || false;
/**
* The workspace this comment belongs to.
* @type {Blockly.Workspace}
* @package
*/
this.workspace = block.workspace;
/**
* The unique identifier for this comment.
* @type {string}
* @package
*/
this.id = goog.isString(id) && !this.workspace.getCommentById(id) ?
id : Blockly.utils.genUid();
this.workspace.addTopComment(this);
/**
* The id of the block this comment belongs to.
* @type {string}
* @package
*/
this.blockId = block.id;
if (!block.rendered) {
Blockly.ScratchBlockComment.fireCreateEvent(this);
}
// If the block is rendered, fire event the create event when the comment is made
// visible
};
goog.inherits(Blockly.ScratchBlockComment, Blockly.Comment);
/**
* Width of bubble.
* @private
*/
Blockly.ScratchBlockComment.prototype.width_ = 200;
/**
* Height of bubble.
* @private
*/
Blockly.ScratchBlockComment.prototype.height_ = 200;
/**
* Comment Icon Size.
* @package
*/
Blockly.ScratchBlockComment.prototype.SIZE = 0;
/**
* Offset for text area in comment bubble.
* @private
*/
Blockly.ScratchBlockComment.TEXTAREA_OFFSET = 12;
/**
* Maximum lable length (actual label length will include
* one additional character, the ellipsis).
* @private
*/
Blockly.ScratchBlockComment.MAX_LABEL_LENGTH = 12;
/**
* Width that a minimized comment should have.
* @private
*/
Blockly.ScratchBlockComment.MINIMIZE_WIDTH = 200;
/**
* Draw the comment icon.
* @param {!Element} _group The icon group.
* @private
*/
Blockly.ScratchBlockComment.prototype.drawIcon_ = function(_group) {
// NO-OP -- Don't render a comment icon for Scratch block comments
};
// Override renderIcon from Blocky.Icon so that the comment bubble is
// anchored correctly on the block. This function takes in the top margin
// as an input instead of setting an arbitrary one.
/**
* Render the icon.
* @param {number} cursorX Horizontal offset at which to position the icon.
* @param {number} topMargin Vertical offset from the top of the block to position the icon.
* @return {number} Horizontal offset for next item to draw.
* @package
*/
Blockly.ScratchBlockComment.prototype.renderIcon = function(cursorX, topMargin) {
if (this.collapseHidden && this.block_.isCollapsed()) {
this.iconGroup_.setAttribute('display', 'none');
return cursorX;
}
this.iconGroup_.setAttribute('display', 'block');
var width = this.SIZE;
if (this.block_.RTL) {
cursorX -= width;
}
this.iconGroup_.setAttribute('transform',
'translate(' + cursorX + ',' + topMargin + ')');
this.computeIconLocation();
if (this.block_.RTL) {
cursorX -= Blockly.BlockSvg.SEP_SPACE_X;
} else {
cursorX += width + Blockly.BlockSvg.SEP_SPACE_X;
}
return cursorX;
};
/**
* Create the editor for the comment's bubble.
* @return {{commentEditor: !Element, labelText: !string}} The components used
* to render the comment editing/writing area and the truncated label text
* to display in the minimized comment top bar.
* @private
*/
Blockly.ScratchBlockComment.prototype.createEditor_ = function() {
this.foreignObject_ = Blockly.utils.createSvgElement('foreignObject',
{
'x': Blockly.ScratchBubble.BORDER_WIDTH,
'y': Blockly.ScratchBubble.BORDER_WIDTH + Blockly.ScratchBubble.TOP_BAR_HEIGHT,
'class': 'scratchCommentForeignObject'
},
null);
var body = document.createElementNS(Blockly.HTML_NS, 'body');
body.setAttribute('xmlns', Blockly.HTML_NS);
body.className = 'blocklyMinimalBody scratchCommentBody';
var textarea = document.createElementNS(Blockly.HTML_NS, 'textarea');
textarea.className = 'scratchCommentTextarea scratchCommentText';
textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
textarea.setAttribute('placeholder', Blockly.Msg.WORKSPACE_COMMENT_DEFAULT_TEXT);
body.appendChild(textarea);
this.textarea_ = textarea;
this.textarea_.style.margin = (Blockly.ScratchBlockComment.TEXTAREA_OFFSET) + 'px';
this.foreignObject_.appendChild(body);
Blockly.bindEventWithChecks_(textarea, 'mousedown', this,
this.textareaFocus_, true, true); // noCapture and do not prevent default
// Don't zoom with mousewheel.
Blockly.bindEventWithChecks_(textarea, 'wheel', this, function(e) {
if (!e.ctrlKey && textarea.clientHeight !== textarea.scrollHeight) {
e.stopPropagation();
}
});
Blockly.bindEventWithChecks_(textarea, 'change', this, function(_e) {
if (this.text_ != textarea.value) {
Blockly.Events.fire(new Blockly.Events.CommentChange(
this, {text: this.text_}, {text: textarea.value}));
this.text_ = textarea.value;
}
});
// Label for comment top bar when comment is minimized
this.label_ = this.getLabelText();
return {
commentEditor: this.foreignObject_,
labelText: this.label_
};
};
/**
* Handle text area click, make sure to stop propagation to allow default selection behavior.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.ScratchBlockComment.prototype.textareaFocus_ = function(e) {
// Stop event from propagating to the workspace to make sure preventDefault _is not called_.
e.stopPropagation();
};
/**
* Callback function triggered when the bubble has resized.
* Resize the text area accordingly.
* @private
*/
Blockly.ScratchBlockComment.prototype.resizeBubble_ = function() {
if (this.isVisible() && !this.isMinimized_) {
var size = this.bubble_.getBubbleSize();
var doubleBorderWidth = 2 * Blockly.ScratchBubble.BORDER_WIDTH;
var textOffset = Blockly.ScratchBlockComment.TEXTAREA_OFFSET * 2;
this.foreignObject_.setAttribute('width', size.width - doubleBorderWidth);
this.foreignObject_.setAttribute('height', size.height - doubleBorderWidth - Blockly.ScratchBubble.TOP_BAR_HEIGHT);
this.textarea_.style.width = (size.width - textOffset) + 'px';
this.textarea_.style.height = (size.height - doubleBorderWidth -
Blockly.ScratchBubble.TOP_BAR_HEIGHT - textOffset) + 'px';
// Actually set the size!
this.width_ = size.width;
this.height_ = size.height;
}
};
/**
* Change the colour of the associated bubble to match its block.
* @package
*/
Blockly.ScratchBlockComment.prototype.updateColour = function() {
if (this.isVisible()) {
this.bubble_.setColour(this.block_.getColourTertiary());
}
};
/**
* Auto position this comment given information about the block that owns this
* comment and the comment state, if this block needs auto positioning.
* @private
*/
Blockly.ScratchBlockComment.prototype.autoPosition_ = function() {
if (!this.needsAutoPositioning_) return;
if (this.isMinimized_) {
var minimizedOffset = 4 * Blockly.BlockSvg.GRID_UNIT;
this.x_ = this.block_.RTL ?
this.iconXY_.x - this.getBubbleSize().width - minimizedOffset :
this.iconXY_.x + minimizedOffset;
this.y_ = this.iconXY_.y - (Blockly.ScratchBubble.TOP_BAR_HEIGHT / 2);
} else {
// TW: We remove comment overhang entirely. We've found that it tends
// to put comments very far away from their target block or even
// completely offscreen far too often. Users end up having to move
// the comment anyways, so let's do them a favor and not make them
// scroll to find the comment.
var overhang = 0;
var offset = 8 * Blockly.BlockSvg.GRID_UNIT;
this.x_ = this.block_.RTL ?
this.iconXY_.x - this.width_ - overhang - offset :
this.iconXY_.x + overhang + offset;
this.y_ = this.iconXY_.y - (Blockly.ScratchBubble.TOP_BAR_HEIGHT / 2);
}
};
/**
* Show or hide the comment bubble.
* @param {boolean} visible True if the bubble should be visible.
* @package
*/
Blockly.ScratchBlockComment.prototype.setVisible = function(visible) {
if (visible == this.isVisible()) {
// No change.
return;
}
if ((!this.block_.isEditable() && !this.textarea_) || goog.userAgent.IE) {
// Steal the code from warnings to make an uneditable text bubble.
// MSIE does not support foreignobject; textareas are impossible.
// http://msdn.microsoft.com/en-us/library/hh834675%28v=vs.85%29.aspx
// Always treat comments in IE as uneditable.
Blockly.Warning.prototype.setVisible.call(this, visible);
return;
}
// Save the bubble stats before the visibility switch.
var text = this.getText();
var size = this.getBubbleSize();
if (visible) {
// Auto position this comment, if necessary.
if (this.needsAutoPositioning_) {
this.autoPosition_();
// This comment has been auto-positioned so reset the flag
this.needsAutoPositioning_ = false;
}
// Create the bubble.
this.bubble_ = new Blockly.ScratchBubble(
this, /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
this.createEditor_(), this.iconXY_, this.width_, this.height_,
this.x_, this.y_, this.isMinimized_);
this.bubble_.setAutoLayout(false);
this.bubble_.registerResizeEvent(this.resizeBubble_.bind(this));
this.bubble_.registerMinimizeToggleEvent(this.toggleMinimize_.bind(this));
this.bubble_.registerDeleteEvent(this.dispose.bind(this));
this.bubble_.registerContextMenuCallback(this.showContextMenu_.bind(this));
this.updateColour();
} else {
// Dispose of the bubble.
this.bubble_.dispose();
this.bubble_ = null;
this.textarea_ = null;
this.foreignObject_ = null;
this.label_ = null;
}
// Restore the bubble stats after the visibility switch.
this.setText(text);
this.setBubbleSize(size.width, size.height);
if (visible) {
Blockly.ScratchBlockComment.fireCreateEvent(this);
}
};
/**
* Toggle the minimization state of this comment.
* @private
*/
Blockly.ScratchBlockComment.prototype.toggleMinimize_ = function() {
this.setMinimized(!this.isMinimized_);
};
/**
* Set the minimized state for this comment.
* @param {boolean} minimize Whether the comment should be minimized
* @package
*/
Blockly.ScratchBlockComment.prototype.setMinimized = function(minimize) {
if (this.isMinimized_ == minimize) {
return;
}
Blockly.Events.fire(new Blockly.Events.CommentChange(this,
{minimized: this.isMinimized_}, {minimized: minimize}));
this.isMinimized_ = minimize;
if (minimize) {
this.bubble_.setMinimized(true, this.getLabelText());
this.setBubbleSize(Blockly.ScratchBlockComment.MINIMIZE_WIDTH,
Blockly.ScratchBubble.TOP_BAR_HEIGHT);
// Note we are not updating this.width_ or this.height_ here
// because we want to keep track of the width/height of the
// maximized comment
} else {
this.bubble_.setMinimized(false);
this.setText(this.text_);
this.setBubbleSize(this.width_, this.height_);
}
};
/**
* Size this comment's bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
* @package
*/
Blockly.ScratchBlockComment.prototype.setBubbleSize = function(width, height) {
if (this.bubble_) {
if (this.isMinimized_) {
this.bubble_.setBubbleSize(Blockly.ScratchBlockComment.MINIMIZE_WIDTH,
Blockly.ScratchBubble.TOP_BAR_HEIGHT);
} else {
this.bubble_.setBubbleSize(width, height);
}
}
};
/**
* Set the un-minimized size of this comment. If the comment has an un-minimized
* bubble, also set the bubble's size.
* @param {number} width Width of the unminimized comment.
* @param {number} height Height of the unminimized comment.
* @package
*/
Blockly.ScratchBlockComment.prototype.setSize = function(width, height) {
var oldWidth = this.width_;
var oldHeight = this.height_;
if (!this.isMinimized_) {
this.setBubbleSize(width, height);
}
this.height_ = height;
this.width_ = width;
if (oldWidth != this.width_ || oldHeight != this.height_) {
Blockly.Events.fire(new Blockly.Events.CommentChange(
this,
{width: oldWidth, height: oldHeight},
{width: this.width_, height: this.height_}));
}
};
/**
* Get the truncated text for this comment to display in the minimized
* top bar.
* @return {string} The truncated comment text
* @package
*/
Blockly.ScratchBlockComment.prototype.getLabelText = function() {
if (this.text_.length > Blockly.ScratchBlockComment.MAX_LABEL_LENGTH) {
if (this.block_.RTL) {
return '\u2026' + this.text_.slice(0, Blockly.ScratchBlockComment.MAX_LABEL_LENGTH);
}
return this.text_.slice(0, Blockly.ScratchBlockComment.MAX_LABEL_LENGTH) + '\u2026';
} else {
return this.text_;
}
};
/**
* Set this comment's text.
* @param {string} text Comment text.
* @package
*/
Blockly.ScratchBlockComment.prototype.setText = function(text) {
if (this.text_ != text) {
Blockly.Events.fire(new Blockly.Events.CommentChange(
this, {text: this.text_}, {text: text}));
this.text_ = text;
}
if (this.textarea_) {
this.textarea_.value = text;
}
};
/**
* Move this comment to a position given x and y coordinates.
* @param {number} x The x-coordinate on the workspace.
* @param {number} y The y-coordinate on the workspace.
* @package
*/
Blockly.ScratchBlockComment.prototype.moveTo = function(x, y) {
var event = new Blockly.Events.CommentMove(this);
if (this.bubble_) {
this.bubble_.moveTo(x, y);
}
this.x_ = x;
this.y_ = y;
event.recordNew();
Blockly.Events.fire(event);
};
/**
* Get the x and y position of this comment.
* @return {goog.math.Coordinate} The XY position
* @package
*/
Blockly.ScratchBlockComment.prototype.getXY = function() {
if (this.bubble_) {
return this.bubble_.getRelativeToSurfaceXY();
}
// Auto position this comment if iconXY_ is provided
// (auto positioning will only occur if it is necessary).
if (this.needsAutoPositioning_ && this.iconXY_) {
this.autoPosition_();
// Do not reset the needsAutoPositioning flag here. This will be reset
// after the comment has been made visible and the re-auto positioned,
// because the block may have moved by that point.
}
return new goog.math.Coordinate(this.x_, this.y_);
};
/**
* Get the height and width of this comment.
* Note: this does not use the current bubble size because
* the bubble may be minimized.
* @return {{height: number, width: number}} The height and width of
* this comment when it is full size. These numbers do not change
* as the workspace zoom changes.
* @package
*/
Blockly.ScratchBlockComment.prototype.getHeightWidth = function() {
return {height: this.height_, width: this.width_};
};
/**
* Returns the coordinates of a bounding box describing the dimensions of this
* comment.
* Coordinate system: workspace coordinates.
* @return {!{topLeft: goog.math.Coordinate, bottomRight: goog.math.Coordinate}}
* Object with top left and bottom right coordinates of the bounding box.
* @package
*/
Blockly.ScratchBlockComment.prototype.getBoundingRectangle = function() {
var commentXY = this.getXY();
var commentBounds = this.getBubbleSize();
var topLeft;
var bottomRight;
if (this.workspace.RTL) {
// TODO (#1562) for some reason this doesn't work with workspace scroll in RTL
topLeft = new goog.math.Coordinate(commentXY.x - commentBounds.width,
commentXY.y);
bottomRight = new goog.math.Coordinate(commentXY.x,
commentXY.y + commentBounds.height);
} else {
topLeft = new goog.math.Coordinate(commentXY.x, commentXY.y);
bottomRight = new goog.math.Coordinate(commentXY.x + commentBounds.width,
commentXY.y + commentBounds.height);
}
return {topLeft: topLeft, bottomRight: bottomRight};
};
/**
* Check whether this comment is currently minimized.
* @return {boolean} True if minimized
* @package
*/
Blockly.ScratchBlockComment.prototype.isMinimized = function() {
return this.isMinimized_;
};
/**
* Show the context menu for this comment's bubble.
* @param {!Event} e The mouse event
* @private
*/
Blockly.ScratchBlockComment.prototype.showContextMenu_ = function(e) {
var menuOptions = [];
menuOptions.push(Blockly.ContextMenu.commentDeleteOption(this, Blockly.Msg.DELETE));
Blockly.ContextMenu.show(e, menuOptions, this.block_.RTL);
};
/**
* Encode a comment subtree as XML with XY coordinates.
* @param {boolean=} opt_noId True if the encoder should skip the comment id.
* @return {!Element} Tree of XML elements.
* @package
*/
Blockly.ScratchBlockComment.prototype.toXmlWithXY = function() {
var element = goog.dom.createDom('comment');
element.setAttribute('id', this.id);
element.textContent = this.text_;
element.setAttribute('x', Math.round(
this.workspace.RTL ? this.workspace.getWidth() - this.x_ : this.x_));
element.setAttribute('y', Math.round(this.y_));
element.setAttribute('h', this.height_);
element.setAttribute('w', this.width_);
return element;
};
/**
* Fire a create event for the given workspace comment, if comments are enabled.
* @param {!Blockly.WorkspaceComment} comment The comment that was just created.
* @package
*/
Blockly.ScratchBlockComment.fireCreateEvent = function(comment) {
if (Blockly.Events.isEnabled()) {
var existingGroup = Blockly.Events.getGroup();
if (!existingGroup) {
Blockly.Events.setGroup(true);
}
try {
Blockly.Events.fire(new Blockly.Events.CommentCreate(comment));
} finally {
if (!existingGroup) {
Blockly.Events.setGroup(false);
}
}
}
};
/**
* Dispose of this comment.
*/
Blockly.ScratchBlockComment.prototype.dispose = function() {
if (Blockly.Events.isEnabled()) {
// Emit delete event before disposal begins so that the
// event's reference to this comment contains all the relevant
// information (for undoing this event)
Blockly.Events.fire(new Blockly.Events.CommentDelete(this));
}
this.block_.comment = null;
this.workspace.removeTopComment(this);
Blockly.Icon.prototype.dispose.call(this);
};
/**
* Focus this comments textarea.
*/
Blockly.ScratchBlockComment.prototype.focus = function() {
this.textarea_.focus();
};

View File

@@ -0,0 +1,229 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Utility methods for Scratch Blocks but not Blockly.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
/**
* @name Blockly.scratchBlocksUtils
* @namespace
**/
goog.provide('Blockly.scratchBlocksUtils');
/**
* Measure some text using a canvas in-memory.
* Does not exist in Blockly, but needed in scratch-blocks
* @param {string} fontSize E.g., '10pt'
* @param {string} fontFamily E.g., 'Arial'
* @param {string} fontWeight E.g., '600'
* @param {string} text The actual text to measure
* @return {number} Width of the text in px.
* @package
*/
Blockly.scratchBlocksUtils.measureText = function(fontSize, fontFamily,
fontWeight, text) {
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
context.font = fontWeight + ' ' + fontSize + ' ' + fontFamily;
return context.measureText(text).width;
};
/**
* Re-assign obscured shadow blocks new IDs to prevent collisions
* Scratch specific to help the VM handle deleting obscured shadows.
* @param {Blockly.Block} block the root block to be processed.
* @package
*/
Blockly.scratchBlocksUtils.changeObscuredShadowIds = function(block) {
var blocks = block.getDescendants(false);
for (var i = blocks.length - 1; i >= 0; i--) {
var descendant = blocks[i];
for (var j = 0; j < descendant.inputList.length; j++) {
var connection = descendant.inputList[j].connection;
if (connection) {
var shadowDom = connection.getShadowDom();
if (shadowDom) {
shadowDom.setAttribute('id', Blockly.utils.genUid());
connection.setShadowDom(shadowDom);
}
}
}
}
};
/**
* Whether a block is both a shadow block and an argument reporter. These
* blocks have special behaviour in scratch-blocks: they're duplicated when
* dragged, and they are rendered slightly differently from normal shadow
* blocks.
* @param {!Blockly.BlockSvg} block The block that should be used to make this
* decision.
* @return {boolean} True if the block should be duplicated on drag.
* @package
*/
Blockly.scratchBlocksUtils.isShadowArgumentReporter = function(block) {
return (block.isShadow() && (block.type == 'argument_reporter_boolean' ||
block.type == 'argument_reporter_string_number'));
};
/**
* Compare strings with natural number sorting.
* @param {string} str1 First input.
* @param {string} str2 Second input.
* @return {number} -1, 0, or 1 to signify greater than, equality, or less than.
*/
Blockly.scratchBlocksUtils.compareStrings = function(str1, str2) {
return str1.localeCompare(str2, [], {
sensitivity: 'base',
numeric: true
});
};
/**
* Determine if this block can be recycled in the flyout. Blocks that have no
* variablees and are not dynamic shadows can be recycled.
* @param {Blockly.Block} block The block to check.
* @return {boolean} True if the block can be recycled.
* @package
*/
Blockly.scratchBlocksUtils.blockIsRecyclable = function(block) {
// If the block needs to parse mutations, never recycle.
if (block.mutationToDom && block.domToMutation) {
return false;
}
for (var i = 0; i < block.inputList.length; i++) {
var input = block.inputList[i];
for (var j = 0; j < input.fieldRow.length; j++) {
var field = input.fieldRow[j];
// No variables.
if (field instanceof Blockly.FieldVariable ||
field instanceof Blockly.FieldVariableGetter) {
return false;
}
if (field instanceof Blockly.FieldDropdown ||
field instanceof Blockly.FieldNumberDropdown ||
field instanceof Blockly.FieldTextDropdown) {
if (field.isOptionListDynamic()) {
return false;
}
}
}
// Check children.
if (input.connection) {
var child = input.connection.targetBlock();
if (child && !Blockly.scratchBlocksUtils.blockIsRecyclable(child)) {
return false;
}
}
}
return true;
};
/**
* Creates a callback function for a click on the "duplicate" context menu
* option in Scratch Blocks. The block is duplicated and attached to the mouse,
* which acts as though it were pressed and mid-drag. Clicking the mouse
* releases the new dragging block.
* @param {!Blockly.BlockSvg} oldBlock The block that will be duplicated.
* @param {!Event} event Event that caused the context menu to open.
* @return {Function} A callback function that duplicates the block and starts a
* drag.
* @package
*/
Blockly.scratchBlocksUtils.duplicateAndDragCallback = function(oldBlock, event) {
var isMouseEvent = Blockly.Touch.getTouchIdentifierFromEvent(event) === 'mouse';
return function(e) {
// Give the context menu a chance to close.
setTimeout(function() {
var ws = oldBlock.workspace;
var svgRootOld = oldBlock.getSvgRoot();
if (!svgRootOld) {
throw new Error('oldBlock is not rendered.');
}
// Create the new block by cloning the block in the flyout (via XML).
var xml = Blockly.Xml.blockToDom(oldBlock);
// The target workspace would normally resize during domToBlock, which
// will lead to weird jumps.
// Resizing will be enabled when the drag ends.
ws.setResizesEnabled(false);
// Disable events and manually emit events after the block has been
// positioned and has had its shadow IDs fixed (Scratch-specific).
Blockly.Events.disable();
try {
// Using domToBlock instead of domToWorkspace means that the new block
// will be placed at position (0, 0) in main workspace units.
var newBlock = Blockly.Xml.domToBlock(xml, ws);
// Scratch-specific: Give shadow dom new IDs to prevent duplicating on paste
Blockly.scratchBlocksUtils.changeObscuredShadowIds(newBlock);
var svgRootNew = newBlock.getSvgRoot();
if (!svgRootNew) {
throw new Error('newBlock is not rendered.');
}
// The position of the old block in workspace coordinates.
var oldBlockPosWs = oldBlock.getRelativeToSurfaceXY();
// Place the new block as the same position as the old block.
// TODO: Offset by the difference between the mouse position and the upper
// left corner of the block.
newBlock.moveBy(oldBlockPosWs.x, oldBlockPosWs.y);
if (!isMouseEvent) {
var offsetX = ws.RTL ? -100 : 100;
var offsetY = 100;
newBlock.moveBy(offsetX, offsetY); // Just offset the block for touch.
}
} finally {
Blockly.Events.enable();
}
if (Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.BlockCreate(newBlock));
}
if (isMouseEvent) {
// e is not a real mouseEvent/touchEvent/pointerEvent. It's an event
// created by the context menu and has the coordinates of the mouse
// click that opened the context menu.
var fakeEvent = {
clientX: event.clientX,
clientY: event.clientY,
type: 'mousedown',
preventDefault: function() {
e.preventDefault();
},
stopPropagation: function() {
e.stopPropagation();
},
target: e.target
};
ws.startDragWithFakeEvent(fakeEvent, newBlock);
}
}, 0);
};
};

View File

@@ -0,0 +1,699 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Object representing a UI bubble.
* @author kchadha@scratch.mit.edu (Karishma Chadha)
*/
'use strict';
goog.provide('Blockly.ScratchBubble');
goog.require('Blockly.Touch');
goog.require('Blockly.Workspace');
goog.require('goog.dom');
goog.require('goog.math');
goog.require('goog.math.Coordinate');
goog.require('goog.userAgent');
/**
* Class for Scratch comment UI bubble.
* @param {!Blockly.ScratchBlockComment} comment The comment this bubble belongs
* to.
* @param {!Blockly.WorkspaceSvg} workspace The workspace on which to draw the
* bubble.
* @param {!Element} content SVG content for the bubble.
* @param {!goog.math.Coordinate} anchorXY Absolute position of bubble's anchor
* point.
* @param {?number} bubbleWidth Width of bubble, or null if not resizable.
* @param {?number} bubbleHeight Height of bubble, or null if not resizable.
* @param {?number} bubbleX X position of bubble
* @param {?number} bubbleY Y position of bubble
* @param {?boolean} minimized Whether or not this comment bubble is minimized
* (only the top bar displays), defaults to false if not provided.
* @extends {Blockly.Bubble}
* @constructor
*/
Blockly.ScratchBubble = function(comment, workspace, content, anchorXY,
bubbleWidth, bubbleHeight, bubbleX, bubbleY, minimized) {
// Needed for Events
/**
* The comment this bubble belongs to.
* @type {Blockly.ScratchBlockComment}
* @package
*/
this.comment = comment;
this.workspace_ = workspace;
this.content_ = content;
this.x = bubbleX;
this.y = bubbleY;
this.isMinimized_ = minimized || false;
var canvas = workspace.getBubbleCanvas();
canvas.appendChild(this.createDom_(content, !!(bubbleWidth && bubbleHeight),
this.isMinimized_));
this.setAnchorLocation(anchorXY);
if (!bubbleWidth || !bubbleHeight) {
var bBox = /** @type {SVGLocatable} */ (this.content_).getBBox();
bubbleWidth = bBox.width + 2 * Blockly.ScratchBubble.BORDER_WIDTH;
bubbleHeight = bBox.height + 2 * Blockly.ScratchBubble.BORDER_WIDTH;
}
this.setBubbleSize(bubbleWidth, bubbleHeight);
// Render the bubble.
this.positionBubble_();
this.renderArrow_();
this.rendered_ = true;
if (!workspace.options.readOnly) {
Blockly.bindEventWithChecks_(
this.minimizeArrow_, 'mousedown', this, this.minimizeArrowMouseDown_, true);
Blockly.bindEventWithChecks_(
this.minimizeArrow_, 'mouseout', this, this.minimizeArrowMouseOut_, true);
Blockly.bindEventWithChecks_(
this.minimizeArrow_, 'mouseup', this, this.minimizeArrowMouseUp_, true);
Blockly.bindEventWithChecks_(
this.deleteIcon_, 'mousedown', this, this.deleteMouseDown_, true);
Blockly.bindEventWithChecks_(
this.deleteIcon_, 'mouseout', this, this.deleteMouseOut_, true);
Blockly.bindEventWithChecks_(
this.deleteIcon_, 'mouseup', this, this.deleteMouseUp_, true);
Blockly.bindEventWithChecks_(
this.commentTopBar_, 'mousedown', this, this.bubbleMouseDown_);
Blockly.bindEventWithChecks_(
this.bubbleBack_, 'mousedown', this, this.bubbleMouseDown_);
if (this.resizeGroup_) {
Blockly.bindEventWithChecks_(
this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_);
Blockly.bindEventWithChecks_(
this.resizeGroup_, 'mouseup', this, this.resizeMouseUp_);
}
}
this.setAutoLayout(false);
this.moveTo(this.x, this.y);
};
goog.inherits(Blockly.ScratchBubble, Blockly.Bubble);
/**
* Width of the border around the bubble.
* @package
*/
Blockly.ScratchBubble.BORDER_WIDTH = 1;
/**
* Thickness of the line connecting the bubble
* to the block.
* @private
*/
Blockly.ScratchBubble.LINE_THICKNESS = 1;
/**
* The height of the comment top bar.
* @package
*/
Blockly.ScratchBubble.TOP_BAR_HEIGHT = 32;
/**
* The size of the minimize arrow icon in the comment top bar.
* @private
*/
Blockly.ScratchBubble.MINIMIZE_ICON_SIZE = 32;
/**
* The size of the delete icon in the comment top bar.
* @private
*/
Blockly.ScratchBubble.DELETE_ICON_SIZE = 32;
/**
* The inset for the top bar icons.
* @private
*/
Blockly.ScratchBubble.TOP_BAR_ICON_INSET = 0;
/**
* The inset for the top bar icons.
* @private
*/
Blockly.ScratchBubble.RESIZE_SIZE = 16;
/**
* The bottom corner padding of the resize handle touch target.
* Extends slightly outside the comment box.
* @private
*/
Blockly.ScratchBubble.RESIZE_CORNER_PAD = 4;
/**
* The top/side padding around resize handle touch target.
* Extends about one extra "diagonal" above resize handle.
* @private
*/
Blockly.ScratchBubble.RESIZE_OUTER_PAD = 8;
/**
* Create the bubble's DOM.
* @param {!Element} content SVG content for the bubble.
* @param {boolean} hasResize Add diagonal resize gripper if true.
* @param {boolean} minimized Whether the bubble is minimized
* @return {!Element} The bubble's SVG group.
* @private
*/
Blockly.ScratchBubble.prototype.createDom_ = function(content, hasResize, minimized) {
this.bubbleGroup_ = Blockly.utils.createSvgElement('g', {}, null);
this.bubbleArrow_ = Blockly.utils.createSvgElement('line',
{'stroke-linecap': 'round'},
this.bubbleGroup_);
this.bubbleBack_ = Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyDraggable scratchCommentRect',
'x': 0,
'y': 0,
'rx': 4 * Blockly.ScratchBubble.BORDER_WIDTH,
'ry': 4 * Blockly.ScratchBubble.BORDER_WIDTH
},
this.bubbleGroup_);
this.labelText_ = content.labelText;
this.createCommentTopBar_();
// Comment Text Editor
this.commentEditor_ = content.commentEditor;
this.bubbleGroup_.appendChild(this.commentEditor_);
// Comment Resize Handle
if (hasResize) {
this.createResizeHandle_();
} else {
this.resizeGroup_ = null;
}
// Show / hide relevant things based on minimized state
if (minimized) {
this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-up.svg');
this.commentEditor_.setAttribute('display', 'none');
this.resizeGroup_.setAttribute('display', 'none');
} else {
this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-down.svg');
this.topBarLabel_.setAttribute('display', 'none');
}
return this.bubbleGroup_;
};
/**
* Create the comment top bar and its contents.
* @private
*/
Blockly.ScratchBubble.prototype.createCommentTopBar_ = function() {
this.commentTopBar_ = Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyDraggable scratchCommentTopBar',
'rx': Blockly.ScratchBubble.BORDER_WIDTH,
'ry': Blockly.ScratchBubble.BORDER_WIDTH,
'height': Blockly.ScratchBubble.TOP_BAR_HEIGHT
}, this.bubbleGroup_);
this.createTopBarIcons_();
this.createTopBarLabel_();
};
/**
* Create the minimize toggle and delete icons that in the comment top bar.
* @private
*/
Blockly.ScratchBubble.prototype.createTopBarIcons_ = function() {
var topBarMiddleY = (Blockly.ScratchBubble.TOP_BAR_HEIGHT / 2) +
Blockly.ScratchBubble.BORDER_WIDTH;
// Minimize Toggle Icon in Comment Top Bar
var xInset = Blockly.ScratchBubble.TOP_BAR_ICON_INSET;
this.minimizeArrow_ = Blockly.utils.createSvgElement('image',
{
'x': xInset,
'y': topBarMiddleY - Blockly.ScratchBubble.MINIMIZE_ICON_SIZE / 2,
'width': Blockly.ScratchBubble.MINIMIZE_ICON_SIZE,
'height': Blockly.ScratchBubble.MINIMIZE_ICON_SIZE
}, this.bubbleGroup_);
// Delete Icon in Comment Top Bar
this.deleteIcon_ = Blockly.utils.createSvgElement('image',
{
'x': xInset,
'y': topBarMiddleY - Blockly.ScratchBubble.DELETE_ICON_SIZE / 2,
'width': Blockly.ScratchBubble.DELETE_ICON_SIZE,
'height': Blockly.ScratchBubble.DELETE_ICON_SIZE
}, this.bubbleGroup_);
this.deleteIcon_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'delete-x.svg');
};
/**
* Create the comment top bar label. This is the truncated comment text
* that shows when comment is minimized.
* @private
*/
Blockly.ScratchBubble.prototype.createTopBarLabel_ = function() {
this.topBarLabel_ = Blockly.utils.createSvgElement('text',
{
'class': 'scratchCommentText',
'x': this.width_ / 2,
'y': (Blockly.ScratchBubble.TOP_BAR_HEIGHT / 2) + Blockly.ScratchBubble.BORDER_WIDTH,
'text-anchor': 'middle',
'dominant-baseline': 'middle'
}, this.bubbleGroup_);
var labelTextNode = document.createTextNode(this.labelText_);
this.topBarLabel_.appendChild(labelTextNode);
};
/**
* Create the comment resize handle.
* @private
*/
Blockly.ScratchBubble.prototype.createResizeHandle_ = function() {
this.resizeGroup_ = Blockly.utils.createSvgElement('g',
{'class': this.workspace_.RTL ?
'scratchCommentResizeSW' : 'scratchCommentResizeSE'},
this.bubbleGroup_);
var resizeSize = Blockly.ScratchBubble.RESIZE_SIZE;
var outerPad = Blockly.ScratchBubble.RESIZE_OUTER_PAD;
var cornerPad = Blockly.ScratchBubble.RESIZE_CORNER_PAD;
// Build an (invisible) triangle that will catch resizes. It is padded on the
// top/left by outerPad, and padded down/right by cornerPad.
Blockly.utils.createSvgElement('polygon',
{
'points': [
-outerPad, resizeSize + cornerPad,
resizeSize + cornerPad, resizeSize + cornerPad,
resizeSize + cornerPad, -outerPad
].join(' ')
},
this.resizeGroup_);
Blockly.utils.createSvgElement('line',
{
'class': 'blocklyResizeLine',
'x1': resizeSize / 3, 'y1': resizeSize - 1,
'x2': resizeSize - 1, 'y2': resizeSize / 3
}, this.resizeGroup_);
Blockly.utils.createSvgElement('line',
{
'class': 'blocklyResizeLine',
'x1': resizeSize * 2 / 3,
'y1': resizeSize - 1,
'x2': resizeSize - 1,
'y2': resizeSize * 2 / 3
}, this.resizeGroup_);
};
/**
* Show the context menu for this bubble.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.ScratchBubble.prototype.showContextMenu_ = function(e) {
if (this.workspace_.options.readOnly) {
return;
}
if (this.contextMenuCallback_) {
this.contextMenuCallback_(e);
}
};
/**
* Handle a mouse-down on bubble's minimize icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.minimizeArrowMouseDown_ = function(e) {
// Set a property indicating that this comment's minimize arrow got a mouse
// down event. This property will get reset if the mouse leaves the icon or
// when a mouse up occurs on this icon after this mouse down.
this.shouldToggleMinimize_ = true;
e.stopPropagation();
};
/**
* Handle a mouse-out on bubble's minimize icon.
* @param {!Event} _e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.minimizeArrowMouseOut_ = function(_e) {
// If the mouse has left the minimize arrow icon, the
// shouldToggleMinimize property should get reset to false.
this.shouldToggleMinimize_ = false;
};
/**
* Handle a mouse-up on bubble's minimize icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.minimizeArrowMouseUp_ = function(e) {
// First check if this icon had a mouse down event
// on it and that the mouse never left the icon
if (this.shouldToggleMinimize_) {
this.shouldToggleMinimize_ = false;
if (this.minimizeToggleCallback_) {
this.minimizeToggleCallback_.call(this);
}
}
e.stopPropagation();
};
/**
* Handle a mouse-down on bubble's delete icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.deleteMouseDown_ = function(e) {
this.shouldDelete_ = true;
e.stopPropagation();
};
/**
* Handle a mouse-out on bubble's delete icon.
* @param {!Event} _e Mouse out event.
* @private
*/
Blockly.ScratchBubble.prototype.deleteMouseOut_ = function(_e) {
// If the mouse has left the delete icon, the shouldDelete_ property
// should get reset to false.
this.shouldDelete_ = false;
};
/**
* Handle a mouse-up on bubble's delete icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.deleteMouseUp_ = function(e) {
// First check that this is actually the same icon that had a mouse down event
// on it and that the mouse never left the icon
if (this.shouldDelete_) {
this.shouldDelete_ = false;
if (this.deleteCallback_) {
this.deleteCallback_.call(this);
}
}
e.stopPropagation();
};
/**
* Handle a mouse-down on bubble's resize corner.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.ScratchBubble.prototype.resizeMouseDown_ = function(e) {
this.resizeStartSize_ = {width: this.width_, height: this.height_};
this.workspace_.setResizesEnabled(false);
Blockly.ScratchBubble.superClass_.resizeMouseDown_.call(this, e);
};
/**
* Handle a mouse-up on bubble's resize corner.
* @param {!Event} _e Mouse up event.
* @private
*/
Blockly.ScratchBubble.prototype.resizeMouseUp_ = function(_e) {
var oldHW = this.resizeStartSize_;
this.resizeStartSize_ = null;
if (this.width_ == oldHW.width && this.height_ == oldHW.height) {
return;
}
// Fire a change event for the new width/height after
// resize mouse up
Blockly.Events.fire(new Blockly.Events.CommentChange(
this.comment, {width: oldHW.width , height: oldHW.height},
{width: this.width_, height: this.height_}));
this.workspace_.setResizesEnabled(true);
};
/**
* Set the minimized state of the bubble.
* @param {boolean} minimize Whether the bubble should be minimized
* @param {?string} labelText Optional label text for the comment top bar
* when it is minimized.
* @package
*/
Blockly.ScratchBubble.prototype.setMinimized = function(minimize, labelText) {
if (minimize == this.isMinimized_) {
return;
}
if (minimize) {
this.isMinimized_ = true;
// Change minimize icon
this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-up.svg');
// Hide text area
this.commentEditor_.setAttribute('display', 'none');
// Hide resize handle if it exists
if (this.resizeGroup_) {
this.resizeGroup_.setAttribute('display', 'none');
}
if (labelText && this.labelText_ != labelText) {
// Update label and display
this.topBarLabel_.textContent = labelText;
}
Blockly.utils.removeAttribute(this.topBarLabel_, 'display');
} else {
this.isMinimized_ = false;
// Change minimize icon
this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-down.svg');
// Hide label
this.topBarLabel_.setAttribute('display', 'none');
// Show text area
Blockly.utils.removeAttribute(this.commentEditor_, 'display');
// Display resize handle if it exists
if (this.resizeGroup_) {
Blockly.utils.removeAttribute(this.resizeGroup_, 'display');
}
}
};
/**
* Register a function as a callback event for when the bubble is minimized.
* @param {!Function} callback The function to call on resize.
* @package
*/
Blockly.ScratchBubble.prototype.registerMinimizeToggleEvent = function(callback) {
this.minimizeToggleCallback_ = callback;
};
/**
* Register a function as a callback event for when the bubble is resized.
* @param {!Function} callback The function to call on resize.
* @package
*/
Blockly.ScratchBubble.prototype.registerDeleteEvent = function(callback) {
this.deleteCallback_ = callback;
};
/**
* Register a function as a callback to show the context menu for this comment.
* @param {!Function} callback The function to call on resize.
* @package
*/
Blockly.ScratchBubble.prototype.registerContextMenuCallback = function(callback) {
this.contextMenuCallback_ = callback;
};
/**
* Notification that the anchor has moved.
* Update the arrow and bubble accordingly.
* @param {!goog.math.Coordinate} xy Absolute location.
* @package
*/
Blockly.ScratchBubble.prototype.setAnchorLocation = function(xy) {
var event = new Blockly.Events.CommentMove(this.comment);
this.anchorXY_ = xy;
if (this.rendered_) {
this.positionBubble_();
}
event.recordNew();
Blockly.Events.fire(event);
};
/**
* Move the bubble group to the specified location in workspace coordinates.
* @param {number} x The x position to move to.
* @param {number} y The y position to move to.
* @package
*/
Blockly.ScratchBubble.prototype.moveTo = function(x, y) {
Blockly.ScratchBubble.superClass_.moveTo.call(this, x, y);
this.updatePosition_(x, y);
};
/**
* Size this bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
* @package
*/
Blockly.ScratchBubble.prototype.setBubbleSize = function(width, height) {
var doubleBorderWidth = 2 * Blockly.ScratchBubble.BORDER_WIDTH;
// Minimum size of a bubble.
width = Math.max(width, doubleBorderWidth + 50);
height = Math.max(height, Blockly.ScratchBubble.TOP_BAR_HEIGHT);
this.width_ = width;
this.height_ = height;
this.bubbleBack_.setAttribute('width', width);
this.bubbleBack_.setAttribute('height', height);
this.commentTopBar_.setAttribute('width', width);
this.commentTopBar_.setAttribute('height', Blockly.ScratchBubble.TOP_BAR_HEIGHT);
if (this.workspace_.RTL) {
this.minimizeArrow_.setAttribute('x', width -
(Blockly.ScratchBubble.MINIMIZE_ICON_SIZE) -
Blockly.ScratchBubble.TOP_BAR_ICON_INSET);
} else {
this.deleteIcon_.setAttribute('x', width -
Blockly.ScratchBubble.DELETE_ICON_SIZE -
Blockly.ScratchBubble.TOP_BAR_ICON_INSET);
}
if (this.resizeGroup_) {
var resizeSize = Blockly.ScratchBubble.RESIZE_SIZE;
if (this.workspace_.RTL) {
// Mirror the resize group.
this.resizeGroup_.setAttribute('transform', 'translate(' +
(resizeSize + doubleBorderWidth) + ',' +
(this.height_ - doubleBorderWidth - resizeSize) + ') scale(-1, 1)');
} else {
this.resizeGroup_.setAttribute('transform', 'translate(' +
(this.width_ - doubleBorderWidth - resizeSize) + ',' +
(this.height_ - doubleBorderWidth - resizeSize) + ')');
}
}
if (this.isMinimized_) {
this.topBarLabel_.setAttribute('x', this.width_ / 2);
this.topBarLabel_.setAttribute('y', this.height_ / 2);
}
if (this.rendered_) {
this.positionBubble_();
this.renderArrow_();
}
// Allow the contents to resize.
if (this.resizeCallback_) {
this.resizeCallback_();
}
};
/**
* Draw the line between the bubble and the origin.
* @private
*/
Blockly.ScratchBubble.prototype.renderArrow_ = function() {
// Find the relative coordinates of the top bar center of the bubble.
var relBubbleX = this.width_ / 2;
var relBubbleY = Blockly.ScratchBubble.TOP_BAR_HEIGHT / 2;
// Find the relative coordinates of the center of the anchor.
var relAnchorX = -this.relativeLeft_;
var relAnchorY = -this.relativeTop_;
if (relBubbleX != relAnchorX || relBubbleY != relAnchorY) {
// Compute the angle of the arrow's line.
var rise = relAnchorY - relBubbleY;
var run = relAnchorX - relBubbleX;
if (this.workspace_.RTL) {
run *= -1;
run -= this.width_;
}
var baseX1 = relBubbleX;
var baseY1 = relBubbleY;
this.bubbleArrow_.setAttribute('x1', baseX1);
this.bubbleArrow_.setAttribute('y1', baseY1);
this.bubbleArrow_.setAttribute('x2', baseX1 + run);
this.bubbleArrow_.setAttribute('y2', baseY1 + rise);
this.bubbleArrow_.setAttribute('stroke-width', Blockly.ScratchBubble.LINE_THICKNESS);
}
};
/**
* Change the colour of a bubble.
* @param {string} hexColour Hex code of colour.
* @package
*/
Blockly.ScratchBubble.prototype.setColour = function(hexColour) {
this.bubbleBack_.setAttribute('stroke', hexColour);
this.bubbleArrow_.setAttribute('stroke', hexColour);
};
/**
* Move this bubble during a drag, taking into account whether or not there is
* a drag surface.
* @param {?Blockly.BlockDragSurfaceSvg} dragSurface The surface that carries
* rendered items during a drag, or null if no drag surface is in use.
* @param {!goog.math.Coordinate} newLoc The location to translate to, in
* workspace coordinates.
* @package
*/
Blockly.ScratchBubble.prototype.moveDuringDrag = function(dragSurface, newLoc) {
if (dragSurface) {
dragSurface.translateSurface(newLoc.x, newLoc.y);
this.updatePosition_(newLoc.x, newLoc.y);
} else {
this.moveTo(newLoc.x, newLoc.y);
}
};
/**
* Update the relative left and top of the bubble after a move.
* @param {number} x The x position of the bubble
* @param {number} y The y position of the bubble
* @private
*/
Blockly.ScratchBubble.prototype.updatePosition_ = function(x, y) {
// Relative left is the distance *and* direction to get from the comment
// anchor position on the block to the starting edge of the comment (e.g.
// the left edge of the comment in LTR and the right edge of the comment in RTL)
if (this.workspace_.RTL) {
// we want relativeLeft_ to actually be the distance from the anchor point to the *right* edge of the comment in RTL
this.relativeLeft_ = this.anchorXY_.x - x;
} else {
this.relativeLeft_ = x - this.anchorXY_.x;
}
this.relativeTop_ = y - this.anchorXY_.y;
this.renderArrow_();
};
/**
* Dispose of this bubble.
* @package
*/
Blockly.ScratchBubble.prototype.dispose = function() {
Blockly.ScratchBubble.superClass_.dispose.call(this);
this.topBarLabel_ = null;
this.commentTopBar_ = null;
this.minimizeArrow_ = null;
this.deleteIcon_ = null;
};

View File

@@ -0,0 +1,131 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Events fired as a result of UI actions in a Scratch-Blocks
* editor that are not fired in Blockly.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.Events.DragBlockOutside');
goog.provide('Blockly.Events.EndBlockDrag');
goog.require('Blockly.Events');
goog.require('Blockly.Events.BlockBase');
goog.require('goog.array');
goog.require('goog.math.Coordinate');
/**
* Class for a block drag event. Fired when block dragged into or out of
* the blocks UI.
* @param {Blockly.Block} block The moved block. Null for a blank event.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.DragBlockOutside = function(block) {
if (!block) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.DragBlockOutside.superClass_.constructor.call(this, block);
this.recordUndo = false;
};
goog.inherits(Blockly.Events.DragBlockOutside, Blockly.Events.BlockBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.DragBlockOutside.prototype.type = Blockly.Events.DRAG_OUTSIDE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.DragBlockOutside.prototype.toJson = function() {
var json = Blockly.Events.DragBlockOutside.superClass_.toJson.call(this);
if (this.isOutside) {
json['isOutside'] = this.isOutside;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.DragBlockOutside.prototype.fromJson = function(json) {
Blockly.Events.DragBlockOutside.superClass_.fromJson.call(this, json);
this.isOutside = json['isOutside'];
};
/**
* Class for a block end drag event.
* @param {Blockly.Block} block The moved block. Null for a blank event.
* @param {boolean} isOutside True if the moved block is outside of the
* blocks workspace.
* @extends {Blockly.Events.BlockBase}
* @constructor
*/
Blockly.Events.EndBlockDrag = function(block, isOutside) {
if (!block) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.EndBlockDrag.superClass_.constructor.call(this, block);
this.isOutside = isOutside;
// If drag ends outside the blocks workspace, send the block XML
if (isOutside) {
this.xml = Blockly.Xml.blockToDom(block, true /* opt_noId */);
}
this.recordUndo = false;
};
goog.inherits(Blockly.Events.EndBlockDrag, Blockly.Events.BlockBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.EndBlockDrag.prototype.type = Blockly.Events.END_DRAG;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.EndBlockDrag.prototype.toJson = function() {
var json = Blockly.Events.EndBlockDrag.superClass_.toJson.call(this);
if (this.isOutside) {
json['isOutside'] = this.isOutside;
}
if (this.xml) {
json['xml'] = this.xml;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.EndBlockDrag.prototype.fromJson = function(json) {
Blockly.Events.EndBlockDrag.superClass_.fromJson.call(this, json);
this.isOutside = json['isOutside'];
this.xml = json['xml'];
};

View File

@@ -0,0 +1,85 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Scratch Messages singleton, with function to override Blockly.Msg values.
* @author chrisg@media.mit.edu (Chris Garrity)
*/
'use strict';
/**
* Name space for the ScratchMsgs singleton.
* Msg gets populated in the message files.
*/
goog.provide('Blockly.ScratchMsgs');
goog.require('Blockly.Msg');
/**
* The object containing messages for all locales - loaded from msg/scratch_msgs.
* @type {Object}
*/
Blockly.ScratchMsgs.locales = {};
/**
* The current locale.
* @type {String}
* @private
*/
Blockly.ScratchMsgs.currentLocale_ = 'en';
/**
* Change the Blockly.Msg strings to a new Locale
* Does not exist in Blockly, but needed in scratch-blocks
* @param {string} locale E.g., 'de', or 'zh-tw'
* @package
*/
Blockly.ScratchMsgs.setLocale = function(locale) {
if (Object.keys(Blockly.ScratchMsgs.locales).includes(locale)) {
Blockly.ScratchMsgs.currentLocale_ = locale;
Blockly.Msg = Object.assign({}, Blockly.Msg, Blockly.ScratchMsgs.locales[locale]);
} else {
// keep current locale
console.warn('Ignoring unrecognized locale: ' + locale);
}
};
/**
* Gets a localized message, for use in the Scratch VM with json init.
* Does not interpolate placeholders. Provided to allow default values in
* dynamic menus, for example, 'next backdrop', or 'random position'
* @param {string} msgId id for the message, key in Msg table.
* @param {string} defaultMsg string to use if the id isn't found.
* @param {string} useLocale optional locale to use in place of currentLocale_.
* @return {string} message with placeholders filled.
* @package
*/
Blockly.ScratchMsgs.translate = function(msgId, defaultMsg, useLocale) {
var locale = useLocale || Blockly.ScratchMsgs.currentLocale_;
if (Object.keys(Blockly.ScratchMsgs.locales).includes(locale)) {
var messages = Blockly.ScratchMsgs.locales[locale];
if (Object.keys(messages).includes(msgId)) {
return messages[msgId];
}
}
return defaultMsg;
};

View File

@@ -0,0 +1,875 @@
/**
* @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 Library for creating scrollbars.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Scrollbar');
goog.provide('Blockly.ScrollbarPair');
goog.require('goog.dom');
goog.require('goog.events');
/**
* A note on units: most of the numbers that are in CSS pixels are scaled if the
* scrollbar is in a mutator.
*/
/**
* Class for a pair of scrollbars. Horizontal and vertical.
* @param {!Blockly.Workspace} workspace Workspace to bind the scrollbars to.
* @constructor
*/
Blockly.ScrollbarPair = function(workspace) {
this.workspace_ = workspace;
this.hScroll = new Blockly.Scrollbar(
workspace, true, true, 'blocklyMainWorkspaceScrollbar');
this.vScroll = new Blockly.Scrollbar(
workspace, false, true, 'blocklyMainWorkspaceScrollbar');
this.corner_ = Blockly.utils.createSvgElement(
'rect',
{
'height': Blockly.Scrollbar.scrollbarThickness,
'width': Blockly.Scrollbar.scrollbarThickness,
'class': 'blocklyScrollbarBackground'
},
null);
Blockly.utils.insertAfter(this.corner_, workspace.getBubbleCanvas());
};
/**
* Previously recorded metrics from the workspace.
* @type {Object}
* @private
*/
Blockly.ScrollbarPair.prototype.oldHostMetrics_ = null;
/**
* Dispose of this pair of scrollbars.
* Unlink from all DOM elements to prevent memory leaks.
*/
Blockly.ScrollbarPair.prototype.dispose = function() {
goog.dom.removeNode(this.corner_);
this.corner_ = null;
this.workspace_ = null;
this.oldHostMetrics_ = null;
this.hScroll.dispose();
this.hScroll = null;
this.vScroll.dispose();
this.vScroll = null;
};
/**
* Recalculate both of the scrollbars' locations and lengths.
* Also reposition the corner rectangle.
*/
Blockly.ScrollbarPair.prototype.resize = function() {
// Look up the host metrics once, and use for both scrollbars.
var hostMetrics = this.workspace_.getMetrics();
if (!hostMetrics) {
// Host element is likely not visible.
return;
}
// Only change the scrollbars if there has been a change in metrics.
var resizeH = false;
var resizeV = false;
if (!this.oldHostMetrics_ ||
this.oldHostMetrics_.viewWidth != hostMetrics.viewWidth ||
this.oldHostMetrics_.viewHeight != hostMetrics.viewHeight ||
this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop ||
this.oldHostMetrics_.absoluteLeft != hostMetrics.absoluteLeft) {
// The window has been resized or repositioned.
resizeH = true;
resizeV = true;
} else {
// Has the content been resized or moved?
if (!this.oldHostMetrics_ ||
this.oldHostMetrics_.contentWidth != hostMetrics.contentWidth ||
this.oldHostMetrics_.viewLeft != hostMetrics.viewLeft ||
this.oldHostMetrics_.contentLeft != hostMetrics.contentLeft) {
resizeH = true;
}
if (!this.oldHostMetrics_ ||
this.oldHostMetrics_.contentHeight != hostMetrics.contentHeight ||
this.oldHostMetrics_.viewTop != hostMetrics.viewTop ||
this.oldHostMetrics_.contentTop != hostMetrics.contentTop) {
resizeV = true;
}
}
if (resizeH) {
this.hScroll.resize(hostMetrics);
}
if (resizeV) {
this.vScroll.resize(hostMetrics);
}
// Reposition the corner square.
if (!this.oldHostMetrics_ ||
this.oldHostMetrics_.viewWidth != hostMetrics.viewWidth ||
this.oldHostMetrics_.absoluteLeft != hostMetrics.absoluteLeft) {
this.corner_.setAttribute('x', this.vScroll.position_.x);
}
if (!this.oldHostMetrics_ ||
this.oldHostMetrics_.viewHeight != hostMetrics.viewHeight ||
this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop) {
this.corner_.setAttribute('y', this.hScroll.position_.y);
}
// Cache the current metrics to potentially short-cut the next resize event.
this.oldHostMetrics_ = hostMetrics;
};
/**
* Set the handles of both scrollbars to be at a certain position in CSS pixels
* relative to their parents.
* @param {number} x Horizontal scroll value.
* @param {number} y Vertical scroll value.
*/
Blockly.ScrollbarPair.prototype.set = function(x, y) {
// This function is equivalent to:
// this.hScroll.set(x);
// this.vScroll.set(y);
// However, that calls setMetrics twice which causes a chain of
// getAttribute->setAttribute->getAttribute resulting in an extra layout pass.
// Combining them speeds up rendering.
var xyRatio = {};
var hHandlePosition = x * this.hScroll.ratio_;
var vHandlePosition = y * this.vScroll.ratio_;
var hBarLength = this.hScroll.scrollViewSize_;
var vBarLength = this.vScroll.scrollViewSize_;
xyRatio.x = this.getRatio_(hHandlePosition, hBarLength);
xyRatio.y = this.getRatio_(vHandlePosition, vBarLength);
this.workspace_.setMetrics(xyRatio);
this.hScroll.setHandlePosition(hHandlePosition);
this.vScroll.setHandlePosition(vHandlePosition);
};
/**
* Helper to calculate the ratio of handle position to scrollbar view size.
* @param {number} handlePosition The value of the handle.
* @param {number} viewSize The total size of the scrollbar's view.
* @return {number} Ratio.
* @private
*/
Blockly.ScrollbarPair.prototype.getRatio_ = function(handlePosition, viewSize) {
var ratio = handlePosition / viewSize;
if (isNaN(ratio)) {
return 0;
}
return ratio;
};
// --------------------------------------------------------------------
/**
* Class for a pure SVG scrollbar.
* This technique offers a scrollbar that is guaranteed to work, but may not
* look or behave like the system's scrollbars.
* @param {!Blockly.Workspace} workspace Workspace to bind the scrollbar to.
* @param {boolean} horizontal True if horizontal, false if vertical.
* @param {boolean=} opt_pair True if scrollbar is part of a horiz/vert pair.
* @param {string=} opt_class A class to be applied to this scrollbar.
* @constructor
*/
Blockly.Scrollbar = function(workspace, horizontal, opt_pair, opt_class) {
this.workspace_ = workspace;
this.pair_ = opt_pair || false;
this.horizontal_ = horizontal;
this.oldHostMetrics_ = null;
this.createDom_(opt_class);
/**
* The upper left corner of the scrollbar's SVG group in CSS pixels relative
* to the scrollbar's origin. This is usually relative to the injection div
* origin.
* @type {goog.math.Coordinate}
* @private
*/
this.position_ = new goog.math.Coordinate(0, 0);
// Store the thickness in a temp variable for readability.
var scrollbarThickness = Blockly.Scrollbar.scrollbarThickness;
if (horizontal) {
this.svgBackground_.setAttribute('height', scrollbarThickness);
this.outerSvg_.setAttribute('height', scrollbarThickness);
this.svgHandle_.setAttribute('height', scrollbarThickness - 5);
this.svgHandle_.setAttribute('y', 2.5);
this.lengthAttribute_ = 'width';
this.positionAttribute_ = 'x';
} else {
this.svgBackground_.setAttribute('width', scrollbarThickness);
this.outerSvg_.setAttribute('width', scrollbarThickness);
this.svgHandle_.setAttribute('width', scrollbarThickness - 5);
this.svgHandle_.setAttribute('x', 2.5);
this.lengthAttribute_ = 'height';
this.positionAttribute_ = 'y';
}
var scrollbar = this;
this.onMouseDownBarWrapper_ = Blockly.bindEventWithChecks_(
this.svgBackground_, 'mousedown', scrollbar, scrollbar.onMouseDownBar_);
this.onMouseDownHandleWrapper_ = Blockly.bindEventWithChecks_(this.svgHandle_,
'mousedown', scrollbar, scrollbar.onMouseDownHandle_);
};
/**
* The location of the origin of the workspace that the scrollbar is in,
* measured in CSS pixels relative to the injection div origin. This is usually
* (0, 0). When the scrollbar is in a flyout it may have a different origin.
* @type {goog.math.Coordinate}
* @private
*/
Blockly.Scrollbar.prototype.origin_ = new goog.math.Coordinate(0, 0);
/**
* Whether or not the origin of the scrollbar has changed. Used
* to help decide whether or not the reflow/resize calls need to happen.
* @type {boolean}
* @private
*/
Blockly.Scrollbar.prototype.originHasChanged_ = true;
/**
* The size of the area within which the scrollbar handle can move, in CSS
* pixels.
* @type {number}
* @private
*/
Blockly.Scrollbar.prototype.scrollViewSize_ = 0;
/**
* The length of the scrollbar handle in CSS pixels.
* @type {number}
* @private
*/
Blockly.Scrollbar.prototype.handleLength_ = 0;
/**
* The offset of the start of the handle from the scrollbar position, in CSS
* pixels.
* @type {number}
* @private
*/
Blockly.Scrollbar.prototype.handlePosition_ = 0;
/**
* Whether the scrollbar handle is visible.
* @type {boolean}
* @private
*/
Blockly.Scrollbar.prototype.isVisible_ = true;
/**
* Whether the workspace containing this scrollbar is visible.
* @type {boolean}
* @private
*/
Blockly.Scrollbar.prototype.containerVisible_ = true;
/**
* Width of vertical scrollbar or height of horizontal scrollbar in CSS pixels.
* Scrollbars should be larger on touch devices.
*/
Blockly.Scrollbar.scrollbarThickness = 11;
if (goog.events.BrowserFeature.TOUCH_ENABLED) {
Blockly.Scrollbar.scrollbarThickness = 14;
}
/**
* @param {!Object} first An object containing computed measurements of a
* workspace.
* @param {!Object} second Another object containing computed measurements of a
* workspace.
* @return {boolean} Whether the two sets of metrics are equivalent.
* @private
*/
Blockly.Scrollbar.metricsAreEquivalent_ = function(first, second) {
if (!(first && second)) {
return false;
}
if (first.viewWidth != second.viewWidth ||
first.viewHeight != second.viewHeight ||
first.viewLeft != second.viewLeft ||
first.viewTop != second.viewTop ||
first.absoluteTop != second.absoluteTop ||
first.absoluteLeft != second.absoluteLeft ||
first.contentWidth != second.contentWidth ||
first.contentHeight != second.contentHeight ||
first.contentLeft != second.contentLeft ||
first.contentTop != second.contentTop) {
return false;
}
return true;
};
/**
* Dispose of this scrollbar.
* Unlink from all DOM elements to prevent memory leaks.
*/
Blockly.Scrollbar.prototype.dispose = function() {
this.cleanUp_();
Blockly.unbindEvent_(this.onMouseDownBarWrapper_);
this.onMouseDownBarWrapper_ = null;
Blockly.unbindEvent_(this.onMouseDownHandleWrapper_);
this.onMouseDownHandleWrapper_ = null;
goog.dom.removeNode(this.outerSvg_);
this.outerSvg_ = null;
this.svgGroup_ = null;
this.svgBackground_ = null;
this.svgHandle_ = null;
this.workspace_ = null;
};
/**
* Set the length of the scrollbar's handle and change the SVG attribute
* accordingly.
* @param {number} newLength The new scrollbar handle length in CSS pixels.
*/
Blockly.Scrollbar.prototype.setHandleLength_ = function(newLength) {
this.handleLength_ = newLength;
this.svgHandle_.setAttribute(this.lengthAttribute_, this.handleLength_);
};
/**
* Set the offset of the scrollbar's handle from the scrollbar's position, and
* change the SVG attribute accordingly.
* @param {number} newPosition The new scrollbar handle offset in CSS pixels.
*/
Blockly.Scrollbar.prototype.setHandlePosition = function(newPosition) {
this.handlePosition_ = newPosition;
this.svgHandle_.setAttribute(this.positionAttribute_, this.handlePosition_);
};
/**
* Set the size of the scrollbar's background and change the SVG attribute
* accordingly.
* @param {number} newSize The new scrollbar background length in CSS pixels.
* @private
*/
Blockly.Scrollbar.prototype.setScrollViewSize_ = function(newSize) {
this.scrollViewSize_ = newSize;
this.outerSvg_.setAttribute(this.lengthAttribute_, this.scrollViewSize_);
this.svgBackground_.setAttribute(this.lengthAttribute_, this.scrollViewSize_);
};
/**
* Set whether this scrollbar's container is visible.
* @param {boolean} visible Whether the container is visible.
*/
Blockly.ScrollbarPair.prototype.setContainerVisible = function(visible) {
this.hScroll.setContainerVisible(visible);
this.vScroll.setContainerVisible(visible);
};
/**
* Set the position of the scrollbar's SVG group in CSS pixels relative to the
* scrollbar's origin. This sets the scrollbar's location within the workspace.
* @param {number} x The new x coordinate.
* @param {number} y The new y coordinate.
* @private
*/
Blockly.Scrollbar.prototype.setPosition_ = function(x, y) {
this.position_.x = x;
this.position_.y = y;
var tempX = this.position_.x + this.origin_.x;
var tempY = this.position_.y + this.origin_.y;
var transform = 'translate(' + tempX + 'px,' + tempY + 'px)';
Blockly.utils.setCssTransform(this.outerSvg_, transform);
};
/**
* Recalculate the scrollbar's location and its length.
* @param {Object=} opt_metrics A data structure of from the describing all the
* required dimensions. If not provided, it will be fetched from the host
* object.
*/
Blockly.Scrollbar.prototype.resize = function(opt_metrics) {
// Determine the location, height and width of the host element.
var hostMetrics = opt_metrics;
if (!hostMetrics) {
hostMetrics = this.workspace_.getMetrics();
if (!hostMetrics) {
// Host element is likely not visible.
return;
}
}
// If the origin has changed (e.g. the toolbox is moving from start to end)
// we want to continue with the resize even if workspace metrics haven't.
if (this.originHasChanged_) {
this.originHasChanged_ = false;
} else if (Blockly.Scrollbar.metricsAreEquivalent_(hostMetrics,
this.oldHostMetrics_)) {
return;
}
this.oldHostMetrics_ = hostMetrics;
/* hostMetrics is an object with the following properties.
* .viewHeight: Height of the visible rectangle,
* .viewWidth: Width of the visible rectangle,
* .contentHeight: Height of the contents,
* .contentWidth: Width of the content,
* .viewTop: Offset of top edge of visible rectangle from parent,
* .viewLeft: Offset of left edge of visible rectangle from parent,
* .contentTop: Offset of the top-most content from the y=0 coordinate,
* .contentLeft: Offset of the left-most content from the x=0 coordinate,
* .absoluteTop: Top-edge of view.
* .absoluteLeft: Left-edge of view.
*/
if (this.horizontal_) {
this.resizeHorizontal_(hostMetrics);
} else {
this.resizeVertical_(hostMetrics);
}
// Resizing may have caused some scrolling.
this.onScroll_();
};
/**
* Recalculate a horizontal scrollbar's location and length.
* @param {!Object} hostMetrics A data structure describing all the
* required dimensions, possibly fetched from the host object.
* @private
*/
Blockly.Scrollbar.prototype.resizeHorizontal_ = function(hostMetrics) {
// TODO: Inspect metrics to determine if we can get away with just a content
// resize.
this.resizeViewHorizontal(hostMetrics);
};
/**
* Recalculate a horizontal scrollbar's location on the screen and path length.
* This should be called when the layout or size of the window has changed.
* @param {!Object} hostMetrics A data structure describing all the
* required dimensions, possibly fetched from the host object.
*/
Blockly.Scrollbar.prototype.resizeViewHorizontal = function(hostMetrics) {
var viewSize = hostMetrics.viewWidth - 1;
if (this.pair_) {
// Shorten the scrollbar to make room for the corner square.
viewSize -= Blockly.Scrollbar.scrollbarThickness;
}
this.setScrollViewSize_(Math.max(0, viewSize));
var xCoordinate = hostMetrics.absoluteLeft + 0.5;
if (this.pair_ && this.workspace_.RTL) {
xCoordinate += Blockly.Scrollbar.scrollbarThickness;
}
// Horizontal toolbar should always be just above the bottom of the workspace.
var yCoordinate = hostMetrics.absoluteTop + hostMetrics.viewHeight -
Blockly.Scrollbar.scrollbarThickness - 0.5;
this.setPosition_(xCoordinate, yCoordinate);
// If the view has been resized, a content resize will also be necessary. The
// reverse is not true.
this.resizeContentHorizontal(hostMetrics);
};
/**
* Recalculate a horizontal scrollbar's location within its path and length.
* This should be called when the contents of the workspace have changed.
* @param {!Object} hostMetrics A data structure describing all the
* required dimensions, possibly fetched from the host object.
*/
Blockly.Scrollbar.prototype.resizeContentHorizontal = function(hostMetrics) {
if (!this.pair_) {
// Only show the scrollbar if needed.
// Ideally this would also apply to scrollbar pairs, but that's a bigger
// headache (due to interactions with the corner square).
this.setVisible(this.scrollViewSize_ < hostMetrics.contentWidth);
}
this.ratio_ = this.scrollViewSize_ / hostMetrics.contentWidth;
if (this.ratio_ == -Infinity || this.ratio_ == Infinity ||
isNaN(this.ratio_)) {
this.ratio_ = 0;
}
var handleLength = hostMetrics.viewWidth * this.ratio_;
this.setHandleLength_(Math.max(0, handleLength));
var handlePosition = (hostMetrics.viewLeft - hostMetrics.contentLeft) *
this.ratio_;
this.setHandlePosition(this.constrainHandle_(handlePosition));
};
/**
* Recalculate a vertical scrollbar's location and length.
* @param {!Object} hostMetrics A data structure describing all the
* required dimensions, possibly fetched from the host object.
* @private
*/
Blockly.Scrollbar.prototype.resizeVertical_ = function(hostMetrics) {
// TODO: Inspect metrics to determine if we can get away with just a content
// resize.
this.resizeViewVertical(hostMetrics);
};
/**
* Recalculate a vertical scrollbar's location on the screen and path length.
* This should be called when the layout or size of the window has changed.
* @param {!Object} hostMetrics A data structure describing all the
* required dimensions, possibly fetched from the host object.
*/
Blockly.Scrollbar.prototype.resizeViewVertical = function(hostMetrics) {
var viewSize = hostMetrics.viewHeight - 1;
if (this.pair_) {
// Shorten the scrollbar to make room for the corner square.
viewSize -= Blockly.Scrollbar.scrollbarThickness;
}
this.setScrollViewSize_(Math.max(0, viewSize));
var xCoordinate = hostMetrics.absoluteLeft + 0.5;
if (!this.workspace_.RTL) {
xCoordinate += hostMetrics.viewWidth -
Blockly.Scrollbar.scrollbarThickness - 1;
}
var yCoordinate = hostMetrics.absoluteTop + 0.5;
this.setPosition_(xCoordinate, yCoordinate);
// If the view has been resized, a content resize will also be necessary. The
// reverse is not true.
this.resizeContentVertical(hostMetrics);
};
/**
* Recalculate a vertical scrollbar's location within its path and length.
* This should be called when the contents of the workspace have changed.
* @param {!Object} hostMetrics A data structure describing all the
* required dimensions, possibly fetched from the host object.
*/
Blockly.Scrollbar.prototype.resizeContentVertical = function(hostMetrics) {
if (!this.pair_) {
// Only show the scrollbar if needed.
this.setVisible(this.scrollViewSize_ < hostMetrics.contentHeight);
}
this.ratio_ = this.scrollViewSize_ / hostMetrics.contentHeight;
if (this.ratio_ == -Infinity || this.ratio_ == Infinity ||
isNaN(this.ratio_)) {
this.ratio_ = 0;
}
var handleLength = hostMetrics.viewHeight * this.ratio_;
this.setHandleLength_(Math.max(0, handleLength));
var handlePosition = (hostMetrics.viewTop - hostMetrics.contentTop) *
this.ratio_;
this.setHandlePosition(this.constrainHandle_(handlePosition));
};
/**
* Create all the DOM elements required for a scrollbar.
* The resulting widget is not sized.
* @param {string=} opt_class A class to be applied to this scrollbar.
* @private
*/
Blockly.Scrollbar.prototype.createDom_ = function(opt_class) {
/* Create the following DOM:
<svg class="blocklyScrollbarHorizontal optionalClass">
<g>
<rect class="blocklyScrollbarBackground" />
<rect class="blocklyScrollbarHandle" rx="8" ry="8" />
</g>
</svg>
*/
var className = 'blocklyScrollbar' +
(this.horizontal_ ? 'Horizontal' : 'Vertical');
if (opt_class) {
className += ' ' + opt_class;
}
this.outerSvg_ = Blockly.utils.createSvgElement(
'svg', {'class': className}, null);
this.svgGroup_ = Blockly.utils.createSvgElement('g', {}, this.outerSvg_);
this.svgBackground_ = Blockly.utils.createSvgElement(
'rect', {'class': 'blocklyScrollbarBackground'}, this.svgGroup_);
var radius = Math.floor((Blockly.Scrollbar.scrollbarThickness - 5) / 2);
this.svgHandle_ = Blockly.utils.createSvgElement(
'rect',
{
'class': 'blocklyScrollbarHandle',
'rx': radius,
'ry': radius
},
this.svgGroup_);
Blockly.utils.insertAfter(this.outerSvg_, this.workspace_.getParentSvg());
};
/**
* Is the scrollbar visible. Non-paired scrollbars disappear when they aren't
* needed.
* @return {boolean} True if visible.
*/
Blockly.Scrollbar.prototype.isVisible = function() {
return this.isVisible_;
};
/**
* Set whether the scrollbar's container is visible and update
* display accordingly if visibility has changed.
* @param {boolean} visible Whether the container is visible
*/
Blockly.Scrollbar.prototype.setContainerVisible = function(visible) {
var visibilityChanged = (visible != this.containerVisible_);
this.containerVisible_ = visible;
if (visibilityChanged) {
this.updateDisplay_();
}
};
/**
* Set whether the scrollbar is visible.
* Only applies to non-paired scrollbars.
* @param {boolean} visible True if visible.
*/
Blockly.Scrollbar.prototype.setVisible = function(visible) {
var visibilityChanged = (visible != this.isVisible());
// Ideally this would also apply to scrollbar pairs, but that's a bigger
// headache (due to interactions with the corner square).
if (this.pair_) {
throw 'Unable to toggle visibility of paired scrollbars.';
}
this.isVisible_ = visible;
if (visibilityChanged) {
this.updateDisplay_();
}
};
/**
* Update visibility of scrollbar based on whether it thinks it should
* be visible and whether its containing workspace is visible.
* We cannot rely on the containing workspace being hidden to hide us
* because it is not necessarily our parent in the DOM.
*/
Blockly.Scrollbar.prototype.updateDisplay_ = function() {
var show = true;
// Check whether our parent/container is visible.
if (!this.containerVisible_) {
show = false;
} else {
show = this.isVisible();
}
if (show) {
this.outerSvg_.setAttribute('display', 'block');
} else {
this.outerSvg_.setAttribute('display', 'none');
}
};
/**
* Scroll by one pageful.
* Called when scrollbar background is clicked.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Scrollbar.prototype.onMouseDownBar_ = function(e) {
this.workspace_.markFocused();
Blockly.Touch.clearTouchIdentifier(); // This is really a click.
this.cleanUp_();
if (Blockly.utils.isRightButton(e)) {
// Right-click.
// Scrollbars have no context menu.
e.stopPropagation();
return;
}
var mouseXY = Blockly.utils.mouseToSvg(e, this.workspace_.getParentSvg(),
this.workspace_.getInverseScreenCTM());
var mouseLocation = this.horizontal_ ? mouseXY.x : mouseXY.y;
var handleXY = Blockly.utils.getInjectionDivXY_(this.svgHandle_);
var handleStart = this.horizontal_ ? handleXY.x : handleXY.y;
var handlePosition = this.handlePosition_;
var pageLength = this.handleLength_ * 0.95;
if (mouseLocation <= handleStart) {
// Decrease the scrollbar's value by a page.
handlePosition -= pageLength;
} else if (mouseLocation >= handleStart + this.handleLength_) {
// Increase the scrollbar's value by a page.
handlePosition += pageLength;
}
// When the scrollbars are clicked, hide the WidgetDiv/DropDownDiv without
// animation in anticipation of a workspace move.
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
this.setHandlePosition(this.constrainHandle_(handlePosition));
this.onScroll_();
e.stopPropagation();
e.preventDefault();
};
/**
* Start a dragging operation.
* Called when scrollbar handle is clicked.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Scrollbar.prototype.onMouseDownHandle_ = function(e) {
this.workspace_.markFocused();
this.cleanUp_();
if (Blockly.utils.isRightButton(e)) {
// Right-click.
// Scrollbars have no context menu.
e.stopPropagation();
return;
}
// 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;
Blockly.Scrollbar.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(document,
'mouseup', this, this.onMouseUpHandle_);
Blockly.Scrollbar.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(document,
'mousemove', this, this.onMouseMoveHandle_);
// When the scrollbars are clicked, hide the WidgetDiv/DropDownDiv without
// animation in anticipation of a workspace move.
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
e.stopPropagation();
e.preventDefault();
};
/**
* Drag the scrollbar's handle.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.Scrollbar.prototype.onMouseMoveHandle_ = function(e) {
var currentMouse = this.horizontal_ ? e.clientX : e.clientY;
var mouseDelta = currentMouse - this.startDragMouse_;
var handlePosition = this.startDragHandle + mouseDelta;
// Position the bar.
this.setHandlePosition(this.constrainHandle_(handlePosition));
this.onScroll_();
};
/**
* Release the scrollbar handle and reset state accordingly.
* @private
*/
Blockly.Scrollbar.prototype.onMouseUpHandle_ = function() {
// Tell the workspace to clean up now that the workspace is done moving.
this.workspace_.resetDragSurface();
Blockly.Touch.clearTouchIdentifier();
this.cleanUp_();
};
/**
* Hide chaff and stop binding to mouseup and mousemove events. Call this to
* wrap up lose ends associated with the scrollbar.
* @private
*/
Blockly.Scrollbar.prototype.cleanUp_ = function() {
Blockly.hideChaff(true);
if (Blockly.Scrollbar.onMouseUpWrapper_) {
Blockly.unbindEvent_(Blockly.Scrollbar.onMouseUpWrapper_);
Blockly.Scrollbar.onMouseUpWrapper_ = null;
}
if (Blockly.Scrollbar.onMouseMoveWrapper_) {
Blockly.unbindEvent_(Blockly.Scrollbar.onMouseMoveWrapper_);
Blockly.Scrollbar.onMouseMoveWrapper_ = null;
}
};
/**
* Constrain the handle's position within the minimum (0) and maximum
* (length of scrollbar) values allowed for the scrollbar.
* @param {number} value Value that is potentially out of bounds, in CSS pixels.
* @return {number} Constrained value, in CSS pixels.
* @private
*/
Blockly.Scrollbar.prototype.constrainHandle_ = function(value) {
if (value <= 0 || isNaN(value) || this.scrollViewSize_ < this.handleLength_) {
value = 0;
} else {
value = Math.min(value, this.scrollViewSize_ - this.handleLength_);
}
return value;
};
/**
* Called when scrollbar is moved.
* @private
*/
Blockly.Scrollbar.prototype.onScroll_ = function() {
var ratio = this.handlePosition_ / this.scrollViewSize_;
if (isNaN(ratio)) {
ratio = 0;
}
var xyRatio = {};
if (this.horizontal_) {
xyRatio.x = ratio;
} else {
xyRatio.y = ratio;
}
this.workspace_.setMetrics(xyRatio);
};
/**
* Set the scrollbar handle's position.
* @param {number} value The distance from the top/left end of the bar, in CSS
* pixels. It may be larger than the maximum allowable position of the
* scrollbar handle.
*/
Blockly.Scrollbar.prototype.set = function(value) {
this.setHandlePosition(this.constrainHandle_(value * this.ratio_));
this.onScroll_();
};
/**
* Record the origin of the workspace that the scrollbar is in, in pixels
* relative to the injection div origin. This is for times when the scrollbar is
* used in an object whose origin isn't the same as the main workspace
* (e.g. in a flyout.)
* @param {number} x The x coordinate of the scrollbar's origin, in CSS pixels.
* @param {number} y The y coordinate of the scrollbar's origin, in CSS pixels.
*/
Blockly.Scrollbar.prototype.setOrigin = function(x, y) {
if (x != this.origin_.x || y != this.origin_.y) {
this.origin_ = new goog.math.Coordinate(x, y);
this.originHasChanged_ = true;
}
};

View File

@@ -0,0 +1,803 @@
/**
* @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 Toolbox from whence to create blocks.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Toolbox');
goog.require('Blockly.Events.Ui');
goog.require('Blockly.HorizontalFlyout');
goog.require('Blockly.Touch');
goog.require('Blockly.VerticalFlyout');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.BrowserFeature');
goog.require('goog.html.SafeHtml');
goog.require('goog.html.SafeStyle');
goog.require('goog.math.Rect');
goog.require('goog.style');
goog.require('goog.ui.tree.TreeControl');
goog.require('goog.ui.tree.TreeNode');
goog.require('goog.string');
/**
* Class for a Toolbox.
* Creates the toolbox's DOM.
* @param {!Blockly.Workspace} workspace The workspace in which to create new
* blocks.
* @constructor
*/
Blockly.Toolbox = function(workspace) {
/**
* @type {!Blockly.Workspace}
* @private
*/
this.workspace_ = workspace;
/**
* Whether toolbox categories should be represented by icons instead of text.
* @type {boolean}
* @private
*/
this.iconic_ = false;
/**
* Is RTL vs LTR.
* @type {boolean}
*/
this.RTL = workspace.options.RTL;
/**
* Whether the toolbox should be laid out horizontally.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = workspace.options.horizontalLayout;
/**
* Position of the toolbox and flyout relative to the workspace.
* @type {number}
*/
this.toolboxPosition = workspace.options.toolboxPosition;
};
/**
* Width of the toolbox, which changes only in vertical layout.
* This is the sum of the width of the flyout (250) and the category menu (60).
* @type {number}
*/
Blockly.Toolbox.prototype.width = 310;
/**
* Height of the toolbox, which changes only in horizontal layout.
* @type {number}
*/
Blockly.Toolbox.prototype.height = 0;
Blockly.Toolbox.prototype.selectedItem_ = null;
/**
* Initializes the toolbox.
*/
Blockly.Toolbox.prototype.init = function() {
var workspace = this.workspace_;
var svg = this.workspace_.getParentSvg();
/**
* HTML container for the Toolbox menu.
* @type {Element}
*/
this.HtmlDiv =
goog.dom.createDom(goog.dom.TagName.DIV, 'blocklyToolboxDiv');
this.HtmlDiv.setAttribute('dir', workspace.RTL ? 'RTL' : 'LTR');
svg.parentNode.insertBefore(this.HtmlDiv, svg);
// Clicking on toolbox closes popups.
Blockly.bindEventWithChecks_(this.HtmlDiv, 'mousedown', this,
function(e) {
// Cancel any gestures in progress.
this.workspace_.cancelCurrentGesture();
if (Blockly.utils.isRightButton(e) || e.target == this.HtmlDiv) {
// Close flyout.
Blockly.hideChaff(false);
} else {
// Just close popups.
Blockly.hideChaff(true);
}
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
}, /*opt_noCaptureIdentifier*/ false, /*opt_noPreventDefault*/ true);
this.createFlyout_();
this.categoryMenu_ = new Blockly.Toolbox.CategoryMenu(this, this.HtmlDiv);
this.populate_(workspace.options.languageTree);
this.position();
};
/**
* Dispose of this toolbox.
*/
Blockly.Toolbox.prototype.dispose = function() {
this.flyout_.dispose();
this.categoryMenu_.dispose();
this.categoryMenu_ = null;
goog.dom.removeNode(this.HtmlDiv);
this.workspace_ = null;
this.lastCategory_ = null;
};
/**
* Create and configure a flyout based on the main workspace's options.
* @private
*/
Blockly.Toolbox.prototype.createFlyout_ = function() {
var workspace = this.workspace_;
var options = {
disabledPatternId: workspace.options.disabledPatternId,
parentWorkspace: workspace,
RTL: workspace.RTL,
oneBasedIndex: workspace.options.oneBasedIndex,
horizontalLayout: workspace.horizontalLayout,
toolboxPosition: workspace.options.toolboxPosition,
stackGlowFilterId: workspace.options.stackGlowFilterId,
pathToMedia: workspace.options.pathToMedia
};
if (workspace.horizontalLayout) {
this.flyout_ = new Blockly.HorizontalFlyout(options);
} else {
this.flyout_ = new Blockly.VerticalFlyout(options);
}
this.flyout_.setParentToolbox(this);
goog.dom.insertSiblingAfter(
this.flyout_.createDom('svg'), this.workspace_.getParentSvg());
this.flyout_.init(workspace);
};
/**
* Fill the toolbox with categories and blocks.
* @param {!Node} newTree DOM tree of blocks.
* @private
*/
Blockly.Toolbox.prototype.populate_ = function(newTree) {
this.categoryMenu_.populate(newTree);
this.showAll_();
this.setSelectedItem(this.categoryMenu_.categories_[0], false);
};
/**
* Show all blocks for all categories in the flyout
* @private
*/
Blockly.Toolbox.prototype.showAll_ = function() {
var allContents = [];
for (var i = 0; i < this.categoryMenu_.categories_.length; i++) {
var category = this.categoryMenu_.categories_[i];
// create a label node to go at the top of the category
var labelString = '<xml><label text="' + goog.string.htmlEscape(category.name_) + '"' +
' id="' + goog.string.htmlEscape(category.id_) + '"' +
' category-label="true"' +
' showStatusButton="' + goog.string.htmlEscape(category.showStatusButton_) + '"' +
' web-class="categoryLabel">' +
'</label></xml>';
var labelXML = Blockly.Xml.textToDom(labelString);
allContents.push(labelXML.firstChild);
allContents = allContents.concat(category.getContents());
}
this.flyout_.show(allContents);
};
/**
* Get the width of the toolbox.
* @return {number} The width of the toolbox.
*/
Blockly.Toolbox.prototype.getWidth = function() {
return this.width;
};
/**
* Get the height of the toolbox, not including the block menu.
* @return {number} The height of the toolbox.
*/
Blockly.Toolbox.prototype.getHeight = function() {
return this.categoryMenu_ ? this.categoryMenu_.getHeight() : 0;
};
/**
* Move the toolbox to the edge.
*/
Blockly.Toolbox.prototype.position = function() {
var treeDiv = this.HtmlDiv;
if (!treeDiv) {
// Not initialized yet.
return;
}
var svg = this.workspace_.getParentSvg();
var svgSize = Blockly.svgSize(svg);
if (this.horizontalLayout_) {
treeDiv.style.left = '0';
treeDiv.style.height = 'auto';
treeDiv.style.width = svgSize.width + 'px';
this.height = treeDiv.offsetHeight;
if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { // Top
treeDiv.style.top = '0';
} else { // Bottom
treeDiv.style.bottom = '0';
}
} else {
if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { // Right
treeDiv.style.right = '0';
} else { // Left
treeDiv.style.left = '0';
}
treeDiv.style.height = '100%';
}
this.flyout_.position();
};
/**
* Unhighlight any previously specified option.
*/
Blockly.Toolbox.prototype.clearSelection = function() {
this.setSelectedItem(null);
};
/**
* Adds a style on the toolbox. Usually used to change the cursor.
* @param {string} style The name of the class to add.
* @package
*/
Blockly.Toolbox.prototype.addStyle = function(style) {
Blockly.utils.addClass(/** @type {!Element} */ (this.HtmlDiv), style);
};
/**
* Removes a style from the toolbox. Usually used to change the cursor.
* @param {string} style The name of the class to remove.
* @package
*/
Blockly.Toolbox.prototype.removeStyle = function(style) {
Blockly.utils.removeClass(/** @type {!Element} */ (this.HtmlDiv), style);
};
/**
* Return the deletion rectangle for this toolbox.
* @return {goog.math.Rect} Rectangle in which to delete.
*/
Blockly.Toolbox.prototype.getClientRect = function() {
if (!this.HtmlDiv) {
return null;
}
// If not an auto closing flyout, always use the (larger) flyout client rect
if (!this.flyout_.autoClose) {
return this.flyout_.getClientRect();
}
// BIG_NUM is offscreen padding so that blocks dragged beyond the toolbox
// area are still deleted. Must be smaller than Infinity, but larger than
// the largest screen size.
var BIG_NUM = 10000000;
var toolboxRect = this.HtmlDiv.getBoundingClientRect();
var x = toolboxRect.left;
var y = toolboxRect.top;
var width = toolboxRect.width;
var height = toolboxRect.height;
// Assumes that the toolbox is on the SVG edge. If this changes
// (e.g. toolboxes in mutators) then this code will need to be more complex.
if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, BIG_NUM + x + width,
2 * BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
return new goog.math.Rect(toolboxRect.right - width, -BIG_NUM, BIG_NUM + width, 2 * BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, 2 * BIG_NUM,
BIG_NUM + y + height);
} else { // Bottom
return new goog.math.Rect(0, y, 2 * BIG_NUM, BIG_NUM);
}
};
/**
* Update the flyout's contents without closing it. Should be used in response
* to a change in one of the dynamic categories, such as variables or
* procedures.
*/
Blockly.Toolbox.prototype.refreshSelection = function() {
this.showAll_();
};
/**
* @return {Blockly.Toolbox.Category} the currently selected category.
*/
Blockly.Toolbox.prototype.getSelectedItem = function() {
return this.selectedItem_;
};
/**
* @return {string} The name of the currently selected category.
*/
Blockly.Toolbox.prototype.getSelectedCategoryName = function() {
return this.selectedItem_.name_;
};
/**
* @return {string} The id of the currently selected category.
* @public
*/
Blockly.Toolbox.prototype.getSelectedCategoryId = function() {
return this.selectedItem_.id_;
};
/**
* @return {number} The distance flyout is scrolled below the top of the currently
* selected category.
*/
Blockly.Toolbox.prototype.getCategoryScrollOffset = function() {
var categoryPos = this.getCategoryPositionById(this.getSelectedCategoryId());
return this.flyout_.getScrollPos() - categoryPos;
};
/**
* Get the position of a category by name.
* @param {string} name The name of the category.
* @return {number} The position of the category.
*/
Blockly.Toolbox.prototype.getCategoryPositionByName = function(name) {
var scrollPositions = this.flyout_.categoryScrollPositions;
for (var i = 0; i < scrollPositions.length; i++) {
if (name === scrollPositions[i].categoryName) {
return scrollPositions[i].position;
}
}
};
/**
* Get the position of a category by id.
* @param {string} id The id of the category.
* @return {number} The position of the category.
* @public
*/
Blockly.Toolbox.prototype.getCategoryPositionById = function(id) {
var scrollPositions = this.flyout_.categoryScrollPositions;
for (var i = 0; i < scrollPositions.length; i++) {
if (id === scrollPositions[i].categoryId) {
return scrollPositions[i].position;
}
}
};
/**
* Get the length of a category by name.
* @param {string} name The name of the category.
* @return {number} The length of the category.
*/
Blockly.Toolbox.prototype.getCategoryLengthByName = function(name) {
var scrollPositions = this.flyout_.categoryScrollPositions;
for (var i = 0; i < scrollPositions.length; i++) {
if (name === scrollPositions[i].categoryName) {
return scrollPositions[i].length;
}
}
};
/**
* Get the length of a category by id.
* @param {string} id The id of the category.
* @return {number} The length of the category.
* @public
*/
Blockly.Toolbox.prototype.getCategoryLengthById = function(id) {
var scrollPositions = this.flyout_.categoryScrollPositions;
for (var i = 0; i < scrollPositions.length; i++) {
if (id === scrollPositions[i].categoryId) {
return scrollPositions[i].length;
}
}
};
/**
* Set the scroll position of the flyout.
* @param {number} pos The position to set.
*/
Blockly.Toolbox.prototype.setFlyoutScrollPos = function(pos) {
this.flyout_.setScrollPos(pos);
};
/**
* Set the currently selected category.
* @param {Blockly.Toolbox.Category} item The category to select.
* @param {boolean=} opt_shouldScroll Whether to scroll to the selected category. Defaults to true.
*/
Blockly.Toolbox.prototype.setSelectedItem = function(item, opt_shouldScroll) {
if (typeof opt_shouldScroll === 'undefined') {
opt_shouldScroll = true;
}
if (this.selectedItem_) {
// They selected a different category but one was already open. Close it.
this.selectedItem_.setSelected(false);
}
this.selectedItem_ = item;
if (this.selectedItem_ != null) {
this.selectedItem_.setSelected(true);
// Scroll flyout to the top of the selected category
var categoryId = item.id_;
if (opt_shouldScroll) {
this.scrollToCategoryById(categoryId);
}
}
};
/**
* Select and scroll to a category by name.
* @param {string} name The name of the category to select and scroll to.
*/
Blockly.Toolbox.prototype.setSelectedCategoryByName = function(name) {
this.selectCategoryByName(name);
this.scrollToCategoryByName(name);
};
/**
* Select and scroll to a category by id.
* @param {string} id The id of the category to select and scroll to.
* @public
*/
Blockly.Toolbox.prototype.setSelectedCategoryById = function(id) {
this.selectCategoryById(id);
this.scrollToCategoryById(id);
};
/**
* Scroll to a category by name.
* @param {string} name The name of the category to scroll to.
* @package
*/
Blockly.Toolbox.prototype.scrollToCategoryByName = function(name) {
var scrollPositions = this.flyout_.categoryScrollPositions;
for (var i = 0; i < scrollPositions.length; i++) {
if (name === scrollPositions[i].categoryName) {
this.flyout_.setVisible(true);
this.flyout_.scrollTo(scrollPositions[i].position);
return;
}
}
};
/**
* Scroll to a category by id.
* @param {string} id The id of the category to scroll to.
* @public
*/
Blockly.Toolbox.prototype.scrollToCategoryById = function(id) {
var scrollPositions = this.flyout_.categoryScrollPositions;
for (var i = 0; i < scrollPositions.length; i++) {
if (id === scrollPositions[i].categoryId) {
this.flyout_.setVisible(true);
this.flyout_.scrollTo(scrollPositions[i].position);
return;
}
}
};
/**
* Get a category by its index.
* @param {number} index The index of the category.
* @return {Blockly.Toolbox.Category} the category, or null if there are no categories.
* @package
*/
Blockly.Toolbox.prototype.getCategoryByIndex = function(index) {
if (!this.categoryMenu_.categories_) return null;
return this.categoryMenu_.categories_[index];
};
/**
* Select a category by name.
* @param {string} name The name of the category to select.
* @package
*/
Blockly.Toolbox.prototype.selectCategoryByName = function(name) {
for (var i = 0; i < this.categoryMenu_.categories_.length; i++) {
var category = this.categoryMenu_.categories_[i];
if (name === category.name_) {
this.selectedItem_.setSelected(false);
this.selectedItem_ = category;
this.selectedItem_.setSelected(true);
}
}
};
/**
* Select a category by id.
* @param {string} id The id of the category to select.
* @package
*/
Blockly.Toolbox.prototype.selectCategoryById = function(id) {
for (var i = 0; i < this.categoryMenu_.categories_.length; i++) {
var category = this.categoryMenu_.categories_[i];
if (id === category.id_) {
this.selectedItem_.setSelected(false);
this.selectedItem_ = category;
this.selectedItem_.setSelected(true);
}
}
};
/**
* Wrapper function for calling setSelectedItem from a touch handler.
* @param {Blockly.Toolbox.Category} item The category to select.
* @return {function} A function that can be passed to bindEvent.
*/
Blockly.Toolbox.prototype.setSelectedItemFactory = function(item) {
var selectedItem = item;
return function() {
if (!this.workspace_.isDragging()) {
this.setSelectedItem(selectedItem);
Blockly.Touch.clearTouchIdentifier();
}
};
};
// Category menu
/**
* Class for a table of category titles that will control which category is
* displayed.
* @param {Blockly.Toolbox} parent The toolbox that owns the category menu.
* @param {Element} parentHtml The containing html div.
* @constructor
*/
Blockly.Toolbox.CategoryMenu = function(parent, parentHtml) {
this.parent_ = parent;
this.height_ = 0;
this.parentHtml_ = parentHtml;
this.createDom();
this.categories_ = [];
};
/**
* @return {number} the height of the category menu.
*/
Blockly.Toolbox.CategoryMenu.prototype.getHeight = function() {
return this.height_;
};
/**
* Create the DOM for the category menu.
*/
Blockly.Toolbox.CategoryMenu.prototype.createDom = function() {
this.table = goog.dom.createDom('div', this.parent_.horizontalLayout_ ?
'scratchCategoryMenuHorizontal' : 'scratchCategoryMenu');
this.parentHtml_.appendChild(this.table);
};
/**
* Fill the toolbox with categories and blocks by creating a new
* {Blockly.Toolbox.Category} for every category tag in the toolbox xml.
* @param {Node} domTree DOM tree of blocks, or null.
*/
Blockly.Toolbox.CategoryMenu.prototype.populate = function(domTree) {
if (!domTree) {
return;
}
// Remove old categories
this.dispose();
this.createDom();
var categories = [];
// Find actual categories from the DOM tree.
for (var i = 0, child; child = domTree.childNodes[i]; i++) {
if (!child.tagName || child.tagName.toUpperCase() != 'CATEGORY') {
continue;
}
categories.push(child);
}
// Create a single column of categories
for (var i = 0; i < categories.length; i++) {
var child = categories[i];
var row = goog.dom.createDom('div', 'scratchCategoryMenuRow');
this.table.appendChild(row);
if (child) {
this.categories_.push(new Blockly.Toolbox.Category(this, row,
child));
}
}
this.height_ = this.table.offsetHeight;
};
/**
* Dispose of this Category Menu and all of its children.
*/
Blockly.Toolbox.CategoryMenu.prototype.dispose = function() {
for (var i = 0, category; category = this.categories_[i]; i++) {
category.dispose();
}
this.categories_ = [];
if (this.table) {
goog.dom.removeNode(this.table);
this.table = null;
}
};
// Category
/**
* Class for the data model of a category in the toolbox.
* @param {Blockly.Toolbox.CategoryMenu} parent The category menu that owns this
* category.
* @param {Element} parentHtml The containing html div.
* @param {Node} domTree DOM tree of blocks.
* @constructor
*/
Blockly.Toolbox.Category = function(parent, parentHtml, domTree) {
this.parent_ = parent;
this.parentHtml_ = parentHtml;
this.name_ = domTree.getAttribute('name');
this.id_ = domTree.getAttribute('id');
this.setColour(domTree);
this.custom_ = domTree.getAttribute('custom');
this.iconURI_ = domTree.getAttribute('iconURI');
this.showStatusButton_ = domTree.getAttribute('showStatusButton');
this.contents_ = [];
if (!this.custom_) {
this.parseContents_(domTree);
}
this.createDom();
};
/**
* Dispose of this category and all of its contents.
*/
Blockly.Toolbox.Category.prototype.dispose = function() {
if (this.item_) {
goog.dom.removeNode(this.item_);
this.item = null;
}
this.parent_ = null;
this.parentHtml_ = null;
this.contents_ = null;
};
/**
* Used to determine the css classes for the menu item for this category
* based on its current state.
* @private
* @param {boolean=} selected Indication whether the category is currently selected.
* @return {string} The css class names to be applied, space-separated.
*/
Blockly.Toolbox.Category.prototype.getMenuItemClassName_ = function(selected) {
var classNames = [
'scratchCategoryMenuItem',
'scratchCategoryId-' + this.id_,
];
if (selected) {
classNames.push('categorySelected');
}
return classNames.join(' ');
};
/**
* Create the DOM for a category in the toolbox.
*/
Blockly.Toolbox.Category.prototype.createDom = function() {
var toolbox = this.parent_.parent_;
this.item_ = goog.dom.createDom('div',
{'class': this.getMenuItemClassName_()});
this.label_ = goog.dom.createDom('div',
{'class': 'scratchCategoryMenuItemLabel'},
Blockly.utils.replaceMessageReferences(this.name_));
if (this.iconURI_) {
this.bubble_ = goog.dom.createDom('div',
{'class': 'scratchCategoryItemIcon'});
this.bubble_.style.backgroundImage = 'url(' + this.iconURI_ + ')';
} else {
this.bubble_ = goog.dom.createDom('div',
{'class': 'scratchCategoryItemBubble'});
this.bubble_.style.backgroundColor = this.colour_;
this.bubble_.style.borderColor = this.secondaryColour_;
}
this.item_.appendChild(this.bubble_);
this.item_.appendChild(this.label_);
this.parentHtml_.appendChild(this.item_);
Blockly.bindEvent_(
this.item_, 'mouseup', toolbox, toolbox.setSelectedItemFactory(this));
};
/**
* Set the selected state of this category.
* @param {boolean} selected Whether this category is selected.
*/
Blockly.Toolbox.Category.prototype.setSelected = function(selected) {
this.item_.className = this.getMenuItemClassName_(selected);
};
/**
* Set the contents of this category from DOM.
* @param {Node} domTree DOM tree of blocks.
* @constructor
*/
Blockly.Toolbox.Category.prototype.parseContents_ = function(domTree) {
for (var i = 0, child; child = domTree.childNodes[i]; i++) {
if (!child.tagName) {
// Skip
continue;
}
switch (child.tagName.toUpperCase()) {
case 'BLOCK':
case 'SHADOW':
case 'LABEL':
case 'BUTTON':
case 'SEP':
case 'TEXT':
this.contents_.push(child);
break;
default:
break;
}
}
};
/**
* Get the contents of this category.
* @return {!Array|string} xmlList List of blocks to show, or a string with the
* name of a custom category.
*/
Blockly.Toolbox.Category.prototype.getContents = function() {
return this.custom_ ? this.custom_ : this.contents_;
};
/**
* Set the colour of the category's background from a DOM node.
* @param {Node} node DOM node with "colour" and "secondaryColour" attribute.
* Colours are a hex string or hue on a colour wheel (0-360).
*/
Blockly.Toolbox.Category.prototype.setColour = function(node) {
var colour = node.getAttribute('colour');
var secondaryColour = node.getAttribute('secondaryColour');
if (goog.isString(colour)) {
if (colour.match(/^#[0-9a-fA-F]{6,8}$/)) {
this.colour_ = colour;
} else {
this.colour_ = Blockly.hueToRgb(colour);
}
if (secondaryColour.match(/^#[0-9a-fA-F]{6,8}$/)) {
this.secondaryColour_ = secondaryColour;
} else {
this.secondaryColour_ = Blockly.hueToRgb(secondaryColour);
}
this.hasColours_ = true;
} else {
this.colour_ = '#000000';
this.secondaryColour_ = '#000000';
}
};

View File

@@ -0,0 +1,337 @@
/**
* @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 Library to create tooltips for Blockly.
* First, call Blockly.Tooltip.init() after onload.
* Second, set the 'tooltip' property on any SVG element that needs a tooltip.
* If the tooltip is a string, then that message will be displayed.
* If the tooltip is an SVG element, then that object's tooltip will be used.
* Third, call Blockly.Tooltip.bindMouseEvents(e) passing the SVG element.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* @name Blockly.Tooltip
* @namespace
**/
goog.provide('Blockly.Tooltip');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
/**
* Is a tooltip currently showing?
*/
Blockly.Tooltip.visible = false;
/**
* Is someone else blocking the tooltip from being shown?
* @type {boolean}
* @private
*/
Blockly.Tooltip.blocked_ = false;
/**
* Maximum width (in characters) of a tooltip.
*/
Blockly.Tooltip.LIMIT = 50;
/**
* PID of suspended thread to clear tooltip on mouse out.
* @private
*/
Blockly.Tooltip.mouseOutPid_ = 0;
/**
* PID of suspended thread to show the tooltip.
* @private
*/
Blockly.Tooltip.showPid_ = 0;
/**
* Last observed X location of the mouse pointer (freezes when tooltip appears).
* @private
*/
Blockly.Tooltip.lastX_ = 0;
/**
* Last observed Y location of the mouse pointer (freezes when tooltip appears).
* @private
*/
Blockly.Tooltip.lastY_ = 0;
/**
* Current element being pointed at.
* @private
*/
Blockly.Tooltip.element_ = null;
/**
* Once a tooltip has opened for an element, that element is 'poisoned' and
* cannot respawn a tooltip until the pointer moves over a different element.
* @private
*/
Blockly.Tooltip.poisonedElement_ = null;
/**
* Horizontal offset between mouse cursor and tooltip.
*/
Blockly.Tooltip.OFFSET_X = 0;
/**
* Vertical offset between mouse cursor and tooltip.
*/
Blockly.Tooltip.OFFSET_Y = 10;
/**
* Radius mouse can move before killing tooltip.
*/
Blockly.Tooltip.RADIUS_OK = 10;
/**
* Delay before tooltip appears.
*/
Blockly.Tooltip.HOVER_MS = 750;
/**
* Horizontal padding between tooltip and screen edge.
*/
Blockly.Tooltip.MARGINS = 5;
/**
* The HTML container. Set once by Blockly.Tooltip.createDom.
* @type {Element}
*/
Blockly.Tooltip.DIV = null;
/**
* Create the tooltip div and inject it onto the page.
*/
Blockly.Tooltip.createDom = function() {
if (Blockly.Tooltip.DIV) {
return; // Already created.
}
// Create an HTML container for popup overlays (e.g. editor widgets).
Blockly.Tooltip.DIV =
goog.dom.createDom(goog.dom.TagName.DIV, 'blocklyTooltipDiv');
document.body.appendChild(Blockly.Tooltip.DIV);
};
/**
* Binds the required mouse events onto an SVG element.
* @param {!Element} element SVG element onto which tooltip is to be bound.
*/
Blockly.Tooltip.bindMouseEvents = function(element) {
Blockly.bindEvent_(element, 'mouseover', null,
Blockly.Tooltip.onMouseOver_);
Blockly.bindEvent_(element, 'mouseout', null,
Blockly.Tooltip.onMouseOut_);
// Don't use bindEvent_ for mousemove since that would create a
// corresponding touch handler, even though this only makes sense in the
// context of a mouseover/mouseout.
element.addEventListener('mousemove', Blockly.Tooltip.onMouseMove_, false);
};
/**
* Hide the tooltip if the mouse is over a different object.
* Initialize the tooltip to potentially appear for this object.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.Tooltip.onMouseOver_ = function(e) {
if (Blockly.Tooltip.blocked_) {
// Someone doesn't want us to show tooltips.
return;
}
// If the tooltip is an object, treat it as a pointer to the next object in
// the chain to look at. Terminate when a string or function is found.
var element = e.target;
while (!goog.isString(element.tooltip) && !goog.isFunction(element.tooltip)) {
element = element.tooltip;
}
if (Blockly.Tooltip.element_ != element) {
Blockly.Tooltip.hide();
Blockly.Tooltip.poisonedElement_ = null;
Blockly.Tooltip.element_ = element;
}
// Forget about any immediately preceding mouseOut event.
clearTimeout(Blockly.Tooltip.mouseOutPid_);
};
/**
* Hide the tooltip if the mouse leaves the object and enters the workspace.
* @param {!Event} _e Mouse event.
* @private
*/
Blockly.Tooltip.onMouseOut_ = function(_e) {
if (Blockly.Tooltip.blocked_) {
// Someone doesn't want us to show tooltips.
return;
}
// Moving from one element to another (overlapping or with no gap) generates
// a mouseOut followed instantly by a mouseOver. Fork off the mouseOut
// event and kill it if a mouseOver is received immediately.
// This way the task only fully executes if mousing into the void.
Blockly.Tooltip.mouseOutPid_ = setTimeout(function() {
Blockly.Tooltip.element_ = null;
Blockly.Tooltip.poisonedElement_ = null;
Blockly.Tooltip.hide();
}, 1);
clearTimeout(Blockly.Tooltip.showPid_);
};
/**
* When hovering over an element, schedule a tooltip to be shown. If a tooltip
* is already visible, hide it if the mouse strays out of a certain radius.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.Tooltip.onMouseMove_ = function(e) {
if (!Blockly.Tooltip.element_ || !Blockly.Tooltip.element_.tooltip) {
// No tooltip here to show.
return;
} else if (Blockly.WidgetDiv.isVisible()) {
// Don't display a tooltip if a widget is open (tooltip would be under it).
return;
} else if (Blockly.Tooltip.blocked_) {
// Someone doesn't want us to show tooltips. We are probably handling a
// user gesture, such as a click or drag.
return;
}
if (Blockly.Tooltip.visible) {
// Compute the distance between the mouse position when the tooltip was
// shown and the current mouse position. Pythagorean theorem.
var dx = Blockly.Tooltip.lastX_ - e.pageX;
var dy = Blockly.Tooltip.lastY_ - e.pageY;
if (Math.sqrt(dx * dx + dy * dy) > Blockly.Tooltip.RADIUS_OK) {
Blockly.Tooltip.hide();
}
} else if (Blockly.Tooltip.poisonedElement_ != Blockly.Tooltip.element_) {
// The mouse moved, clear any previously scheduled tooltip.
clearTimeout(Blockly.Tooltip.showPid_);
// Maybe this time the mouse will stay put. Schedule showing of tooltip.
Blockly.Tooltip.lastX_ = e.pageX;
Blockly.Tooltip.lastY_ = e.pageY;
Blockly.Tooltip.showPid_ =
setTimeout(Blockly.Tooltip.show_, Blockly.Tooltip.HOVER_MS);
}
};
/**
* Hide the tooltip.
*/
Blockly.Tooltip.hide = function() {
if (Blockly.Tooltip.visible) {
Blockly.Tooltip.visible = false;
if (Blockly.Tooltip.DIV) {
Blockly.Tooltip.DIV.style.display = 'none';
}
}
if (Blockly.Tooltip.showPid_) {
clearTimeout(Blockly.Tooltip.showPid_);
}
};
/**
* Hide any in-progress tooltips and block showing new tooltips until the next
* call to unblock().
* @package
*/
Blockly.Tooltip.block = function() {
Blockly.Tooltip.hide();
Blockly.Tooltip.blocked_ = true;
};
/**
* Unblock tooltips: allow them to be scheduled and shown according to their own
* logic.
* @package
*/
Blockly.Tooltip.unblock = function() {
Blockly.Tooltip.blocked_ = false;
};
/**
* Create the tooltip and show it.
* @private
*/
Blockly.Tooltip.show_ = function() {
if (Blockly.Tooltip.blocked_) {
// Someone doesn't want us to show tooltips.
return;
}
Blockly.Tooltip.poisonedElement_ = Blockly.Tooltip.element_;
if (!Blockly.Tooltip.DIV) {
return;
}
// Erase all existing text.
goog.dom.removeChildren(/** @type {!Element} */ (Blockly.Tooltip.DIV));
// Get the new text.
var tip = Blockly.Tooltip.element_.tooltip;
while (goog.isFunction(tip)) {
tip = tip();
}
tip = Blockly.utils.wrap(tip, Blockly.Tooltip.LIMIT);
// Create new text, line by line.
var lines = tip.split('\n');
for (var i = 0; i < lines.length; i++) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(lines[i]));
Blockly.Tooltip.DIV.appendChild(div);
}
var rtl = Blockly.Tooltip.element_.RTL;
var windowSize = goog.dom.getViewportSize();
// Display the tooltip.
Blockly.Tooltip.DIV.style.direction = rtl ? 'rtl' : 'ltr';
Blockly.Tooltip.DIV.style.display = 'block';
Blockly.Tooltip.visible = true;
// Move the tooltip to just below the cursor.
var anchorX = Blockly.Tooltip.lastX_;
if (rtl) {
anchorX -= Blockly.Tooltip.OFFSET_X + Blockly.Tooltip.DIV.offsetWidth;
} else {
anchorX += Blockly.Tooltip.OFFSET_X;
}
var anchorY = Blockly.Tooltip.lastY_ + Blockly.Tooltip.OFFSET_Y;
if (anchorY + Blockly.Tooltip.DIV.offsetHeight >
windowSize.height + window.scrollY) {
// Falling off the bottom of the screen; shift the tooltip up.
anchorY -= Blockly.Tooltip.DIV.offsetHeight + 2 * Blockly.Tooltip.OFFSET_Y;
}
if (rtl) {
// Prevent falling off left edge in RTL mode.
anchorX = Math.max(Blockly.Tooltip.MARGINS - window.scrollX, anchorX);
} else {
if (anchorX + Blockly.Tooltip.DIV.offsetWidth >
windowSize.width + window.scrollX - 2 * Blockly.Tooltip.MARGINS) {
// Falling off the right edge of the screen;
// clamp the tooltip on the edge.
anchorX = windowSize.width - Blockly.Tooltip.DIV.offsetWidth -
2 * Blockly.Tooltip.MARGINS;
}
}
Blockly.Tooltip.DIV.style.top = anchorY + 'px';
Blockly.Tooltip.DIV.style.left = anchorX + 'px';
};

View File

@@ -0,0 +1,226 @@
/**
* @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 Touch handling for Blockly.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
/**
* @name Blockly.Touch
* @namespace
**/
goog.provide('Blockly.Touch');
goog.require('goog.events');
goog.require('goog.events.BrowserFeature');
goog.require('goog.string');
/**
* Which touch events are we currently paying attention to?
* @type {DOMString}
* @private
*/
Blockly.Touch.touchIdentifier_ = null;
/**
* The TOUCH_MAP lookup dictionary specifies additional touch events to fire,
* in conjunction with mouse events.
* @type {Object}
*/
Blockly.Touch.TOUCH_MAP = {};
if (goog.events.BrowserFeature.TOUCH_ENABLED) {
Blockly.Touch.TOUCH_MAP = {
'mousedown': ['touchstart'],
'mousemove': ['touchmove'],
'mouseup': ['touchend', 'touchcancel']
};
}
/**
* PID of queued long-press task.
* @private
*/
Blockly.longPid_ = 0;
/**
* Context menus on touch devices are activated using a long-press.
* Unfortunately the contextmenu touch event is currently (2015) only supported
* by Chrome. This function is fired on any touchstart event, queues a task,
* which after about a second opens the context menu. The tasks is killed
* if the touch event terminates early.
* @param {!Event} e Touch start event.
* @param {Blockly.Gesture} gesture The gesture that triggered this longStart.
* @private
*/
Blockly.longStart_ = function(e, gesture) {
Blockly.longStop_();
// Punt on multitouch events.
if (e.changedTouches.length != 1) {
return;
}
Blockly.longPid_ = setTimeout(function() {
e.button = 2; // Simulate a right button click.
// e was a touch event. It needs to pretend to be a mouse event.
e.clientX = e.changedTouches[0].clientX;
e.clientY = e.changedTouches[0].clientY;
// Let the gesture route the right-click correctly.
if (gesture) {
gesture.handleRightClick(e);
}
}, Blockly.LONGPRESS);
};
/**
* Nope, that's not a long-press. Either touchend or touchcancel was fired,
* or a drag hath begun. Kill the queued long-press task.
* @private
*/
Blockly.longStop_ = function() {
if (Blockly.longPid_) {
clearTimeout(Blockly.longPid_);
Blockly.longPid_ = 0;
}
};
/**
* Clear the touch identifier that tracks which touch stream to pay attention
* to. This ends the current drag/gesture and allows other pointers to be
* captured.
*/
Blockly.Touch.clearTouchIdentifier = function() {
Blockly.Touch.touchIdentifier_ = null;
};
/**
* Decide whether Blockly should handle or ignore this event.
* Mouse and touch events require special checks because we only want to deal
* with one touch stream at a time. All other events should always be handled.
* @param {!Event} e The event to check.
* @return {boolean} True if this event should be passed through to the
* registered handler; false if it should be blocked.
*/
Blockly.Touch.shouldHandleEvent = function(e) {
return !Blockly.Touch.isMouseOrTouchEvent(e) ||
Blockly.Touch.checkTouchIdentifier(e);
};
/**
* Get the touch identifier from the given event. If it was a mouse event, the
* identifier is the string 'mouse'.
* @param {!Event} e Mouse event or touch event.
* @return {string} The touch identifier from the first changed touch, if
* defined. Otherwise 'mouse'.
*/
Blockly.Touch.getTouchIdentifierFromEvent = function(e) {
return (e.changedTouches && e.changedTouches[0] &&
e.changedTouches[0].identifier != undefined &&
e.changedTouches[0].identifier != null) ?
e.changedTouches[0].identifier : 'mouse';
};
/**
* Check whether the touch identifier on the event matches the current saved
* identifier. If there is no identifier, that means it's a mouse event and
* we'll use the identifier "mouse". This means we won't deal well with
* multiple mice being used at the same time. That seems okay.
* If the current identifier was unset, save the identifier from the
* event. This starts a drag/gesture, during which touch events with other
* identifiers will be silently ignored.
* @param {!Event} e Mouse event or touch event.
* @return {boolean} Whether the identifier on the event matches the current
* saved identifier.
*/
Blockly.Touch.checkTouchIdentifier = function(e) {
var identifier = Blockly.Touch.getTouchIdentifierFromEvent(e);
// if (Blockly.touchIdentifier_ )is insufficient because Android touch
// identifiers may be zero.
if (Blockly.Touch.touchIdentifier_ != undefined &&
Blockly.Touch.touchIdentifier_ != null) {
// We're already tracking some touch/mouse event. Is this from the same
// source?
return Blockly.Touch.touchIdentifier_ == identifier;
}
if (e.type == 'mousedown' || e.type == 'touchstart') {
// No identifier set yet, and this is the start of a drag. Set it and
// return.
Blockly.Touch.touchIdentifier_ = identifier;
return true;
}
// There was no identifier yet, but this wasn't a start event so we're going
// to ignore it. This probably means that another drag finished while this
// pointer was down.
return false;
};
/**
* Set an event's clientX and clientY from its first changed touch. Use this to
* make a touch event work in a mouse event handler.
* @param {!Event} e A touch event.
*/
Blockly.Touch.setClientFromTouch = function(e) {
if (Blockly.utils.startsWith(e.type, 'touch')) {
// Map the touch event's properties to the event.
var touchPoint = e.changedTouches[0];
e.clientX = touchPoint.clientX;
e.clientY = touchPoint.clientY;
}
};
/**
* Check whether a given event is a mouse or touch event.
* @param {!Event} e An event.
* @return {boolean} true if it is a mouse or touch event; false otherwise.
*/
Blockly.Touch.isMouseOrTouchEvent = function(e) {
return Blockly.utils.startsWith(e.type, 'touch') ||
Blockly.utils.startsWith(e.type, 'mouse');
};
/**
* Split an event into an array of events, one per changed touch or mouse
* point.
* @param {!Event} e A mouse event or a touch event with one or more changed
* touches.
* @return {!Array.<!Event>} An array of mouse or touch events. Each touch
* event will have exactly one changed touch.
*/
Blockly.Touch.splitEventByTouches = function(e) {
var events = [];
if (e.changedTouches) {
for (var i = 0; i < e.changedTouches.length; i++) {
var newEvent = {
type: e.type,
changedTouches: [e.changedTouches[i]],
target: e.target,
stopPropagation: function(){ e.stopPropagation(); },
preventDefault: function(){ e.preventDefault(); }
};
events[i] = newEvent;
}
} else {
events.push(e);
}
return events;
};

View File

@@ -0,0 +1,343 @@
/**
* @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 Object representing a trash can icon.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Trashcan');
goog.require('goog.dom');
goog.require('goog.math.Rect');
/**
* Class for a trash can.
* @param {!Blockly.Workspace} workspace The workspace to sit in.
* @constructor
*/
Blockly.Trashcan = function(workspace) {
this.workspace_ = workspace;
};
/**
* Width of both the trash can and lid images.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.WIDTH_ = 47;
/**
* Height of the trashcan image (minus lid).
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.BODY_HEIGHT_ = 44;
/**
* Height of the lid image.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.LID_HEIGHT_ = 16;
/**
* Distance between trashcan and bottom edge of workspace.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.MARGIN_BOTTOM_ = 20;
/**
* Distance between trashcan and right edge of workspace.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.MARGIN_SIDE_ = 20;
/**
* Extent of hotspot on all sides beyond the size of the image.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.MARGIN_HOTSPOT_ = 10;
/**
* Location of trashcan in sprite image.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.SPRITE_LEFT_ = 0;
/**
* Location of trashcan in sprite image.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.SPRITE_TOP_ = 32;
/**
* Current open/close state of the lid.
* @type {boolean}
*/
Blockly.Trashcan.prototype.isOpen = false;
/**
* The SVG group containing the trash can.
* @type {Element}
* @private
*/
Blockly.Trashcan.prototype.svgGroup_ = null;
/**
* The SVG image element of the trash can lid.
* @type {Element}
* @private
*/
Blockly.Trashcan.prototype.svgLid_ = null;
/**
* Task ID of opening/closing animation.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.lidTask_ = 0;
/**
* Current state of lid opening (0.0 = closed, 1.0 = open).
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.lidOpen_ = 0;
/**
* Left coordinate of the trash can.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.left_ = 0;
/**
* Top coordinate of the trash can.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.top_ = 0;
/**
* Create the trash can elements.
* @return {!Element} The trash can's SVG group.
*/
Blockly.Trashcan.prototype.createDom = function() {
/* Here's the markup that will be generated:
<g class="blocklyTrash">
<clippath id="blocklyTrashBodyClipPath837493">
<rect width="47" height="45" y="15"></rect>
</clippath>
<image width="64" height="92" y="-32" xlink:href="media/sprites.png"
clip-path="url(#blocklyTrashBodyClipPath837493)"></image>
<clippath id="blocklyTrashLidClipPath837493">
<rect width="47" height="15"></rect>
</clippath>
<image width="84" height="92" y="-32" xlink:href="media/sprites.png"
clip-path="url(#blocklyTrashLidClipPath837493)"></image>
</g>
*/
this.svgGroup_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyTrash'}, null);
var clip;
var rnd = String(Math.random()).substring(2);
clip = Blockly.utils.createSvgElement('clipPath',
{'id': 'blocklyTrashBodyClipPath' + rnd},
this.svgGroup_);
Blockly.utils.createSvgElement('rect',
{
'width': this.WIDTH_,
'height': this.BODY_HEIGHT_,
'y': this.LID_HEIGHT_
},
clip);
var body = Blockly.utils.createSvgElement('image',
{
'width': Blockly.SPRITE.width,
'x': -this.SPRITE_LEFT_,
'height': Blockly.SPRITE.height,
'y': -this.SPRITE_TOP_,
'clip-path': 'url(#blocklyTrashBodyClipPath' + rnd + ')'
},
this.svgGroup_);
body.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
this.workspace_.options.pathToMedia + Blockly.SPRITE.url);
clip = Blockly.utils.createSvgElement('clipPath',
{'id': 'blocklyTrashLidClipPath' + rnd},
this.svgGroup_);
Blockly.utils.createSvgElement('rect',
{'width': this.WIDTH_, 'height': this.LID_HEIGHT_}, clip);
this.svgLid_ = Blockly.utils.createSvgElement('image',
{
'width': Blockly.SPRITE.width,
'x': -this.SPRITE_LEFT_,
'height': Blockly.SPRITE.height,
'y': -this.SPRITE_TOP_,
'clip-path': 'url(#blocklyTrashLidClipPath' + rnd + ')'
},
this.svgGroup_);
this.svgLid_.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
this.workspace_.options.pathToMedia + Blockly.SPRITE.url);
Blockly.bindEventWithChecks_(this.svgGroup_, 'mouseup', this, this.click);
this.animateLid_();
return this.svgGroup_;
};
/**
* Initialize the trash can.
* @param {number} bottom Distance from workspace bottom to bottom of trashcan.
* @return {number} Distance from workspace bottom to the top of trashcan.
*/
Blockly.Trashcan.prototype.init = function(bottom) {
this.bottom_ = this.MARGIN_BOTTOM_ + bottom;
this.setOpen_(false);
return this.bottom_ + this.BODY_HEIGHT_ + this.LID_HEIGHT_;
};
/**
* Dispose of this trash can.
* Unlink from all DOM elements to prevent memory leaks.
*/
Blockly.Trashcan.prototype.dispose = function() {
if (this.svgGroup_) {
goog.dom.removeNode(this.svgGroup_);
this.svgGroup_ = null;
}
this.svgLid_ = null;
this.workspace_ = null;
clearTimeout(this.lidTask_);
};
/**
* Move the trash can to the bottom-right corner.
*/
Blockly.Trashcan.prototype.position = function() {
var metrics = this.workspace_.getMetrics();
if (!metrics) {
// There are no metrics available (workspace is probably not visible).
return;
}
if (this.workspace_.RTL) {
this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
this.left_ += metrics.flyoutWidth;
if (this.workspace_.toolbox_) {
this.left_ += metrics.absoluteLeft;
}
}
} else {
this.left_ = metrics.viewWidth + metrics.absoluteLeft -
this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
this.left_ -= metrics.flyoutWidth;
}
}
this.top_ = metrics.viewHeight + metrics.absoluteTop -
(this.BODY_HEIGHT_ + this.LID_HEIGHT_) - this.bottom_;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
this.top_ -= metrics.flyoutHeight;
}
this.svgGroup_.setAttribute('transform',
'translate(' + this.left_ + ',' + this.top_ + ')');
};
/**
* Return the deletion rectangle for this trash can.
* @return {goog.math.Rect} Rectangle in which to delete.
*/
Blockly.Trashcan.prototype.getClientRect = function() {
if (!this.svgGroup_) {
return null;
}
var trashRect = this.svgGroup_.getBoundingClientRect();
var left = trashRect.left + this.SPRITE_LEFT_ - this.MARGIN_HOTSPOT_;
var top = trashRect.top + this.SPRITE_TOP_ - this.MARGIN_HOTSPOT_;
var width = this.WIDTH_ + 2 * this.MARGIN_HOTSPOT_;
var height = this.LID_HEIGHT_ + this.BODY_HEIGHT_ + 2 * this.MARGIN_HOTSPOT_;
return new goog.math.Rect(left, top, width, height);
};
/**
* Flip the lid open or shut.
* @param {boolean} state True if open.
* @private
*/
Blockly.Trashcan.prototype.setOpen_ = function(state) {
if (this.isOpen == state) {
return;
}
clearTimeout(this.lidTask_);
this.isOpen = state;
this.animateLid_();
};
/**
* Rotate the lid open or closed by one step. Then wait and recurse.
* @private
*/
Blockly.Trashcan.prototype.animateLid_ = function() {
this.lidOpen_ += this.isOpen ? 0.2 : -0.2;
this.lidOpen_ = Math.min(Math.max(this.lidOpen_, 0), 1);
var lidAngle = this.lidOpen_ * 45;
this.svgLid_.setAttribute('transform', 'rotate(' +
(this.workspace_.RTL ? -lidAngle : lidAngle) + ',' +
(this.workspace_.RTL ? 4 : this.WIDTH_ - 4) + ',' +
(this.LID_HEIGHT_ - 2) + ')');
// Linear interpolation between 0.4 and 0.8.
var opacity = 0.4 + this.lidOpen_ * (0.8 - 0.4);
this.svgGroup_.style.opacity = opacity;
if (this.lidOpen_ > 0 && this.lidOpen_ < 1) {
this.lidTask_ = setTimeout(this.animateLid_.bind(this), 20);
}
};
/**
* Flip the lid shut.
* Called externally after a drag.
*/
Blockly.Trashcan.prototype.close = function() {
this.setOpen_(false);
};
/**
* Inspect the contents of the trash.
*/
Blockly.Trashcan.prototype.click = function() {
var dx = this.workspace_.startScrollX - this.workspace_.scrollX;
var dy = this.workspace_.startScrollY - this.workspace_.scrollY;
if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) {
return;
}
console.log('TODO: Inspect trash.');
};

View File

@@ -0,0 +1,91 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Events fired as a result of UI actions in Blockly's editor.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Events.Ui');
goog.require('Blockly.Events');
goog.require('Blockly.Events.Abstract');
goog.require('goog.array');
goog.require('goog.math.Coordinate');
/**
* Class for a UI event.
* UI events are events that don't need to be sent over the wire for multi-user
* editing to work (e.g. scrolling the workspace, zooming, opening toolbox
* categories).
* UI events do not undo or redo.
* @param {Blockly.Block} block The affected block.
* @param {string} element One of 'selected', 'comment', 'mutator', etc.
* @param {*} oldValue Previous value of element.
* @param {*} newValue New value of element.
* @extends {Blockly.Events.Abstract}
* @constructor
*/
Blockly.Events.Ui = function(block, element, oldValue, newValue) {
Blockly.Events.Ui.superClass_.constructor.call(this);
this.blockId = block ? block.id : null;
this.workspaceId = block ? block.workspace.id : null;
this.element = element;
this.oldValue = oldValue;
this.newValue = newValue;
// UI events do not undo or redo.
this.recordUndo = false;
};
goog.inherits(Blockly.Events.Ui, Blockly.Events.Abstract);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.Ui.prototype.type = Blockly.Events.UI;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.Ui.prototype.toJson = function() {
var json = Blockly.Events.Ui.superClass_.toJson.call(this);
json['element'] = this.element;
if (this.newValue !== undefined) {
json['newValue'] = this.newValue;
}
if (this.blockId) {
json['blockId'] = this.blockId;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.Ui.prototype.fromJson = function(json) {
Blockly.Events.Ui.superClass_.fromJson.call(this, json);
this.element = json['element'];
this.newValue = json['newValue'];
this.blockId = json['blockId'];
};

View File

@@ -0,0 +1,68 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Utility methods for working with the closure menu (goog.ui.menu).
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
/**
* @name Blockly.utils.uiMenu
* @namespace
**/
goog.provide('Blockly.utils.uiMenu');
/**
* Get the size of a rendered goog.ui.Menu.
* @param {!goog.ui.Menu} menu The menu to measure.
* @return {!goog.math.Size} Object with width and height properties.
* @package
*/
Blockly.utils.uiMenu.getSize = function(menu) {
var menuDom = menu.getElement();
var menuSize = goog.style.getSize(menuDom);
// Recalculate height for the total content, not only box height.
menuSize.height = menuDom.scrollHeight;
return menuSize;
};
/**
* Adjust the bounding boxes used to position the widget div to deal with RTL
* goog.ui.Menu positioning. In RTL mode the menu renders down and to the left
* of its start point, instead of down and to the right. Adjusting all of the
* bounding boxes accordingly allows us to use the same code for all widgets.
* This function in-place modifies the provided bounding boxes.
* @param {!Object} viewportBBox The bounding rectangle of the current viewport,
* in window coordinates.
* @param {!Object} anchorBBox The bounding rectangle of the anchor, in window
* coordinates.
* @param {!goog.math.Size} menuSize The size of the menu that is inside the
* widget div, in window coordinates.
* @package
*/
Blockly.utils.uiMenu.adjustBBoxesForRTL = function(viewportBBox, anchorBBox,
menuSize) {
anchorBBox.left += menuSize.width;
anchorBBox.right += menuSize.width;
viewportBBox.left += menuSize.width;
viewportBBox.right += menuSize.width;
};

View File

@@ -0,0 +1,825 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Utility methods.
* These methods are not specific to Blockly, and could be factored out into
* a JavaScript framework such as Closure.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* @name Blockly.utils
* @namespace
**/
goog.provide('Blockly.utils');
goog.require('Blockly.Touch');
goog.require('goog.dom');
goog.require('goog.events.BrowserFeature');
goog.require('goog.math.Coordinate');
goog.require('goog.userAgent');
/**
* To allow ADVANCED_OPTIMIZATIONS, combining variable.name and variable['name']
* is not possible. To access the exported Blockly.Msg.Something it needs to be
* accessed through the exact name that was exported. Note, that all the exports
* are happening as the last thing in the generated js files, so they won't be
* accessible before JavaScript loads!
* @return {!Object.<string, string>} The message array.
* @private
*/
Blockly.utils.getMessageArray_ = function() {
return goog.global['Blockly']['Msg'];
};
/**
* Remove an attribute from a element even if it's in IE 10.
* Similar to Element.removeAttribute() but it works on SVG elements in IE 10.
* Sets the attribute to null in IE 10, which treats removeAttribute as a no-op
* if it's called on an SVG element.
* @param {!Element} element DOM element to remove attribute from.
* @param {string} attributeName Name of attribute to remove.
*/
Blockly.utils.removeAttribute = function(element, attributeName) {
// goog.userAgent.isVersion is deprecated, but the replacement is
// goog.userAgent.isVersionOrHigher.
if (goog.userAgent.IE && goog.userAgent.isVersion('10.0')) {
element.setAttribute(attributeName, null);
} else {
element.removeAttribute(attributeName);
}
};
/**
* Add a CSS class to a element.
* Similar to Closure's goog.dom.classes.add, except it handles SVG elements.
* @param {!Element} element DOM element to add class to.
* @param {string} className Name of class to add.
* @return {boolean} True if class was added, false if already present.
*/
Blockly.utils.addClass = function(element, className) {
var classes = element.getAttribute('class') || '';
if ((' ' + classes + ' ').indexOf(' ' + className + ' ') != -1) {
return false;
}
if (classes) {
classes += ' ';
}
element.setAttribute('class', classes + className);
return true;
};
/**
* Remove a CSS class from a element.
* Similar to Closure's goog.dom.classes.remove, except it handles SVG elements.
* @param {!Element} element DOM element to remove class from.
* @param {string} className Name of class to remove.
* @return {boolean} True if class was removed, false if never present.
*/
Blockly.utils.removeClass = function(element, className) {
var classes = element.getAttribute('class');
if ((' ' + classes + ' ').indexOf(' ' + className + ' ') == -1) {
return false;
}
var classList = classes.split(/\s+/);
for (var i = 0; i < classList.length; i++) {
if (!classList[i] || classList[i] == className) {
classList.splice(i, 1);
i--;
}
}
if (classList.length) {
element.setAttribute('class', classList.join(' '));
} else {
Blockly.utils.removeAttribute(element, 'class');
}
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.
* @package
*/
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.
*/
Blockly.utils.noEvent = function(e) {
// This event has been handled. No need to bubble up to the document.
e.preventDefault();
e.stopPropagation();
};
/**
* Is this event targeting a text input widget?
* @param {!Event} e An event.
* @return {boolean} True if text input.
*/
Blockly.utils.isTargetInput = function(e) {
return e.target.type == 'textarea' || e.target.type == 'text' ||
e.target.type == 'number' || e.target.type == 'email' ||
e.target.type == 'password' || e.target.type == 'search' ||
e.target.type == 'tel' || e.target.type == 'url' ||
e.target.isContentEditable;
};
/**
* Return the coordinates of the top-left corner of this element relative to
* its parent. Only for SVG elements and children (e.g. rect, g, path).
* @param {!Element} element SVG element to find the coordinates of.
* @return {!goog.math.Coordinate} Object with .x and .y properties.
*/
Blockly.utils.getRelativeXY = function(element) {
var xy = new goog.math.Coordinate(0, 0);
// First, check for x and y attributes.
var x = element.getAttribute('x');
if (x) {
xy.x = parseInt(x, 10);
}
var y = element.getAttribute('y');
if (y) {
xy.y = parseInt(y, 10);
}
// Second, check for transform="translate(...)" attribute.
var transform = element.getAttribute('transform');
var r = transform && transform.match(Blockly.utils.getRelativeXY.XY_REGEX_);
if (r) {
xy.x += parseFloat(r[1]);
if (r[3]) {
xy.y += parseFloat(r[3]);
}
}
// Then check for style = transform: translate(...) or translate3d(...)
var style = element.getAttribute('style');
if (style && style.indexOf('translate') > -1) {
var styleComponents = style.match(Blockly.utils.getRelativeXY.XY_STYLE_REGEX_);
if (styleComponents) {
xy.x += parseFloat(styleComponents[1]);
if (styleComponents[3]) {
xy.y += parseFloat(styleComponents[3]);
}
}
}
return xy;
};
/**
* Return the coordinates of the top-left corner of this element relative to
* the div blockly was injected into.
* @param {!Element} element SVG element to find the coordinates of. If this is
* not a child of the div blockly was injected into, the behaviour is
* undefined.
* @return {!goog.math.Coordinate} Object with .x and .y properties.
*/
Blockly.utils.getInjectionDivXY_ = function(element) {
var x = 0;
var y = 0;
while (element) {
var xy = Blockly.utils.getRelativeXY(element);
var scale = Blockly.utils.getScale_(element);
x = (x * scale) + xy.x;
y = (y * scale) + xy.y;
var classes = element.getAttribute('class') || '';
if ((' ' + classes + ' ').indexOf(' injectionDiv ') != -1) {
break;
}
element = element.parentNode;
}
return new goog.math.Coordinate(x, y);
};
/**
* Return the scale of this element.
* @param {!Element} element The element to find the coordinates of.
* @return {!number} number represending the scale applied to the element.
* @private
*/
Blockly.utils.getScale_ = function(element) {
var scale = 1;
var transform = element.getAttribute('transform');
if (transform) {
var transformComponents =
transform.match(Blockly.utils.getScale_.REGEXP_);
if (transformComponents && transformComponents[0]) {
scale = parseFloat(transformComponents[0]);
}
}
return scale;
};
/**
* Static regex to pull the x,y values out of an SVG translate() directive.
* Note that Firefox and IE (9,10) return 'translate(12)' instead of
* 'translate(12, 0)'.
* Note that IE (9,10) returns 'translate(16 8)' instead of 'translate(16, 8)'.
* Note that IE has been reported to return scientific notation (0.123456e-42).
* @type {!RegExp}
* @private
*/
Blockly.utils.getRelativeXY.XY_REGEX_ =
/translate\(\s*([-+\d.e]+)([ ,]\s*([-+\d.e]+)\s*)?/;
/**
* Static regex to pull the scale values out of a transform style property.
* Accounts for same exceptions as XY_REGEXP_.
* @type {!RegExp}
* @private
*/
Blockly.utils.getScale_REGEXP_ = /scale\(\s*([-+\d.e]+)\s*\)/;
/**
* Static regex to pull the x,y values out of a translate3d() or translate3d()
* style property.
* Accounts for same exceptions as XY_REGEXP_.
* @type {!RegExp}
* @private
*/
Blockly.utils.getRelativeXY.XY_STYLE_REGEX_ =
/transform:\s*translate(?:3d)?\(\s*([-+\d.e]+)\s*px([ ,]\s*([-+\d.e]+)\s*px)?/;
/**
* Helper method for creating SVG elements.
* @param {string} name Element's tag name.
* @param {!Object} attrs Dictionary of attribute names and values.
* @param {Element} parent Optional parent on which to append the element.
* @return {!SVGElement} Newly created SVG element.
*/
Blockly.utils.createSvgElement = function(name, attrs, parent /*, opt_workspace */) {
var e = /** @type {!SVGElement} */
(document.createElementNS(Blockly.SVG_NS, name));
for (var key in attrs) {
e.setAttribute(key, attrs[key]);
}
// IE defines a unique attribute "runtimeStyle", it is NOT applied to
// elements created with createElementNS. However, Closure checks for IE
// and assumes the presence of the attribute and crashes.
if (document.body.runtimeStyle) { // Indicates presence of IE-only attr.
e.runtimeStyle = e.currentStyle = e.style;
}
if (parent) {
parent.appendChild(e);
}
return e;
};
/**
* Is this event a right-click?
* @param {!Event} e Mouse event.
* @return {boolean} True if right-click.
*/
Blockly.utils.isRightButton = function(e) {
if (e.ctrlKey && goog.userAgent.MAC) {
// Control-clicking on Mac OS X is treated as a right-click.
// WebKit on Mac OS X fails to change button to 2 (but Gecko does).
return true;
}
return e.button == 2;
};
/**
* Return the converted coordinates of the given mouse event.
* The origin (0,0) is the top-left corner of the Blockly SVG.
* @param {!Event} e Mouse event.
* @param {!Element} svg SVG element.
* @param {SVGMatrix} matrix Inverted screen CTM to use.
* @return {!SVGPoint} Object with .x and .y properties.
*/
Blockly.utils.mouseToSvg = function(e, svg, matrix) {
var svgPoint = svg.createSVGPoint();
svgPoint.x = e.clientX;
svgPoint.y = e.clientY;
if (!matrix) {
matrix = svg.getScreenCTM().inverse();
}
return svgPoint.matrixTransform(matrix);
};
/**
* Parse a string with any number of interpolation tokens (%1, %2, ...).
* It will also replace string table references (e.g., %{bky_my_msg} and
* %{BKY_MY_MSG} will both be replaced with the value in
* Blockly.Msg['MY_MSG']). Percentage sign characters '%' may be self-escaped
* (e.g., '%%').
* @param {string} message Text which might contain string table references and
* interpolation tokens.
* @return {!Array.<string|number>} Array of strings and numbers.
*/
Blockly.utils.tokenizeInterpolation = function(message) {
return Blockly.utils.tokenizeInterpolation_(message, true);
};
/**
* Replaces string table references in a message, if the message is a string.
* For example, "%{bky_my_msg}" and "%{BKY_MY_MSG}" will both be replaced with
* the value in Blockly.Msg['MY_MSG'].
* @param {string|?} message Message, which may be a string that contains
* string table references.
* @return {!string} String with message references replaced.
*/
Blockly.utils.replaceMessageReferences = function(message) {
if (!goog.isString(message)) {
return message;
}
var interpolatedResult = Blockly.utils.tokenizeInterpolation_(message, false);
// When parseInterpolationTokens == false, interpolatedResult should be at
// most length 1.
return interpolatedResult.length ? interpolatedResult[0] : '';
};
/**
* Validates that any %{BKY_...} references in the message refer to keys of
* the Blockly.Msg string table.
* @param {string} message Text which might contain string table references.
* @return {boolean} True if all message references have matching values.
* Otherwise, false.
*/
Blockly.utils.checkMessageReferences = function(message) {
var isValid = true; // True until a bad reference is found.
var regex = /%{BKY_([a-zA-Z][a-zA-Z0-9_]*)}/g;
var match = regex.exec(message);
while (match) {
var msgKey = match[1];
if (Blockly.utils.getMessageArray_()[msgKey] == undefined) {
console.log('WARNING: No message string for %{BKY_' + msgKey + '}.');
isValid = false;
}
// Re-run on remainder of string.
message = message.substring(match.index + msgKey.length + 1);
match = regex.exec(message);
}
return isValid;
};
/**
* Internal implementation of the message reference and interpolation token
* parsing used by tokenizeInterpolation() and replaceMessageReferences().
* @param {string} message Text which might contain string table references and
* interpolation tokens.
* @param {boolean} parseInterpolationTokens Option to parse numeric
* interpolation tokens (%1, %2, ...) when true.
* @return {!Array.<string|number>} Array of strings and numbers.
* @private
*/
Blockly.utils.tokenizeInterpolation_ = function(message,
parseInterpolationTokens) {
var tokens = [];
var chars = message.split('');
chars.push(''); // End marker.
// Parse the message with a finite state machine.
// 0 - Base case.
// 1 - % found.
// 2 - Digit found.
// 3 - Message ref found.
var state = 0;
var buffer = [];
var number = null;
for (var i = 0; i < chars.length; i++) {
var c = chars[i];
if (state == 0) {
if (c == '%') {
var text = buffer.join('');
if (text) {
tokens.push(text);
}
buffer.length = 0;
state = 1; // Start escape.
} else {
buffer.push(c); // Regular char.
}
} else if (state == 1) {
if (c == '%') {
buffer.push(c); // Escaped %: %%
state = 0;
} else if (parseInterpolationTokens && '0' <= c && c <= '9') {
state = 2;
number = c;
var text = buffer.join('');
if (text) {
tokens.push(text);
}
buffer.length = 0;
} else if (c == '{') {
state = 3;
} else {
buffer.push('%', c); // Not recognized. Return as literal.
state = 0;
}
} else if (state == 2) {
if ('0' <= c && c <= '9') {
number += c; // Multi-digit number.
} else {
tokens.push(parseInt(number, 10));
i--; // Parse this char again.
state = 0;
}
} else if (state == 3) { // String table reference
if (c == '') {
// Premature end before closing '}'
buffer.splice(0, 0, '%{'); // Re-insert leading delimiter
i--; // Parse this char again.
state = 0; // and parse as string literal.
} else if (c != '}') {
buffer.push(c);
} else {
var rawKey = buffer.join('');
if (/[a-zA-Z][a-zA-Z0-9_]*/.test(rawKey)) { // Strict matching
// Found a valid string key. Attempt case insensitive match.
var keyUpper = rawKey.toUpperCase();
// BKY_ is the prefix used to namespace the strings used in Blockly
// core files and the predefined blocks in ../blocks/. These strings
// are defined in ../msgs/ files.
var bklyKey = goog.string.startsWith(keyUpper, 'BKY_') ?
keyUpper.substring(4) : null;
if (bklyKey && bklyKey in Blockly.Msg) {
var rawValue = Blockly.Msg[bklyKey];
if (goog.isString(rawValue)) {
// Attempt to dereference substrings, too, appending to the end.
Array.prototype.push.apply(tokens,
Blockly.utils.tokenizeInterpolation(rawValue));
} else if (parseInterpolationTokens) {
// When parsing interpolation tokens, numbers are special
// placeholders (%1, %2, etc). Make sure all other values are
// strings.
tokens.push(String(rawValue));
} else {
tokens.push(rawValue);
}
} else {
// No entry found in the string table. Pass reference as string.
tokens.push('%{' + rawKey + '}');
}
buffer.length = 0; // Clear the array
state = 0;
} else {
tokens.push('%{' + rawKey + '}');
buffer.length = 0;
state = 0; // and parse as string literal.
}
}
}
}
var text = buffer.join('');
if (text) {
tokens.push(text);
}
// Merge adjacent text tokens into a single string.
var mergedTokens = [];
buffer.length = 0;
for (var i = 0; i < tokens.length; ++i) {
if (typeof tokens[i] == 'string') {
buffer.push(tokens[i]);
} else {
text = buffer.join('');
if (text) {
mergedTokens.push(text);
}
buffer.length = 0;
mergedTokens.push(tokens[i]);
}
}
text = buffer.join('');
if (text) {
mergedTokens.push(text);
}
buffer.length = 0;
return mergedTokens;
};
/**
* Generate a unique ID. This should be globally unique.
* 87 characters ^ 20 length > 128 bits (better than a UUID).
* @return {string} A globally unique ID string.
*/
Blockly.utils.genUid = function() {
var length = 20;
var soupLength = Blockly.utils.genUid.soup_.length;
var id = [];
for (var i = 0; i < length; i++) {
id[i] = Blockly.utils.genUid.soup_.charAt(Math.random() * soupLength);
}
return id.join('');
};
/**
* Legal characters for the unique ID. Should be all on a US keyboard.
* No characters that conflict with XML or JSON. Requests to remove additional
* 'problematic' characters from this soup will be denied. That's your failure
* to properly escape in your own environment. Issues #251, #625, #682, #1304.
* @private
*/
Blockly.utils.genUid.soup_ = '!#$%()*+,-./:;=?@[]^_`{|}~' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
/**
* Wrap text to the specified width.
* @param {string} text Text to wrap.
* @param {number} limit Width to wrap each line.
* @return {string} Wrapped text.
*/
Blockly.utils.wrap = function(text, limit) {
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
lines[i] = Blockly.utils.wrapLine_(lines[i], limit);
}
return lines.join('\n');
};
/**
* Wrap single line of text to the specified width.
* @param {string} text Text to wrap.
* @param {number} limit Width to wrap each line.
* @return {string} Wrapped text.
* @private
*/
Blockly.utils.wrapLine_ = function(text, limit) {
if (text.length <= limit) {
// Short text, no need to wrap.
return text;
}
// Split the text into words.
var words = text.trim().split(/\s+/);
// Set limit to be the length of the largest word.
for (var i = 0; i < words.length; i++) {
if (words[i].length > limit) {
limit = words[i].length;
}
}
var lastScore;
var score = -Infinity;
var lastText;
var lineCount = 1;
do {
lastScore = score;
lastText = text;
// Create a list of booleans representing if a space (false) or
// a break (true) appears after each word.
var wordBreaks = [];
// Seed the list with evenly spaced linebreaks.
var steps = words.length / lineCount;
var insertedBreaks = 1;
for (var i = 0; i < words.length - 1; i++) {
if (insertedBreaks < (i + 1.5) / steps) {
insertedBreaks++;
wordBreaks[i] = true;
} else {
wordBreaks[i] = false;
}
}
wordBreaks = Blockly.utils.wrapMutate_(words, wordBreaks, limit);
score = Blockly.utils.wrapScore_(words, wordBreaks, limit);
text = Blockly.utils.wrapToText_(words, wordBreaks);
lineCount++;
} while (score > lastScore);
return lastText;
};
/**
* Compute a score for how good the wrapping is.
* @param {!Array.<string>} words Array of each word.
* @param {!Array.<boolean>} wordBreaks Array of line breaks.
* @param {number} limit Width to wrap each line.
* @return {number} Larger the better.
* @private
*/
Blockly.utils.wrapScore_ = function(words, wordBreaks, limit) {
// If this function becomes a performance liability, add caching.
// Compute the length of each line.
var lineLengths = [0];
var linePunctuation = [];
for (var i = 0; i < words.length; i++) {
lineLengths[lineLengths.length - 1] += words[i].length;
if (wordBreaks[i] === true) {
lineLengths.push(0);
linePunctuation.push(words[i].charAt(words[i].length - 1));
} else if (wordBreaks[i] === false) {
lineLengths[lineLengths.length - 1]++;
}
}
var maxLength = Math.max.apply(Math, lineLengths);
var score = 0;
for (var i = 0; i < lineLengths.length; i++) {
// Optimize for width.
// -2 points per char over limit (scaled to the power of 1.5).
score -= Math.pow(Math.abs(limit - lineLengths[i]), 1.5) * 2;
// Optimize for even lines.
// -1 point per char smaller than max (scaled to the power of 1.5).
score -= Math.pow(maxLength - lineLengths[i], 1.5);
// Optimize for structure.
// Add score to line endings after punctuation.
if ('.?!'.indexOf(linePunctuation[i]) != -1) {
score += limit / 3;
} else if (',;)]}'.indexOf(linePunctuation[i]) != -1) {
score += limit / 4;
}
}
// All else being equal, the last line should not be longer than the
// previous line. For example, this looks wrong:
// aaa bbb
// ccc ddd eee
if (lineLengths.length > 1 && lineLengths[lineLengths.length - 1] <=
lineLengths[lineLengths.length - 2]) {
score += 0.5;
}
return score;
};
/**
* Mutate the array of line break locations until an optimal solution is found.
* No line breaks are added or deleted, they are simply moved around.
* @param {!Array.<string>} words Array of each word.
* @param {!Array.<boolean>} wordBreaks Array of line breaks.
* @param {number} limit Width to wrap each line.
* @return {!Array.<boolean>} New array of optimal line breaks.
* @private
*/
Blockly.utils.wrapMutate_ = function(words, wordBreaks, limit) {
var bestScore = Blockly.utils.wrapScore_(words, wordBreaks, limit);
var bestBreaks;
// Try shifting every line break forward or backward.
for (var i = 0; i < wordBreaks.length - 1; i++) {
if (wordBreaks[i] == wordBreaks[i + 1]) {
continue;
}
var mutatedWordBreaks = [].concat(wordBreaks);
mutatedWordBreaks[i] = !mutatedWordBreaks[i];
mutatedWordBreaks[i + 1] = !mutatedWordBreaks[i + 1];
var mutatedScore =
Blockly.utils.wrapScore_(words, mutatedWordBreaks, limit);
if (mutatedScore > bestScore) {
bestScore = mutatedScore;
bestBreaks = mutatedWordBreaks;
}
}
if (bestBreaks) {
// Found an improvement. See if it may be improved further.
return Blockly.utils.wrapMutate_(words, bestBreaks, limit);
}
// No improvements found. Done.
return wordBreaks;
};
/**
* Reassemble the array of words into text, with the specified line breaks.
* @param {!Array.<string>} words Array of each word.
* @param {!Array.<boolean>} wordBreaks Array of line breaks.
* @return {string} Plain text.
* @private
*/
Blockly.utils.wrapToText_ = function(words, wordBreaks) {
var text = [];
for (var i = 0; i < words.length; i++) {
text.push(words[i]);
if (wordBreaks[i] !== undefined) {
text.push(wordBreaks[i] ? '\n' : ' ');
}
}
return text.join('');
};
/**
* Check if 3D transforms are supported by adding an element
* and attempting to set the property.
* @return {boolean} true if 3D transforms are supported.
*/
Blockly.utils.is3dSupported = function() {
// TW: Every browser we care about supports 3d. Don't bother checking.
// This saves about 0.5ms on every page load.
return true;
};
/**
* Insert a node after a reference node.
* Contrast with node.insertBefore function.
* @param {!Element} newNode New element to insert.
* @param {!Element} refNode Existing element to precede new node.
* @package
*/
Blockly.utils.insertAfter = function(newNode, refNode) {
var siblingNode = refNode.nextSibling;
var parentNode = refNode.parentNode;
if (!parentNode) {
throw 'Reference node has no parent.';
}
if (siblingNode) {
parentNode.insertBefore(newNode, siblingNode);
} else {
parentNode.appendChild(newNode);
}
};
/**
* Calls a function after the page has loaded, possibly immediately.
* @param {function()} fn Function to run.
* @throws Error Will throw if no global document can be found (e.g., Node.js).
*/
Blockly.utils.runAfterPageLoad = function(fn) {
if (!document) {
throw new Error('Blockly.utils.runAfterPageLoad() requires browser document.');
}
if (document.readyState === 'complete') {
fn(); // Page has already loaded. Call immediately.
} else {
// Poll readyState.
var readyStateCheckInterval = setInterval(function() {
if (document.readyState === 'complete') {
clearInterval(readyStateCheckInterval);
fn();
}
}, 10);
}
};
/**
* Sets the CSS transform property on an element. This function sets the
* non-vendor-prefixed and vendor-prefixed versions for backwards compatibility
* with older browsers. See http://caniuse.com/#feat=transforms2d
* @param {!Element} node The node which the CSS transform should be applied.
* @param {string} transform The value of the CSS `transform` property.
*/
Blockly.utils.setCssTransform = function(node, transform) {
node.style['transform'] = transform;
node.style['-webkit-transform'] = transform;
};
/**
* Get the position of the current viewport in window coordinates. This takes
* scroll into account.
* @return {!Object} an object containing window width, height, and scroll
* position in window coordinates.
* @package
*/
Blockly.utils.getViewportBBox = function() {
// Pixels.
var windowSize = goog.dom.getViewportSize();
// Pixels, in window coordinates.
var scrollOffset = goog.style.getViewportPageOffset(document);
return {
right: windowSize.width + scrollOffset.x,
bottom: windowSize.height + scrollOffset.y,
top: scrollOffset.y,
left: scrollOffset.x
};
};
/**
* Fast prefix-checker.
* Copied from Closure's goog.string.startsWith.
* @param {string} str The string to check.
* @param {string} prefix A string to look for at the start of `str`.
* @return {boolean} True if `str` begins with `prefix`.
* @package
*/
Blockly.utils.startsWith = function(str, prefix) {
return str.lastIndexOf(prefix, 0) == 0;
};
/**
* Converts degrees to radians.
* Copied from Closure's goog.math.toRadians.
* @param {number} angleDegrees Angle in degrees.
* @return {number} Angle in radians.
* @package
*/
Blockly.utils.toRadians = function(angleDegrees) {
return angleDegrees * Math.PI / 180;
};

View File

@@ -0,0 +1,259 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Classes for all types of variable events.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.Events.VarBase');
goog.provide('Blockly.Events.VarCreate');
goog.provide('Blockly.Events.VarDelete');
goog.provide('Blockly.Events.VarRename');
goog.require('Blockly.Events');
goog.require('Blockly.Events.Abstract');
goog.require('goog.array');
goog.require('goog.math.Coordinate');
/**
* Abstract class for a variable event.
* @param {Blockly.VariableModel} variable The variable this event corresponds
* to.
* @extends {Blockly.Events.Abstract}
* @constructor
*/
Blockly.Events.VarBase = function(variable) {
Blockly.Events.VarBase.superClass_.constructor.call(this);
/**
* The variable id for the variable this event pertains to.
* @type {string}
*/
this.varId = variable.getId();
this.workspaceId = variable.workspace.id;
};
goog.inherits(Blockly.Events.VarBase, Blockly.Events.Abstract);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.VarBase.prototype.toJson = function() {
var json = Blockly.Events.VarBase.superClass_.toJson.call(this);
json['varId'] = this.varId;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.VarBase.prototype.fromJson = function(json) {
Blockly.Events.VarBase.superClass_.toJson.call(this);
this.varId = json['varId'];
};
/**
* Class for a variable creation event.
* @param {Blockly.VariableModel} variable The created variable.
* Null for a blank event.
* @extends {Blockly.Events.VarBase}
* @constructor
*/
Blockly.Events.VarCreate = function(variable) {
if (!variable) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.VarCreate.superClass_.constructor.call(this, variable);
this.varType = variable.type;
this.varName = variable.name;
this.isLocal = variable.isLocal;
this.isCloud = variable.isCloud;
};
goog.inherits(Blockly.Events.VarCreate, Blockly.Events.VarBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.VarCreate.prototype.type = Blockly.Events.VAR_CREATE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.VarCreate.prototype.toJson = function() {
var json = Blockly.Events.VarCreate.superClass_.toJson.call(this);
json['varType'] = this.varType;
json['varName'] = this.varName;
json['isLocal'] = this.isLocal;
json['isCloud'] = this.isCloud;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.VarCreate.prototype.fromJson = function(json) {
Blockly.Events.VarCreate.superClass_.fromJson.call(this, json);
this.varType = json['varType'];
this.varName = json['varName'];
this.isLocal = json['isLocal'];
this.isCloud = json['isCloud'];
};
/**
* Run a variable creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.VarCreate.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
if (forward) {
workspace.createVariable(this.varName, this.varType, this.varId, this.isLocal, this.isCloud);
} else {
workspace.deleteVariableById(this.varId);
}
};
/**
* Class for a variable deletion event.
* @param {Blockly.VariableModel} variable The deleted variable.
* Null for a blank event.
* @extends {Blockly.Events.VarBase}
* @constructor
*/
Blockly.Events.VarDelete = function(variable) {
if (!variable) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.VarDelete.superClass_.constructor.call(this, variable);
this.varType = variable.type;
this.varName = variable.name;
this.isLocal = variable.isLocal;
this.isCloud = variable.isCloud;
};
goog.inherits(Blockly.Events.VarDelete, Blockly.Events.VarBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.VarDelete.prototype.type = Blockly.Events.VAR_DELETE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.VarDelete.prototype.toJson = function() {
var json = Blockly.Events.VarDelete.superClass_.toJson.call(this);
json['varType'] = this.varType;
json['varName'] = this.varName;
json['isLocal'] = this.isLocal;
json['isCloud'] = this.isCloud;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.VarDelete.prototype.fromJson = function(json) {
Blockly.Events.VarDelete.superClass_.fromJson.call(this, json);
this.varType = json['varType'];
this.varName = json['varName'];
this.isLocal = json['isLocal'];
this.isCloud = json['isCloud'];
};
/**
* Run a variable deletion event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.VarDelete.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
if (forward) {
workspace.deleteVariableById(this.varId);
} else {
workspace.createVariable(this.varName, this.varType, this.varId, this.isLocal, this.isCloud);
}
};
/**
* Class for a variable rename event.
* @param {Blockly.VariableModel} variable The renamed variable.
* Null for a blank event.
* @param {string} newName The new name the variable will be changed to.
* @extends {Blockly.Events.VarBase}
* @constructor
*/
Blockly.Events.VarRename = function(variable, newName) {
if (!variable) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.VarRename.superClass_.constructor.call(this, variable);
this.oldName = variable.name;
this.newName = newName;
};
goog.inherits(Blockly.Events.VarRename, Blockly.Events.VarBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.VarRename.prototype.type = Blockly.Events.VAR_RENAME;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.VarRename.prototype.toJson = function() {
var json = Blockly.Events.VarRename.superClass_.toJson.call(this);
json['oldName'] = this.oldName;
json['newName'] = this.newName;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.VarRename.prototype.fromJson = function(json) {
Blockly.Events.VarRename.superClass_.fromJson.call(this, json);
this.oldName = json['oldName'];
this.newName = json['newName'];
};
/**
* Run a variable rename event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.VarRename.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
if (forward) {
workspace.renameVariableById(this.varId, this.newName);
} else {
workspace.renameVariableById(this.varId, this.oldName);
}
};

View File

@@ -0,0 +1,415 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Object representing a map of variables and their types.
* @author marisaleung@google.com (Marisa Leung)
*/
'use strict';
goog.provide('Blockly.VariableMap');
goog.require('Blockly.Events.VarDelete');
goog.require('Blockly.Events.VarRename');
goog.require('Blockly.VariableModel');
/**
* Class for a variable map. This contains a dictionary data structure with
* variable types as keys and lists of variables as values. The list of
* variables are the type indicated by the key.
* @param {!Blockly.Workspace} workspace The workspace this map belongs to.
* @constructor
*/
Blockly.VariableMap = function(workspace) {
/**
* A map from variable type to list of variable names. The lists contain all
* of the named variables in the workspace, including variables
* that are not currently in use.
* @type {!Object.<string, !Array.<Blockly.VariableModel>>}
* @private
*/
this.variableMap_ = {};
/**
* The workspace this map belongs to.
* @type {!Blockly.Workspace}
*/
this.workspace = workspace;
};
/**
* Clear the variable map.
*/
Blockly.VariableMap.prototype.clear = function() {
this.variableMap_ = new Object(null);
};
/* Begin functions for renaming variables. */
/**
* Rename the given variable by updating its name in the variable map.
* @param {!Blockly.VariableModel} variable Variable to rename.
* @param {string} newName New variable name.
* @package
*/
Blockly.VariableMap.prototype.renameVariable = function(variable, newName) {
var type = variable.type;
var conflictVar = this.getVariable(newName, type);
var blocks = this.workspace.getAllBlocks();
Blockly.Events.setGroup(true);
try {
if (!conflictVar) {
this.renameVariableAndUses_(variable, newName, blocks);
} else {
// We don't want to rename the variable if one with the exact new name
// already exists.
console.warn('Unexpected conflict when attempting to rename ' +
'variable with name: ' + variable.name + ' and id: ' + variable.getId() +
' to new name: ' + newName + '. A variable with the new name already exists' +
' and has id: ' + conflictVar.getId());
}
} finally {
Blockly.Events.setGroup(false);
}
};
/**
* Rename a variable by updating its name in the variable map. Identify the
* variable to rename with the given ID.
* @param {string} id ID of the variable to rename.
* @param {string} newName New variable name.
*/
Blockly.VariableMap.prototype.renameVariableById = function(id, newName) {
var variable = this.getVariableById(id);
if (!variable) {
throw new Error('Tried to rename a variable that didn\'t exist. ID: ' + id);
}
this.renameVariable(variable, newName);
};
/**
* Update the name of the given variable and refresh all references to it.
* The new name must not conflict with any existing variable names.
* @param {!Blockly.VariableModel} variable Variable to rename.
* @param {string} newName New variable name.
* @param {!Array.<!Blockly.Block>} blocks The list of all blocks in the
* workspace.
* @private
*/
Blockly.VariableMap.prototype.renameVariableAndUses_ = function(variable,
newName, blocks) {
Blockly.Events.fire(new Blockly.Events.VarRename(variable, newName));
variable.name = newName;
for (var i = 0; i < blocks.length; i++) {
blocks[i].updateVarName(variable);
}
};
/**
* Update the name of the given variable to the same name as an existing
* variable. The two variables are coalesced into a single variable with the ID
* of the existing variable that was already using newName.
* Refresh all references to the variable.
* @param {!Blockly.VariableModel} variable Variable to rename.
* @param {string} newName New variable name.
* @param {!Blockly.VariableModel} conflictVar The variable that was already
* using newName.
* @param {!Array.<!Blockly.Block>} blocks The list of all blocks in the
* workspace.
* @private
*/
Blockly.VariableMap.prototype.renameVariableWithConflict_ = function(variable,
newName, conflictVar, blocks) {
var type = variable.type;
var oldCase = conflictVar.name;
if (newName != oldCase) {
// Simple rename to change the case and update references.
this.renameVariableAndUses_(conflictVar, newName, blocks);
}
// These blocks now refer to a different variable.
// These will fire change events.
for (var i = 0; i < blocks.length; i++) {
blocks[i].renameVarById(variable.getId(), conflictVar.getId());
}
// Finally delete the original variable, which is now unreferenced.
Blockly.Events.fire(new Blockly.Events.VarDelete(variable));
// And remove it from the list.
var variableList = this.getVariablesOfType(type);
var variableIndex = variableList.indexOf(variable);
this.variableMap_[type].splice(variableIndex, 1);
};
/* End functions for renaming variabless. */
/**
* Create a variable with a given name, optional type, and optional id.
* @param {!string} name The name of the variable. This must be unique across
* each variable type.
* @param {?string} opt_type The type of the variable like 'int' or 'string'.
* Does not need to be unique. Field_variable can filter variables based on
* their type. This will default to '' which is a specific type.
* @param {string=} opt_id The unique ID of the variable. This will default to
* a UUID.
* @param {boolean=} opt_isLocal Whether the variable is locally scoped.
* @param {boolean=} opt_isCloud Whether the variable is a cloud variable.
* @return {?Blockly.VariableModel} The newly created variable.
*/
Blockly.VariableMap.prototype.createVariable = function(name,
opt_type, opt_id, opt_isLocal, opt_isCloud) {
var variable = this.getVariable(name, opt_type);
if (variable) {
if (opt_id && variable.getId() != opt_id) {
// There is a variable conflict. Variable conflicts should be eliminated
// in the scratch-vm, or before we get to this point,
// so log a warning, because throwing an error crashes projects.
console.warn('Variable "' + name + '" is already in use and its id is "'
+ variable.getId() + '" which conflicts with the passed in ' +
'id, "' + opt_id + '".');
}
// The variable already exists and has the same ID.
return variable;
}
if (opt_id) {
variable = this.getVariableById(opt_id);
if (variable) {
console.warn('Variable id, "' + opt_id + '", is already in use.');
return variable;
}
}
opt_id = opt_id || Blockly.utils.genUid();
opt_type = opt_type || '';
variable = new Blockly.VariableModel(this.workspace, name, opt_type, opt_id,
opt_isLocal, opt_isCloud);
// If opt_type is not a key, create a new list.
if (!this.variableMap_[opt_type]) {
this.variableMap_[opt_type] = [variable];
} else {
// Else append the variable to the preexisting list.
this.variableMap_[opt_type].push(variable);
}
return variable;
};
/* Begin functions for variable deletion. */
/**
* Delete a variable.
* @param {Blockly.VariableModel} variable Variable to delete.
*/
Blockly.VariableMap.prototype.deleteVariable = function(variable) {
var variableList = this.variableMap_[variable.type];
for (var i = 0, tempVar; tempVar = variableList[i]; i++) {
if (tempVar.getId() == variable.getId()) {
variableList.splice(i, 1);
Blockly.Events.fire(new Blockly.Events.VarDelete(variable));
return;
}
}
};
/**
* Delete a variable and all of its uses from this workspace by the passed
* in ID. May prompt the user for confirmation.
* @param {string} id ID of variable to delete.
*/
Blockly.VariableMap.prototype.deleteVariableById = function(id) {
var variable = this.getVariableById(id);
if (variable) {
// Check whether this variable is a function parameter before deleting.
var variableName = variable.name;
var uses = this.getVariableUsesById(id);
for (var i = 0, block; block = uses[i]; i++) {
if (block.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE ||
block.type == 'procedures_defreturn') {
var procedureName = block.getFieldValue('NAME');
var deleteText = Blockly.Msg.CANNOT_DELETE_VARIABLE_PROCEDURE.
replace('%1', variableName).
replace('%2', procedureName);
Blockly.alert(deleteText);
return;
}
}
var map = this;
if (uses.length > 1) {
// Confirm before deleting multiple blocks.
var confirmText = Blockly.Msg.DELETE_VARIABLE_CONFIRMATION.
replace('%1', String(uses.length)).
replace('%2', variableName);
Blockly.confirm(confirmText,
function(ok) {
if (ok) {
map.deleteVariableInternal_(variable, uses);
}
});
} else {
// No confirmation necessary for a single block.
map.deleteVariableInternal_(variable, uses);
}
} else {
console.warn("Can't delete non-existent variable: " + id);
}
};
/**
* Deletes a variable and all of its uses from this workspace without asking the
* user for confirmation.
* @param {!Blockly.VariableModel} variable Variable to delete.
* @param {!Array.<!Blockly.Block>} uses An array of uses of the variable.
* @private
*/
Blockly.VariableMap.prototype.deleteVariableInternal_ = function(variable,
uses) {
var existingGroup = Blockly.Events.getGroup();
if (!existingGroup) {
Blockly.Events.setGroup(true);
}
try {
for (var i = 0; i < uses.length; i++) {
uses[i].dispose(true, false);
}
this.deleteVariable(variable);
} finally {
if (!existingGroup) {
Blockly.Events.setGroup(false);
}
}
};
/* End functions for variable deletion. */
/**
* Find the variable by the given name and type and return it. Return null if
* it is not found.
* @param {string} name The name to check for.
* @param {string=} opt_type The type of the variable. If not provided it
* defaults to the empty string, which is a specific type.
* @return {Blockly.VariableModel} The variable with the given name, or null if
* it was not found.
*/
Blockly.VariableMap.prototype.getVariable = function(name, opt_type) {
var type = opt_type || '';
var list = this.variableMap_[type];
if (list) {
for (var j = 0, variable; variable = list[j]; j++) {
if (variable.name == name) {
return variable;
}
}
}
return null;
};
/**
* Find the variable by the given ID and return it. Return null if it is not
* found.
* @param {!string} id The id to check for.
* @return {?Blockly.VariableModel} The variable with the given id.
*/
Blockly.VariableMap.prototype.getVariableById = function(id) {
var keys = Object.keys(this.variableMap_);
for (var i = 0; i < keys.length; i++ ) {
var key = keys[i];
for (var j = 0, variable; variable = this.variableMap_[key][j]; j++) {
if (variable.getId() == id) {
return variable;
}
}
}
return null;
};
/**
* Get a list containing all of the variables of a specified type. If type is
* null, return list of variables with empty string type.
* @param {?string} type Type of the variables to find.
* @return {!Array.<!Blockly.VariableModel>} The sought after variables of the
* passed in type. An empty array if none are found.
*/
Blockly.VariableMap.prototype.getVariablesOfType = function(type) {
type = type || '';
var variable_list = this.variableMap_[type];
if (variable_list) {
return variable_list.slice();
}
return [];
};
/**
* Return all variable types. This list always contains the empty string.
* @return {!Array.<string>} List of variable types.
* @package
*/
Blockly.VariableMap.prototype.getVariableTypes = function() {
var types = Object.keys(this.variableMap_);
var hasEmpty = false;
for (var i = 0; i < types.length; i++) {
if (types[i] == '') {
hasEmpty = true;
}
}
if (!hasEmpty) {
types.push('');
}
return types;
};
/**
* Return all variables of all types.
* @return {!Array.<!Blockly.VariableModel>} List of variable models.
*/
Blockly.VariableMap.prototype.getAllVariables = function() {
var all_variables = [];
var keys = Object.keys(this.variableMap_);
for (var i = 0; i < keys.length; i++ ) {
all_variables = all_variables.concat(this.variableMap_[keys[i]]);
}
return all_variables;
};
/**
* Find all the uses of a named variable.
* @param {string} id ID of the variable to find.
* @return {!Array.<!Blockly.Block>} Array of block usages.
*/
Blockly.VariableMap.prototype.getVariableUsesById = function(id) {
var uses = [];
var blocks = this.workspace.getAllBlocks();
// Iterate through every block and check the name.
for (var i = 0; i < blocks.length; i++) {
var blockVariables = blocks[i].getVarModels();
if (blockVariables) {
for (var j = 0; j < blockVariables.length; j++) {
if (blockVariables[j].getId() == id) {
uses.push(blocks[i]);
}
}
}
}
return uses;
};

View File

@@ -0,0 +1,116 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Components for the variable model.
* @author marisaleung@google.com (Marisa Leung)
*/
'use strict';
goog.provide('Blockly.VariableModel');
goog.require('Blockly.Events.VarCreate');
goog.require('goog.string');
/**
* Class for a variable model.
* Holds information for the variable including name, ID, and type.
* @param {!Blockly.Workspace} workspace The variable's workspace.
* @param {!string} name The name of the variable. This must be unique across
* each variable type.
* @param {?string} opt_type The type of the variable like 'int' or 'string'.
* Does not need to be unique. Field_variable can filter variables based on
* their type. This will default to '' which is a specific type.
* @param {string=} opt_id The unique ID of the variable. This will default to
* a UUID.
* @param {boolean=} opt_isLocal Whether the variable is locally scoped.
* @param {boolean=} opt_isCloud Whether the variable is a cloud variable.
* @see {Blockly.FieldVariable}
* @constructor
*/
Blockly.VariableModel = function(workspace, name, opt_type, opt_id,
opt_isLocal, opt_isCloud) {
/**
* The workspace the variable is in.
* @type {!Blockly.Workspace}
*/
this.workspace = workspace;
/**
* The name of the variable, typically defined by the user. It must be
* unique across all names used for procedures and variables. It may be
* changed by the user.
* @type {string}
*/
this.name = name;
/**
* The type of the variable, such as 'int' or 'sound_effect'. This may be
* used to build a list of variables of a specific type. By default this is
* the empty string '', which is a specific type.
* @see {Blockly.FieldVariable}
* @type {string}
*/
this.type = opt_type || '';
/**
* A unique id for the variable. This should be defined at creation and
* not change, even if the name changes. In most cases this should be a
* UUID.
* @type {string}
* @private
*/
this.id_ = opt_id || Blockly.utils.genUid();
/**
* Whether this variable is locally scoped.
* @package
*/
this.isLocal = opt_isLocal || false;
/**
* Whether the variable is a cloud variable.
* @package
*/
this.isCloud = opt_isCloud || false;
Blockly.Events.fire(new Blockly.Events.VarCreate(this));
};
/**
* @return {!string} The ID for the variable.
*/
Blockly.VariableModel.prototype.getId = function() {
return this.id_;
};
/**
* A custom compare function for the VariableModel objects.
* @param {Blockly.VariableModel} var1 First variable to compare.
* @param {Blockly.VariableModel} var2 Second variable to compare.
* @return {number} -1 if name of var1 is less than name of var2, 0 if equal,
* and 1 if greater.
* @package
*/
Blockly.VariableModel.compareByName = function(var1, var2) {
return Blockly.scratchBlocksUtils.compareStrings(var1.name, var2.name);
};

View File

@@ -0,0 +1,674 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Utility functions for handling variables.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* @name Blockly.Variables
* @namespace
**/
goog.provide('Blockly.Variables');
goog.require('Blockly.Blocks');
goog.require('Blockly.constants');
goog.require('Blockly.VariableModel');
goog.require('Blockly.Workspace');
goog.require('goog.string');
/**
* Constant to separate variable names from procedures and generated functions
* when running generators.
* @deprecated Use Blockly.VARIABLE_CATEGORY_NAME
*/
Blockly.Variables.NAME_TYPE = Blockly.VARIABLE_CATEGORY_NAME;
/**
* Constant prefix to differentiate cloud variable names from other types
* of variables.
* This is the \u2601 cloud unicode character followed by a space.
* @type {string}
* @package
*/
Blockly.Variables.CLOUD_PREFIX = '☁ ';
/**
* Find all user-created variables that are in use in the workspace.
* For use by generators.
* @param {!Blockly.Block|!Blockly.Workspace} root Root block or workspace.
* @return {!Array.<string>} Array of variable names.
*/
Blockly.Variables.allUsedVariables = function(root) {
var blocks;
if (root instanceof Blockly.Block) {
// Root is Block.
blocks = root.getDescendants(false);
} else if (root instanceof Blockly.Workspace ||
root instanceof Blockly.WorkspaceSvg) {
// Root is Workspace.
blocks = root.getAllBlocks();
} else {
throw 'Not Block or Workspace: ' + root;
}
var ignorableName = Blockly.Variables.noVariableText();
var variableHash = Object.create(null);
// Iterate through every block and add each variable to the hash.
for (var x = 0; x < blocks.length; x++) {
var blockVariables = blocks[x].getVarModels();
if (blockVariables) {
for (var y = 0; y < blockVariables.length; y++) {
var variable = blockVariables[y];
// Variable ID may be null if the block is only half-built.
if (variable.getId() && variable.name.toLowerCase() != ignorableName) {
variableHash[variable.name.toLowerCase()] = variable.name;
}
}
}
}
// Flatten the hash into a list.
var variableList = [];
for (var name in variableHash) {
variableList.push(variableHash[name]);
}
return variableList;
};
/**
* Find all variables that the user has created through the workspace or
* toolbox. For use by generators.
* @param {!Blockly.Workspace} root The workspace to inspect.
* @return {!Array.<Blockly.VariableModel>} Array of variable models.
*/
Blockly.Variables.allVariables = function(root) {
if (root instanceof Blockly.Block) {
// Root is Block.
console.warn('Deprecated call to Blockly.Variables.allVariables ' +
'with a block instead of a workspace. You may want ' +
'Blockly.Variables.allUsedVariables');
return {};
}
return root.getAllVariables();
};
/**
* Find all developer variables used by blocks in the workspace.
* Developer variables are never shown to the user, but are declared as global
* variables in the generated code.
* To declare developer variables, define the getDeveloperVariables function on
* your block and return a list of variable names.
* For use by generators.
* @param {!Blockly.Workspace} workspace The workspace to search.
* @return {!Array.<string>} A list of non-duplicated variable names.
* @package
*/
Blockly.Variables.allDeveloperVariables = function(workspace) {
var blocks = workspace.getAllBlocks();
var hash = {};
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (block.getDeveloperVars) {
var devVars = block.getDeveloperVars();
for (var j = 0; j < devVars.length; j++) {
hash[devVars[j]] = devVars[j];
}
}
}
// Flatten the hash into a list.
var list = [];
for (var name in hash) {
list.push(hash[name]);
}
return list;
};
/**
* Return the text that should be used in a field_variable or
* field_variable_getter when no variable exists.
* TODO: #572
* @return {string} The text to display.
*/
Blockly.Variables.noVariableText = function() {
return "No variable selected";
};
/**
* Return a new variable name that is not yet being used. This will try to
* generate single letter variable names in the range 'i' to 'z' to start with.
* If no unique name is located it will try 'i' to 'z', 'a' to 'h',
* then 'i2' to 'z2' etc. Skip 'l'.
* @param {!Blockly.Workspace} workspace The workspace to be unique in.
* @return {string} New variable name.
*/
Blockly.Variables.generateUniqueName = function(workspace) {
var variableList = workspace.getAllVariables();
var newName = '';
if (variableList.length) {
var nameSuffix = 1;
var letters = 'ijkmnopqrstuvwxyzabcdefgh'; // No 'l'.
var letterIndex = 0;
var potName = letters.charAt(letterIndex);
while (!newName) {
var inUse = false;
for (var i = 0; i < variableList.length; i++) {
if (variableList[i].name.toLowerCase() == potName) {
// This potential name is already used.
inUse = true;
break;
}
}
if (inUse) {
// Try the next potential name.
letterIndex++;
if (letterIndex == letters.length) {
// Reached the end of the character sequence so back to 'i'.
// a new suffix.
letterIndex = 0;
nameSuffix++;
}
potName = letters.charAt(letterIndex);
if (nameSuffix > 1) {
potName += nameSuffix;
}
} else {
// We can use the current potential name.
newName = potName;
}
}
} else {
newName = 'i';
}
return newName;
};
/**
* Remove any possiblity of conflict/duplication between a real and potential variable.
* When creating a new variable, checks whether the desired name and type already exists
* as a real or potential variable.
* If 'checkReal' is true, checks whether a real variable with the given
* name and type already exists.
* Checks whether a potential variable (using the given 'potentialVarWs') exists.
* If a potential var exists and a real var also exists, discards the potential var
* and returns the real var.
* If a potential var exists and a real var does not exist (or 'checkReal'
* was false), creates the potential var as a real var,
* discards the potential var, and returns the newly created real var.
* If a potential var does not exist, returns null.
*
* @param {string} varName The name of the variable to check for.
* @param {string} varType The type of the variable to check for.
* @param {!Blockly.Workspace} potentialVarWs The workspace containing the
* potential variable map we want to check against.
* @param {boolean} checkReal Whether or not to check if a variable of the given
* name and type exists as a real variable.
* @return {?Blockly.VariableModel} The matching variable, if one already existed
* in the real workspace; the newly transformed variable, if one already
* existed as a potential variable. Null, if no matching variable, real or
* potential, was found.
*/
Blockly.Variables.realizePotentialVar = function(varName, varType, potentialVarWs,
checkReal) {
var potentialVarMap = potentialVarWs.getPotentialVariableMap();
var realWs = potentialVarWs.targetWorkspace;
if (!potentialVarMap) {
console.warn('Called Blockly.Variables.realizePotentialVar with incorrect ' +
'workspace. The provided workspace does not have a potential variable map.');
return;
}
// First check if a variable with the same name and type already exists as a
// real variable.
var realVar;
if (checkReal) {
realVar = Blockly.Variables.getVariable(realWs, null, varName, varType);
}
// Check if variable with same name and type exists as a potential var
var potentialVar = potentialVarMap.getVariable(varName, varType);
if (!potentialVar) {
return null;
}
// The potential var exists, so save its id and delete it from the potential
// variable map.
var id = potentialVar.getId();
potentialVarMap.deleteVariable(potentialVar);
// Depending on whether a real var already exists or not, either return the
// existing real var or turn the potential var into a new one using its id.
if (realVar) {
return realVar;
}
return realWs.createVariable(varName, varType, id);
};
/**
* Create a new variable on the given workspace.
* @param {!Blockly.Workspace} workspace The workspace on which to create the
* variable.
* @param {function(?string=)=} opt_callback An optional callback function to act
* on the id of the variable that is created from the user's input, or null
* if the change is to be aborted (cancel button or an invalid name was provided).
* @param {string} opt_type Optional type of the variable to be created,
* like 'string' or 'list'.
*/
Blockly.Variables.createVariable = function(workspace, opt_callback, opt_type) {
// Decide on a modal message based on the opt_type. If opt_type was not
// provided, default to the original message for scalar variables.
var newMsg, modalTitle;
if (opt_type == Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE) {
newMsg = Blockly.Msg.NEW_BROADCAST_MESSAGE_TITLE;
modalTitle = Blockly.Msg.BROADCAST_MODAL_TITLE;
} else if (opt_type == Blockly.LIST_VARIABLE_TYPE) {
newMsg = Blockly.Msg.NEW_LIST_TITLE;
modalTitle = Blockly.Msg.LIST_MODAL_TITLE;
} else {
// Note: this case covers 1) scalar variables, 2) any new type of
// variable not explicitly checked for above, and 3) a null or undefined
// opt_type -- turns a falsey opt_type into ''
// TODO (#1251) Warn developers that they didn't provide an opt_type/provided
// a falsey opt_type
opt_type = opt_type ? opt_type : '';
newMsg = Blockly.Msg.NEW_VARIABLE_TITLE;
modalTitle = Blockly.Msg.VARIABLE_MODAL_TITLE;
}
var validate = Blockly.Variables.nameValidator_.bind(null, opt_type);
// Prompt the user to enter a name for the variable
Blockly.prompt(newMsg, '',
function(text, additionalVars, variableOptions) {
variableOptions = variableOptions || {};
var scope = variableOptions.scope;
var isLocal = (scope === 'local') || false;
var isCloud = variableOptions.isCloud || false;
// Default to [] if additionalVars is not provided
additionalVars = additionalVars || [];
// Only use additionalVars for global variable creation.
var additionalVarNames = isLocal ? [] : additionalVars;
var validatedText = validate(text, workspace, additionalVarNames, isCloud, opt_callback);
if (validatedText) {
// The name is valid according to the type, create the variable
var potentialVarMap = workspace.getPotentialVariableMap();
var variable;
// This check ensures that if a new variable is being created from a
// workspace that already has a variable of the same name and type as
// a potential variable, that potential variable gets turned into a
// real variable and thus there aren't duplicate options in the field_variable
// dropdown.
if (potentialVarMap && opt_type) {
variable = Blockly.Variables.realizePotentialVar(validatedText,
opt_type, workspace, false);
}
if (!variable) {
variable = workspace.createVariable(validatedText, opt_type, null, isLocal, isCloud);
}
var flyout = workspace.isFlyout ? workspace : workspace.getFlyout();
var variableBlockId = variable.getId();
if (flyout.setCheckboxState) {
flyout.setCheckboxState(variableBlockId, true);
}
if (opt_callback) {
opt_callback(variableBlockId);
}
} else {
// User canceled prompt without a value.
if (opt_callback) {
opt_callback(null);
}
}
}, modalTitle, opt_type);
};
/**
* This function provides a common interface for variable name validation agnostic
* of type. This is so that functions like Blockly.Variables.createVariable and
* Blockly.Variables.renameVariable can call a single function (with a single
* type signature) to validate the user-provided name for a variable.
* @param {string} type The type of the variable for which the provided name
* should be validated.
* @param {string} text The user-provided text that should be validated as a
* variable name.
* @param {!Blockly.Workspace} workspace The workspace on which to validate the
* variable name. This is the workspace used to check whether the variable
* already exists.
* @param {Array<string>} additionalVars A list of additional var names to check
* for conflicts against.
* @param {boolean} isCloud Whether the variable is a cloud variable.
* @param {function(?string=)=} opt_callback An optional function to be called on
* a pre-existing variable of the user-provided name. This function is currently
* only used for broadcast messages.
* @return {string} The validated name according to the parameters given, if
* the name is determined to be valid, or null if the name
* is determined to be invalid/in-use, and the calling function should not
* proceed with creating or renaming the variable.
* @private
*/
Blockly.Variables.nameValidator_ = function(type, text, workspace, additionalVars,
isCloud, opt_callback) {
// The validators for the different variable types require slightly different arguments.
// For broadcast messages, if a broadcast message of the provided name already exists,
// the validator needs to call a function that updates the selected
// field option of the dropdown menu of the block that was used to create the new message.
// For scalar variables and lists, the validator has the same validation behavior, but needs
// to know which type of variable to check for and needs a type-specific error message
// that is displayed when a variable of the given name and type already exists.
if (type == Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE) {
return Blockly.Variables.validateBroadcastMessageName_(text, workspace, opt_callback);
} else if (type == Blockly.LIST_VARIABLE_TYPE) {
return Blockly.Variables.validateScalarVarOrListName_(text, workspace, additionalVars, false, type,
Blockly.Msg.LIST_ALREADY_EXISTS);
} else {
return Blockly.Variables.validateScalarVarOrListName_(text, workspace, additionalVars, isCloud, type,
Blockly.Msg.VARIABLE_ALREADY_EXISTS);
}
};
/**
* Validate the given name as a broadcast message type.
* @param {string} name The name to validate
* @param {!Blockly.Workspace} workspace The workspace the name should be validated
* against.
* @param {function(?string=)=} opt_callback An optional function to call if a broadcast
* message already exists with the given name. This function will be called on the id
* of the existing variable.
* @return {string} The validated name, or null if invalid.
* @private
*/
Blockly.Variables.validateBroadcastMessageName_ = function(name, workspace, opt_callback) {
if (!name) { // no name was provided or the user cancelled the prompt
return null;
}
var variable = workspace.getVariable(name, Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE);
if (variable) {
// If the user provided a name for a broadcast message that already exists,
// use the provided callback function to update the selected option in
// the field of the block that was used to create
// this message.
if (opt_callback) {
opt_callback(variable.getId());
}
// Return null to signal to the calling function that we do not want to create
// a new variable since one already exists.
return null;
} else {
// The name provided is actually a new name, so the calling
// function should go ahead and create it as a new variable.
return name;
}
};
/**
* Validate the given name as a scalar variable or list type.
* This function is also responsible for any user facing error-handling.
* @param {string} name The name to validate
* @param {!Blockly.Workspace} workspace The workspace the name should be validated
* against.
* @param {Array<string>} additionalVars A list of additional variable names to check
* for conflicts against.
* @param {boolean} isCloud Whether the variable is a cloud variable.
* @param {string} type The type to validate the variable as. This should be one of
* Blockly.SCALAR_VARIABLE_TYPE or Blockly.LIST_VARIABLE_TYPE.
* @param {string} errorMsg The type-specific error message the user should see
* if a variable of the validated, given name and type already exists.
* @return {string} The validated name, or null if invalid.
* @private
*/
Blockly.Variables.validateScalarVarOrListName_ = function(name, workspace, additionalVars,
isCloud, type, errorMsg) {
// For scalar variables, we don't want leading or trailing white space
name = Blockly.Variables.trimName_(name);
if (!name) {
return null;
}
if (isCloud) {
name = Blockly.Variables.CLOUD_PREFIX + name;
}
if (workspace.getVariable(name, type) || additionalVars.indexOf(name) >= 0) {
// error
Blockly.alert(errorMsg.replace('%1', name));
return null;
} else { // trimmed name is valid
return name;
}
};
/**
* Rename a variable with the given workspace, variableType, and oldName.
* @param {!Blockly.Workspace} workspace The workspace on which to rename the
* variable.
* @param {Blockly.VariableModel} variable Variable to rename.
* @param {function(?string=)=} opt_callback A callback. It will
* be passed an acceptable new variable name, or null if change is to be
* aborted (cancel button), or undefined if an existing variable was chosen.
*/
Blockly.Variables.renameVariable = function(workspace, variable,
opt_callback) {
// Validation and modal message/title depends on the variable type
var promptMsg, modalTitle;
var varType = variable.type;
if (varType == Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE) {
console.warn('Unexpected attempt to rename a broadcast message with ' +
'id: ' + variable.getId() + ' and name: ' + variable.name);
return;
}
if (varType == Blockly.LIST_VARIABLE_TYPE) {
promptMsg = Blockly.Msg.RENAME_LIST_TITLE;
modalTitle = Blockly.Msg.RENAME_LIST_MODAL_TITLE;
} else {
// Default for all other types of variables
promptMsg = Blockly.Msg.RENAME_VARIABLE_TITLE;
modalTitle = Blockly.Msg.RENAME_VARIABLE_MODAL_TITLE;
}
var validate = Blockly.Variables.nameValidator_.bind(null, varType);
var promptText = promptMsg.replace('%1', variable.name);
var promptDefaultText = variable.name;
if (variable.isCloud && variable.name.indexOf(Blockly.Variables.CLOUD_PREFIX) == 0) {
promptDefaultText = promptDefaultText.substring(Blockly.Variables.CLOUD_PREFIX.length);
}
Blockly.prompt(promptText, promptDefaultText,
function(newName, additionalVars) {
if (variable.isCloud &&
newName.length > 0 && newName.indexOf(Blockly.Variables.CLOUD_PREFIX) == 0) {
newName = newName.substring(Blockly.Variables.CLOUD_PREFIX.length);
// The name validator will add the prefix back
}
additionalVars = additionalVars || [];
var additionalVarNames = variable.isLocal ? [] : additionalVars;
var validatedText = validate(newName, workspace, additionalVarNames, variable.isCloud);
if (validatedText) {
workspace.renameVariableById(variable.getId(), validatedText);
if (opt_callback) {
opt_callback(newName);
}
} else {
// User canceled prompt without a value.
if (opt_callback) {
opt_callback(null);
}
}
}, modalTitle, varType);
};
/**
* Strip leading and trailing whitespace from the given name, for use with
* user provided name for scalar variables and lists.
* @param {string} name The user-provided name of the variable.
* @return {string} The trimmed name, or whatever falsey value was originally provided.
*/
Blockly.Variables.trimName_ = function(name) {
if (name) {
return goog.string.trim(name);
} else {
// Return whatever was provided
return name;
}
};
/**
* Generate XML string for variable field.
* @param {!Blockly.VariableModel} variableModel The variable model to generate
* an XML string from.
* @param {?string} opt_name The optional name of the field, such as "VARIABLE"
* or "LIST". Defaults to "VARIABLE".
* @return {string} The generated XML.
* @private
*/
Blockly.Variables.generateVariableFieldXml_ = function(variableModel, opt_name) {
// The variable name may be user input, so it may contain characters that need
// to be escaped to create valid XML.
var typeString = variableModel.type;
if (typeString == '') {
typeString = '\'\'';
}
var fieldName = opt_name || 'VARIABLE';
var text = '<field name="' + fieldName + '" id="' + variableModel.getId() +
'" variabletype="' + goog.string.htmlEscape(typeString) +
'">' + goog.string.htmlEscape(variableModel.name) + '</field>';
return text;
};
/**
* Helper function to look up or create a variable on the given workspace.
* If no variable exists, creates and returns it.
* @param {!Blockly.Workspace} workspace The workspace to search for the
* variable. It may be a flyout workspace or main workspace.
* @param {string} id The ID to use to look up or create the variable, or null.
* @param {string=} opt_name The string to use to look up or create the
* variable.
* @param {string=} opt_type The type to use to look up or create the variable.
* @return {!Blockly.VariableModel} The variable corresponding to the given ID
* or name + type combination.
* @package
*/
Blockly.Variables.getOrCreateVariablePackage = function(workspace, id, opt_name,
opt_type) {
var variable = Blockly.Variables.getVariable(workspace, id, opt_name,
opt_type);
if (!variable) {
variable = Blockly.Variables.createVariable_(workspace, id, opt_name,
opt_type);
}
return variable;
};
/**
* Look up a variable on the given workspace.
* Always looks in the main workspace before looking in the flyout workspace.
* Always prefers lookup by ID to lookup by name + type.
* @param {!Blockly.Workspace} workspace The workspace to search for the
* variable. It may be a flyout workspace or main workspace.
* @param {string} id The ID to use to look up the variable, or null.
* @param {string=} opt_name The string to use to look up the variable. Only
* used if lookup by ID fails.
* @param {string=} opt_type The type to use to look up the variable. Only used
* if lookup by ID fails.
* @return {?Blockly.VariableModel} The variable corresponding to the given ID
* or name + type combination, or null if not found.
* @package
*/
Blockly.Variables.getVariable = function(workspace, id, opt_name, opt_type) {
var potentialVariableMap = workspace.getPotentialVariableMap();
// Try to just get the variable, by ID if possible.
if (id) {
// Look in the real variable map before checking the potential variable map.
var variable = workspace.getVariableById(id);
if (!variable && potentialVariableMap) {
variable = potentialVariableMap.getVariableById(id);
}
} else if (opt_name) {
if (opt_type == undefined) {
throw new Error('Tried to look up a variable by name without a type');
}
// Otherwise look up by name and type.
var variable = workspace.getVariable(opt_name, opt_type);
if (!variable && potentialVariableMap) {
variable = potentialVariableMap.getVariable(opt_name, opt_type);
}
}
return variable;
};
/**
* Helper function to create a variable on the given workspace.
* @param {!Blockly.Workspace} workspace The workspace in which to create the
* variable. It may be a flyout workspace or main workspace.
* @param {string} id The ID to use to create the variable, or null.
* @param {string=} opt_name The string to use to create the variable.
* @param {string=} opt_type The type to use to create the variable.
* @return {!Blockly.VariableModel} The variable corresponding to the given ID
* or name + type combination.
* @private
*/
Blockly.Variables.createVariable_ = function(workspace, id, opt_name,
opt_type) {
var potentialVariableMap = workspace.getPotentialVariableMap();
// Variables without names get uniquely named for this workspace.
if (!opt_name) {
var ws = workspace.isFlyout ? workspace.targetWorkspace : workspace;
opt_name = Blockly.Variables.generateUniqueName(ws);
}
// Create a potential variable if in the flyout.
if (potentialVariableMap) {
var variable = potentialVariableMap.createVariable(opt_name, opt_type, id);
} else { // In the main workspace, create a real variable.
var variable = workspace.createVariable(opt_name, opt_type, id);
}
return variable;
};
/**
* Helper function to get the list of variables that have been added to the
* workspace after adding a new block, using the given list of variables that
* were in the workspace before the new block was added.
* @param {!Blockly.Workspace} workspace The workspace to inspect.
* @param {!Array.<!Blockly.VariableModel>} originalVariables The array of
* variables that existed in the workspace before adding the new block.
* @return {!Array.<!Blockly.VariableModel>} The new array of variables that were
* freshly added to the workspace after creating the new block, or [] if no
* new variables were added to the workspace.
* @package
*/
Blockly.Variables.getAddedVariables = function(workspace, originalVariables) {
var allCurrentVariables = workspace.getAllVariables();
var addedVariables = [];
if (originalVariables.length != allCurrentVariables.length) {
for (var i = 0; i < allCurrentVariables.length; i++) {
var variable = allCurrentVariables[i];
// For any variable that is present in allCurrentVariables but not
// present in originalVariables, add the variable to addedVariables.
if (!originalVariables.includes(variable)) {
addedVariables.push(variable);
}
}
}
return addedVariables;
};

View File

@@ -0,0 +1,199 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Object representing a warning.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Warning');
goog.require('Blockly.Bubble');
goog.require('Blockly.Events.Ui');
goog.require('Blockly.Icon');
/**
* Class for a warning.
* @param {!Blockly.Block} block The block associated with this warning.
* @extends {Blockly.Icon}
* @constructor
*/
Blockly.Warning = function(block) {
Blockly.Warning.superClass_.constructor.call(this, block);
this.createIcon();
// The text_ object can contain multiple warnings.
this.text_ = {};
};
goog.inherits(Blockly.Warning, Blockly.Icon);
/**
* Does this icon get hidden when the block is collapsed.
*/
Blockly.Warning.prototype.collapseHidden = false;
/**
* Draw the warning icon.
* @param {!Element} group The icon group.
* @private
*/
Blockly.Warning.prototype.drawIcon_ = function(group) {
// Triangle with rounded corners.
Blockly.utils.createSvgElement('path',
{
'class': 'blocklyIconShape',
'd': 'M2,15Q-1,15 0.5,12L6.5,1.7Q8,-1 9.5,1.7L15.5,12Q17,15 14,15z'
},
group);
// Can't use a real '!' text character since different browsers and operating
// systems render it differently.
// Body of exclamation point.
Blockly.utils.createSvgElement('path',
{
'class': 'blocklyIconSymbol',
'd': 'm7,4.8v3.16l0.27,2.27h1.46l0.27,-2.27v-3.16z'
},
group);
// Dot of exclamation point.
Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyIconSymbol',
'x': '7',
'y': '11',
'height': '2',
'width': '2'
},
group);
};
/**
* Create the text for the warning's bubble.
* @param {string} text The text to display.
* @return {!SVGTextElement} The top-level node of the text.
* @private
*/
Blockly.Warning.textToDom_ = function(text) {
var paragraph = /** @type {!SVGTextElement} */
(Blockly.utils.createSvgElement(
'text',
{
'class': 'blocklyText blocklyBubbleText',
'y': Blockly.Bubble.BORDER_WIDTH
},
null)
);
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var tspanElement = Blockly.utils.createSvgElement('tspan',
{'dy': '1em', 'x': Blockly.Bubble.BORDER_WIDTH}, paragraph);
var textNode = document.createTextNode(lines[i]);
tspanElement.appendChild(textNode);
}
return paragraph;
};
/**
* Show or hide the warning bubble.
* @param {boolean} visible True if the bubble should be visible.
*/
Blockly.Warning.prototype.setVisible = function(visible) {
if (visible == this.isVisible()) {
// No change.
return;
}
Blockly.Events.fire(
new Blockly.Events.Ui(this.block_, 'warningOpen', !visible, visible));
if (visible) {
// Create the bubble to display all warnings.
var paragraph = Blockly.Warning.textToDom_(this.getText());
this.bubble_ = new Blockly.Bubble(
/** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
paragraph, this.block_.svgPath_, this.iconXY_, null, null);
if (this.block_.RTL) {
// Right-align the paragraph.
// This cannot be done until the bubble is rendered on screen.
var maxWidth = paragraph.getBBox().width;
for (var i = 0, textElement; textElement = paragraph.childNodes[i]; i++) {
textElement.setAttribute('text-anchor', 'end');
textElement.setAttribute('x', maxWidth + Blockly.Bubble.BORDER_WIDTH);
}
}
this.updateColour();
// Bump the warning into the right location.
var size = this.bubble_.getBubbleSize();
this.bubble_.setBubbleSize(size.width, size.height);
} else {
// Dispose of the bubble.
this.bubble_.dispose();
this.bubble_ = null;
this.body_ = null;
}
};
/**
* Bring the warning to the top of the stack when clicked on.
* @param {!Event} _e Mouse up event.
* @private
*/
Blockly.Warning.prototype.bodyFocus_ = function(_e) {
this.bubble_.promote_();
};
/**
* Set this warning's text.
* @param {string} text Warning text (or '' to delete).
* @param {string} id An ID for this text entry to be able to maintain
* multiple warnings.
*/
Blockly.Warning.prototype.setText = function(text, id) {
if (this.text_[id] == text) {
return;
}
if (text) {
this.text_[id] = text;
} else {
delete this.text_[id];
}
if (this.isVisible()) {
this.setVisible(false);
this.setVisible(true);
}
};
/**
* Get this warning's texts.
* @return {string} All texts concatenated into one string.
*/
Blockly.Warning.prototype.getText = function() {
var allWarnings = [];
for (var id in this.text_) {
allWarnings.push(this.text_[id]);
}
return allWarnings.join('\n');
};
/**
* Dispose of this warning.
*/
Blockly.Warning.prototype.dispose = function() {
this.block_.warning = null;
Blockly.Icon.prototype.dispose.call(this);
};

View File

@@ -0,0 +1,344 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2013 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 A div that floats on top of Blockly. This singleton contains
* temporary HTML UI widgets that the user is currently interacting with.
* E.g. text input areas, colour pickers, context menus.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* @name Blockly.WidgetDiv
* @namespace
**/
goog.provide('Blockly.WidgetDiv');
goog.require('Blockly.Css');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.style');
/**
* The HTML container. Set once by Blockly.WidgetDiv.createDom.
* @type {Element}
*/
Blockly.WidgetDiv.DIV = null;
/**
* The object currently using this container.
* @type {Object}
* @private
*/
Blockly.WidgetDiv.owner_ = null;
/**
* Optional cleanup function set by whichever object uses the widget.
* This is called as soon as a dispose is desired. If the dispose should
* be animated, the animation should start on the call of dispose_.
* @type {Function}
* @private
*/
Blockly.WidgetDiv.dispose_ = null;
/**
* Optional function called at the end of a dispose animation.
* Set by whichever object is using the widget.
* @type {Function}
* @private
*/
Blockly.WidgetDiv.disposeAnimationFinished_ = null;
/**
* Timer ID for the dispose animation.
* @type {number}
* @private
*/
Blockly.WidgetDiv.disposeAnimationTimer_ = null;
/**
* Length of time in seconds for the dispose animation.
* @type {number}
* @private
*/
Blockly.WidgetDiv.disposeAnimationTimerLength_ = 0;
/**
* Create the widget div and inject it onto the page.
*/
Blockly.WidgetDiv.createDom = function() {
if (Blockly.WidgetDiv.DIV) {
return; // Already created.
}
// Create an HTML container for popup overlays (e.g. editor widgets).
Blockly.WidgetDiv.DIV =
goog.dom.createDom(goog.dom.TagName.DIV, 'blocklyWidgetDiv');
document.body.appendChild(Blockly.WidgetDiv.DIV);
};
/**
* Initialize and display the widget div. Close the old one if needed.
* @param {!Object} newOwner The object that will be using this container.
* @param {boolean} rtl Right-to-left (true) or left-to-right (false).
* @param {Function=} opt_dispose Optional cleanup function to be run when the widget
* is closed. If the dispose is animated, this function must start the animation.
* @param {Function=} opt_disposeAnimationFinished Optional cleanup function to be run
* when the widget is done animating and must disappear.
* @param {number=} opt_disposeAnimationTimerLength Length of animation time in seconds
if a dispose animation is provided.
*/
Blockly.WidgetDiv.show = function(newOwner, rtl, opt_dispose,
opt_disposeAnimationFinished, opt_disposeAnimationTimerLength) {
Blockly.WidgetDiv.hide();
Blockly.WidgetDiv.owner_ = newOwner;
Blockly.WidgetDiv.dispose_ = opt_dispose;
Blockly.WidgetDiv.disposeAnimationFinished_ = opt_disposeAnimationFinished;
Blockly.WidgetDiv.disposeAnimationTimerLength_ = opt_disposeAnimationTimerLength;
// Temporarily move the widget to the top of the screen so that it does not
// cause a scrollbar jump in Firefox when displayed.
var xy = goog.style.getViewportPageOffset(document);
Blockly.WidgetDiv.DIV.style.top = xy.y + 'px';
Blockly.WidgetDiv.DIV.style.direction = rtl ? 'rtl' : 'ltr';
Blockly.WidgetDiv.DIV.style.display = 'block';
};
/**
* Repositions the widgetDiv on window resize. If it doesn't know how to
* calculate the new position, it wll just hide it instead.
*/
Blockly.WidgetDiv.repositionForWindowResize = function() {
// This condition mainly catches the widget div when it is being used as a
// text input. It is important not to close it in this case because on Android,
// when a field is focused, the soft keyboard opens triggering a window resize
// event and we want the widget div to stick around so users can type into it.
if (Blockly.WidgetDiv.owner_
&& Blockly.WidgetDiv.owner_.getScaledBBox_
&& Blockly.WidgetDiv.owner_.getSize) {
var widgetScaledBBox = Blockly.WidgetDiv.owner_.getScaledBBox_();
var widgetSize = Blockly.WidgetDiv.owner_.getSize();
Blockly.WidgetDiv.positionInternal_(widgetScaledBBox.left, widgetScaledBBox.top,
widgetSize.height);
} else {
Blockly.WidgetDiv.hide();
}
};
/**
* Destroy the widget and hide the div.
* @param {boolean=} opt_noAnimate If set, animation will not be run for the hide.
*/
Blockly.WidgetDiv.hide = function(opt_noAnimate) {
if (Blockly.WidgetDiv.disposeAnimationTimer_) {
// An animation timer is set already.
// This happens when a previous widget was animating out,
// but Blockly is hiding the widget to create a new one.
// So, short-circuit the animation and clear the timer.
window.clearTimeout(Blockly.WidgetDiv.disposeAnimationTimer_);
Blockly.WidgetDiv.disposeAnimationFinished_ && Blockly.WidgetDiv.disposeAnimationFinished_();
Blockly.WidgetDiv.disposeAnimationFinished_ = null;
Blockly.WidgetDiv.disposeAnimationTimer_ = null;
Blockly.WidgetDiv.owner_ = null;
Blockly.WidgetDiv.hideAndClearDom_();
} else if (Blockly.WidgetDiv.isVisible()) {
// No animation timer set, but the widget is visible
// Start animation out (or immediately hide)
Blockly.WidgetDiv.dispose_ && Blockly.WidgetDiv.dispose_();
Blockly.WidgetDiv.dispose_ = null;
// If we want to animate out, set the appropriate timer for final dispose.
if (Blockly.WidgetDiv.disposeAnimationFinished_ && !opt_noAnimate) {
Blockly.WidgetDiv.disposeAnimationTimer_ = window.setTimeout(
Blockly.WidgetDiv.hide, // Come back to hide and take the first branch.
Blockly.WidgetDiv.disposeAnimationTimerLength_ * 1000
);
} else {
// No timer provided (or no animation desired) - auto-hide the DOM now.
Blockly.WidgetDiv.disposeAnimationFinished_ && Blockly.WidgetDiv.disposeAnimationFinished_();
Blockly.WidgetDiv.disposeAnimationFinished_ = null;
Blockly.WidgetDiv.owner_ = null;
Blockly.WidgetDiv.hideAndClearDom_();
}
}
};
/**
* Hide all DOM for the WidgetDiv, and clear its children.
* @private
*/
Blockly.WidgetDiv.hideAndClearDom_ = function() {
Blockly.WidgetDiv.DIV.style.display = 'none';
Blockly.WidgetDiv.DIV.style.left = '';
Blockly.WidgetDiv.DIV.style.top = '';
Blockly.WidgetDiv.DIV.style.height = '';
goog.dom.removeChildren(Blockly.WidgetDiv.DIV);
};
/**
* Is the container visible?
* @return {boolean} True if visible.
*/
Blockly.WidgetDiv.isVisible = function() {
return !!Blockly.WidgetDiv.owner_;
};
/**
* Destroy the widget and hide the div if it is being used by the specified
* object.
* @param {!Object} oldOwner The object that was using this container.
*/
Blockly.WidgetDiv.hideIfOwner = function(oldOwner) {
if (Blockly.WidgetDiv.owner_ == oldOwner) {
Blockly.WidgetDiv.hide();
}
};
/**
* Position the widget at a given location. Prevent the widget from going
* offscreen top or left (right in RTL).
* @param {number} anchorX Horizontal location (window coordinates, not body).
* @param {number} anchorY Vertical location (window coordinates, not body).
* @param {!goog.math.Size} windowSize Height/width of window.
* @param {!goog.math.Coordinate} scrollOffset X/y of window scrollbars.
* @param {boolean} rtl True if RTL, false if LTR.
*/
Blockly.WidgetDiv.position = function(anchorX, anchorY, windowSize,
scrollOffset, rtl) {
// Don't let the widget go above the top edge of the window.
if (anchorY < scrollOffset.y) {
anchorY = scrollOffset.y;
}
if (rtl) {
// Don't let the widget go right of the right edge of the window.
if (anchorX > windowSize.width + scrollOffset.x) {
anchorX = windowSize.width + scrollOffset.x;
}
} else {
// Don't let the widget go left of the left edge of the window.
if (anchorX < scrollOffset.x) {
anchorX = scrollOffset.x;
}
}
Blockly.WidgetDiv.positionInternal_(anchorX, anchorY, windowSize.height);
};
/**
* Set the widget div's position and height. This function does nothing clever:
* it will not ensure that your widget div ends up in the visible window.
* @param {number} x Horizontal location (window coordinates, not body).
* @param {number} y Vertical location (window coordinates, not body).
* @param {number} height The height of the widget div (pixels).
* @private
*/
Blockly.WidgetDiv.positionInternal_ = function(x, y, height) {
Blockly.WidgetDiv.DIV.style.left = x + 'px';
Blockly.WidgetDiv.DIV.style.top = y + 'px';
Blockly.WidgetDiv.DIV.style.height = height + 'px';
};
/**
* Position the widget div based on an anchor rectangle.
* The widget should be placed adjacent to but not overlapping the anchor
* rectangle. The preferred position is directly below and aligned to the left
* (ltr) or right (rtl) side of the anchor.
* @param {!Object} viewportBBox The bounding rectangle of the current viewport,
* in window coordinates.
* @param {!Object} anchorBBox The bounding rectangle of the anchor, in window
* coordinates.
* @param {!goog.math.Size} widgetSize The size of the widget that is inside the
* widget div, in window coordinates.
* @param {boolean} rtl Whether the workspace is in RTL mode. This determines
* horizontal alignment.
* @package
*/
Blockly.WidgetDiv.positionWithAnchor = function(viewportBBox, anchorBBox,
widgetSize, rtl) {
var y = Blockly.WidgetDiv.calculateY_(viewportBBox, anchorBBox, widgetSize);
var x = Blockly.WidgetDiv.calculateX_(viewportBBox, anchorBBox, widgetSize,
rtl);
if (y < 0) {
Blockly.WidgetDiv.positionInternal_(x, 0, widgetSize.height + y);
}
else {
Blockly.WidgetDiv.positionInternal_(x, y, widgetSize.height);
}
};
/**
* Calculate an x position (in window coordinates) such that the widget will not
* be offscreen on the right or left.
* @param {!Object} viewportBBox The bounding rectangle of the current viewport,
* in window coordinates.
* @param {!Object} anchorBBox The bounding rectangle of the anchor, in window
* coordinates.
* @param {goog.math.Size} widgetSize The dimensions of the widget inside the
* widget div.
* @param {boolean} rtl Whether the Blockly workspace is in RTL mode.
* @return {number} A valid x-coordinate for the top left corner of the widget
* div, in window coordinates.
* @private
*/
Blockly.WidgetDiv.calculateX_ = function(viewportBBox, anchorBBox, widgetSize,
rtl) {
if (rtl) {
// Try to align the right side of the field and the right side of the widget.
var widgetLeft = anchorBBox.right - widgetSize.width;
// Don't go offscreen left.
var x = Math.max(widgetLeft, viewportBBox.left);
// But really don't go offscreen right:
return Math.min(x, viewportBBox.right - widgetSize.width);
} else {
// Try to align the left side of the field and the left side of the widget.
// Don't go offscreen right.
var x = Math.min(anchorBBox.left,
viewportBBox.right - widgetSize.width);
// But left is more important, because that's where the text is.
return Math.max(x, viewportBBox.left);
}
};
/**
* Calculate a y position (in window coordinates) such that the widget will not
* be offscreen on the top or bottom.
* @param {!Object} viewportBBox The bounding rectangle of the current viewport,
* in window coordinates.
* @param {!Object} anchorBBox The bounding rectangle of the anchor, in window
* coordinates.
* @param {goog.math.Size} widgetSize The dimensions of the widget inside the
* widget div.
* @return {number} A valid y-coordinate for the top left corner of the widget
* div, in window coordinates.
* @private
*/
Blockly.WidgetDiv.calculateY_ = function(viewportBBox, anchorBBox, widgetSize) {
// Flip the widget vertically if off the bottom.
if (anchorBBox.bottom + widgetSize.height >=
viewportBBox.bottom) {
// The bottom of the widget is at the top of the field.
return anchorBBox.top - widgetSize.height;
// The widget could go off the top of the window, but it would also go off
// the bottom. The window is just too small.
} else {
// The top of the widget is at the bottom of the field.
return anchorBBox.bottom;
}
};

View File

@@ -0,0 +1,673 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 Object representing a workspace.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Workspace');
goog.require('Blockly.VariableMap');
goog.require('Blockly.WorkspaceComment');
goog.require('goog.array');
goog.require('goog.math');
/**
* Class for a workspace. This is a data structure that contains blocks.
* There is no UI, and can be created headlessly.
* @param {!Blockly.Options=} opt_options Dictionary of options.
* @constructor
*/
Blockly.Workspace = function(opt_options) {
/** @type {string} */
this.id = Blockly.utils.genUid();
Blockly.Workspace.WorkspaceDB_[this.id] = this;
/** @type {!Blockly.Options} */
this.options = opt_options || {};
/** @type {boolean} */
this.RTL = !!this.options.RTL;
/** @type {boolean} */
this.horizontalLayout = !!this.options.horizontalLayout;
/** @type {number} */
this.toolboxPosition = this.options.toolboxPosition;
/**
* @type {!Array.<!Blockly.Block>}
* @private
*/
this.topBlocks_ = [];
/**
* @type {!Array.<!Blockly.WorkspaceComment>}
* @private
*/
this.topComments_ = [];
/**
* @type {!Object}
* @private
*/
this.commentDB_ = Object.create(null);
/**
* @type {!Array.<!Function>}
* @private
*/
this.listeners_ = [];
/** @type {!Array.<!Function>} */
this.tapListeners_ = [];
/**
* @type {!Array.<!Blockly.Events.Abstract>}
* @protected
*/
this.undoStack_ = [];
/**
* @type {!Array.<!Blockly.Events.Abstract>}
* @protected
*/
this.redoStack_ = [];
/**
* @type {!Object}
* @private
*/
this.blockDB_ = Object.create(null);
/**
* @type {!Blockly.VariableMap}
* A map from variable type to list of variable names. The lists contain all
* of the named variables in the workspace, including variables
* that are not currently in use.
* @private
*/
this.variableMap_ = new Blockly.VariableMap(this);
/**
* Blocks in the flyout can refer to variables that don't exist in the main
* workspace. For instance, the "get item in list" block refers to an "item"
* variable regardless of whether the variable has been created yet.
* A FieldVariable must always refer to a Blockly.VariableModel. We reconcile
* these by tracking "potential" variables in the flyout. These variables
* become real when references to them are dragged into the main workspace.
* @type {!Blockly.VariableMap}
* @private
*/
this.potentialVariableMap_ = null;
};
/**
* Returns `true` if the workspace is visible and `false` if it's headless.
* @type {boolean}
*/
Blockly.Workspace.prototype.rendered = false;
/**
* Returns `true` if the workspace is currently in the process of a bulk clear.
* @type {boolean}
* @package
*/
Blockly.Workspace.prototype.isClearing = false;
/**
* Maximum number of undo events in stack. `0` turns off undo, `Infinity` sets it to unlimited.
* @type {number}
*/
Blockly.Workspace.prototype.MAX_UNDO = 1024;
// TODO (#1354) Update this function when it is fixed upstream
/**
* Refresh the toolbox. This is a no-op in a non-rendered workspace,
* but may be overriden by subclasses.
* @private
*/
Blockly.Workspace.prototype.refreshToolboxSelection_ = function() {
// No-op. Overriden by subclass.
};
/**
* Dispose of this workspace.
* Unlink from all DOM elements to prevent memory leaks.
*/
Blockly.Workspace.prototype.dispose = function() {
this.listeners_.length = 0;
this.clear();
// Remove from workspace database.
delete Blockly.Workspace.WorkspaceDB_[this.id];
};
/**
* Angle away from the horizontal to sweep for blocks. Order of execution is
* generally top to bottom, but a small angle changes the scan to give a bit of
* a left to right bias (reversed in RTL). Units are in degrees.
* See: http://tvtropes.org/pmwiki/pmwiki.php/Main/DiagonalBilling.
*/
Blockly.Workspace.SCAN_ANGLE = 3;
/**
* Add a block to the list of top blocks.
* @param {!Blockly.Block} block Block to add.
*/
Blockly.Workspace.prototype.addTopBlock = function(block) {
this.topBlocks_.push(block);
};
/**
* Remove a block from the list of top blocks.
* @param {!Blockly.Block} block Block to remove.
*/
Blockly.Workspace.prototype.removeTopBlock = function(block) {
if (!goog.array.remove(this.topBlocks_, block)) {
throw 'Block not present in workspace\'s list of top-most blocks.';
}
};
/**
* Finds the top-level blocks and returns them. Blocks are optionally sorted
* by position; top to bottom (with slight LTR or RTL bias).
* @param {boolean} ordered Sort the list if true.
* @return {!Array.<!Blockly.Block>} The top-level block objects.
*/
Blockly.Workspace.prototype.getTopBlocks = function(ordered) {
// Copy the topBlocks_ list.
var blocks = [].concat(this.topBlocks_);
if (ordered && blocks.length > 1) {
var offset = Math.sin(goog.math.toRadians(Blockly.Workspace.SCAN_ANGLE));
if (this.RTL) {
offset *= -1;
}
blocks.sort(function(a, b) {
var aXY = a.getRelativeToSurfaceXY();
var bXY = b.getRelativeToSurfaceXY();
return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
});
}
return blocks;
};
/**
* Add a comment to the list of top comments.
* @param {!Blockly.WorkspaceComment} comment comment to add.
* @package
*/
Blockly.Workspace.prototype.addTopComment = function(comment) {
this.topComments_.push(comment);
// Note: If the comment database starts to hold block comments, this may need
// to move to a separate function.
if (this.commentDB_[comment.id]) {
console.warn('Overriding an existing comment on this workspace, with id "' +
comment.id + '"');
}
this.commentDB_[comment.id] = comment;
};
/**
* Remove a comment from the list of top comments.
* @param {!Blockly.WorkspaceComment} comment comment to remove.
* @package
*/
Blockly.Workspace.prototype.removeTopComment = function(comment) {
if (!goog.array.remove(this.topComments_, comment)) {
throw 'Comment not present in workspace\'s list of top-most comments.';
}
// Note: If the comment database starts to hold block comments, this may need
// to move to a separate function.
delete this.commentDB_[comment.id];
};
/**
* Finds the top-level comments and returns them. Comments are optionally sorted
* by position; top to bottom (with slight LTR or RTL bias).
* @param {boolean} ordered Sort the list if true.
* @return {!Array.<!Blockly.WorkspaceComment>} The top-level comment objects.
* @package
*/
Blockly.Workspace.prototype.getTopComments = function(ordered) {
// Copy the topComments_ list.
var comments = [].concat(this.topComments_);
if (ordered && comments.length > 1) {
var offset = Math.sin(goog.math.toRadians(Blockly.Workspace.SCAN_ANGLE));
if (this.RTL) {
offset *= -1;
}
comments.sort(function(a, b) {
var aXY = a instanceof Blockly.ScratchBlockComment ? a.getXY() : a.getRelativeToSurfaceXY();
var bXY = b instanceof Blockly.ScratchBlockComment ? b.getXY() : b.getRelativeToSurfaceXY();
return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
});
}
return comments;
};
/**
* Find all blocks in workspace. Blocks are optionally sorted
* by position; top to bottom (with slight LTR or RTL bias).
* @param {boolean} ordered Sort the list if true.
* @return {!Array.<!Blockly.Block>} Array of blocks.
*/
Blockly.Workspace.prototype.getAllBlocks = function(ordered) {
if (ordered) {
// Slow, but ordered.
// This gets all levels of descendants because getDescendants
// is called recuusively. They are added to a new list, not the
// list that it's iterating over.
var topBlocks = this.getTopBlocks(true);
var blocks = [];
for (var i = 0; i < topBlocks.length; i++) {
blocks.push.apply(blocks, topBlocks[i].getDescendants(true));
}
} else {
// Fast, but in no particular order.
// This gets all of levels of descendants by always adding to the
// list that it's iterating over.
var blocks = this.getTopBlocks(false);
for (var i = 0; i < blocks.length; i++) {
blocks.push.apply(blocks, blocks[i].getChildren(false));
}
}
return blocks;
};
/**
* Dispose of all blocks and comments in workspace.
*/
Blockly.Workspace.prototype.clear = function() {
this.isClearing = true;
var existingGroup = Blockly.Events.getGroup();
if (!existingGroup) {
Blockly.Events.setGroup(true);
}
while (this.topBlocks_.length) {
this.topBlocks_[0].dispose();
}
while (this.topComments_.length) {
this.topComments_[this.topComments_.length - 1].dispose();
}
if (!existingGroup) {
Blockly.Events.setGroup(false);
}
this.variableMap_.clear();
// Any block with a drop-down or WidgetDiv was disposed.
if (Blockly.DropDownDiv) {
Blockly.DropDownDiv.hideWithoutAnimation();
}
if (Blockly.WidgetDiv) {
Blockly.WidgetDiv.hide(true);
}
if (this.potentialVariableMap_) {
this.potentialVariableMap_.clear();
}
this.isClearing = false;
};
/* Begin functions that are just pass-throughs to the variable map. */
/**
* Rename a variable by updating its name in the variable map. Identify the
* variable to rename with the given ID.
* @param {string} id ID of the variable to rename.
* @param {string} newName New variable name.
*/
Blockly.Workspace.prototype.renameVariableById = function(id, newName) {
this.variableMap_.renameVariableById(id, newName);
};
/**
* Create a variable with a given name, optional type, and optional ID.
* @param {!string} name The name of the variable. This must be unique across
* each variable type.
* @param {?string} opt_type The type of the variable like 'int' or 'string'.
* Does not need to be unique. Field_variable can filter variables based on
* their type. This will default to '' which is a specific type.
* @param {string=} opt_id The unique ID of the variable. This will default to
* a UUID.
* @param {boolean=} opt_isLocal Whether the variable to create is locally scoped.
* @param {boolean=} opt_isCloud Whether the variable to create is locally scoped.
* @return {?Blockly.VariableModel} The newly created variable.
*/
Blockly.Workspace.prototype.createVariable = function(name, opt_type, opt_id,
opt_isLocal, opt_isCloud) {
return this.variableMap_.createVariable(name, opt_type, opt_id, opt_isLocal, opt_isCloud);
};
/**
* Find all the uses of the given variable, which is identified by ID.
* @param {string} id ID of the variable to find.
* @return {!Array.<!Blockly.Block>} Array of block usages.
*/
Blockly.Workspace.prototype.getVariableUsesById = function(id) {
return this.variableMap_.getVariableUsesById(id);
};
/**
* Delete a variables by the passed in ID and all of its uses from this
* workspace. May prompt the user for confirmation.
* @param {string} id ID of variable to delete.
*/
Blockly.Workspace.prototype.deleteVariableById = function(id) {
this.variableMap_.deleteVariableById(id);
};
/**
* Deletes a variable and all of its uses from this workspace without asking the
* user for confirmation.
* @param {!Blockly.VariableModel} variable Variable to delete.
* @param {!Array.<!Blockly.Block>} uses An array of uses of the variable.
* @private
*/
Blockly.Workspace.prototype.deleteVariableInternal_ = function(variable, uses) {
this.variableMap_.deleteVariableInternal_(variable, uses);
};
/**
* Check whether a variable exists with the given name. The check is
* case-insensitive.
* @param {string} _name The name to check for.
* @return {number} The index of the name in the variable list, or -1 if it is
* not present.
* @deprecated April 2017
*/
Blockly.Workspace.prototype.variableIndexOf = function(_name) {
console.warn(
'Deprecated call to Blockly.Workspace.prototype.variableIndexOf');
return -1;
};
/**
* Find the variable by the given name and return it. Return null if it is not
* found.
* TODO (#1199): Possibly delete this function.
* @param {!string} name The name to check for.
* @param {string=} opt_type The type of the variable. If not provided it
* defaults to the empty string, which is a specific type.
* @return {?Blockly.VariableModel} the variable with the given name.
*/
Blockly.Workspace.prototype.getVariable = function(name, opt_type) {
return this.variableMap_.getVariable(name, opt_type);
};
/**
* Find the variable by the given ID and return it. Return null if it is not
* found.
* @param {!string} id The ID to check for.
* @return {?Blockly.VariableModel} The variable with the given ID.
*/
Blockly.Workspace.prototype.getVariableById = function(id) {
return this.variableMap_.getVariableById(id);
};
/**
* Find the variable with the specified type. If type is null, return list of
* variables with empty string type.
* @param {?string} type Type of the variables to find.
* @return {Array.<Blockly.VariableModel>} The sought after variables of the
* passed in type. An empty array if none are found.
*/
Blockly.Workspace.prototype.getVariablesOfType = function(type) {
return this.variableMap_.getVariablesOfType(type);
};
/**
* Return all variable types.
* @return {!Array.<string>} List of variable types.
* @package
*/
Blockly.Workspace.prototype.getVariableTypes = function() {
return this.variableMap_.getVariableTypes();
};
/**
* Return all variables of all types.
* @return {!Array.<Blockly.VariableModel>} List of variable models.
*/
Blockly.Workspace.prototype.getAllVariables = function() {
return this.variableMap_.getAllVariables();
};
/* End functions that are just pass-throughs to the variable map. */
/**
* Returns the horizontal offset of the workspace.
* Intended for LTR/RTL compatibility in XML.
* Not relevant for a headless workspace.
* @return {number} Width.
*/
Blockly.Workspace.prototype.getWidth = function() {
return 0;
};
/**
* Obtain a newly created block.
* @param {?string} prototypeName Name of the language object containing
* type-specific functions for this block.
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
* create a new ID.
* @return {!Blockly.Block} The created block.
*/
Blockly.Workspace.prototype.newBlock = function(prototypeName, opt_id) {
return new Blockly.Block(this, prototypeName, opt_id);
};
/**
* Undo or redo the previous action.
* @param {boolean} redo False if undo, true if redo.
*/
Blockly.Workspace.prototype.undo = function(redo) {
var inputStack = redo ? this.redoStack_ : this.undoStack_;
var outputStack = redo ? this.undoStack_ : this.redoStack_;
var inputEvent = inputStack.pop();
if (!inputEvent) {
return;
}
var events = [inputEvent];
// Do another undo/redo if the next one is of the same group.
while (inputStack.length && inputEvent.group &&
inputEvent.group == inputStack[inputStack.length - 1].group) {
events.push(inputStack.pop());
}
// Push these popped events on the opposite stack.
for (var i = 0, event; event = events[i]; i++) {
outputStack.push(event);
}
events = Blockly.Events.filter(events, redo);
Blockly.Events.recordUndo = false;
if (Blockly.selected) {
Blockly.Events.disable();
try {
Blockly.selected.unselect();
} finally {
Blockly.Events.enable();
}
}
try {
for (var i = 0, event; event = events[i]; i++) {
event.run(redo);
}
} finally {
Blockly.Events.recordUndo = true;
}
};
/**
* Clear the undo/redo stacks.
*/
Blockly.Workspace.prototype.clearUndo = function() {
this.undoStack_.length = 0;
this.redoStack_.length = 0;
// Stop any events already in the firing queue from being undoable.
Blockly.Events.clearPendingUndo();
};
/**
* @return {boolean} whether there are any events in the redo stack.
* @package
*/
Blockly.Workspace.prototype.hasRedoStack = function() {
return this.redoStack_.length != 0;
};
/**
* @return {boolean} whether there are any events in the undo stack.
* @package
*/
Blockly.Workspace.prototype.hasUndoStack = function() {
return this.undoStack_.length != 0;
};
/**
* When something in this workspace changes, call a function.
* @param {!Function} func Function to call.
* @return {!Function} Function that can be passed to
* removeChangeListener.
*/
Blockly.Workspace.prototype.addChangeListener = function(func) {
this.listeners_.push(func);
return func;
};
/**
* Stop listening for this workspace's changes.
* @param {Function} func Function to stop calling.
*/
Blockly.Workspace.prototype.removeChangeListener = function(func) {
goog.array.remove(this.listeners_, func);
};
/**
* Fire a change event.
* @param {!Blockly.Events.Abstract} event Event to fire.
*/
Blockly.Workspace.prototype.fireChangeListener = function(event) {
if (event.recordUndo) {
this.undoStack_.push(event);
this.redoStack_.length = 0;
if (this.undoStack_.length > this.MAX_UNDO) {
this.undoStack_.unshift();
}
}
// Copy listeners in case a listener attaches/detaches itself.
var currentListeners = this.listeners_.slice();
for (var i = 0, func; func = currentListeners[i]; i++) {
func(event);
}
};
/**
* Find the block on this workspace with the specified ID.
* @param {string} id ID of block to find.
* @return {Blockly.Block} The sought after block or null if not found.
*/
Blockly.Workspace.prototype.getBlockById = function(id) {
var block = this.blockDB_[id];
if (!block && this.getFlyout() && this.getFlyout().getWorkspace()) {
block = this.getFlyout().getWorkspace().blockDB_[id];
}
return block || null;
};
/**
* Find the comment on this workspace with the specified ID.
* @param {string} id ID of comment to find.
* @return {Blockly.WorkspaceComment} The sought after comment or null if not
* found.
* @package
*/
Blockly.Workspace.prototype.getCommentById = function(id) {
return this.commentDB_[id] || null;
};
/**
* Getter for the flyout associated with this workspace. This is null in a
* non-rendered workspace, but may be overriden by subclasses.
* @return {Blockly.Flyout} The flyout on this workspace.
*/
Blockly.Workspace.prototype.getFlyout = function() {
return null;
};
/**
* Checks whether all value and statement inputs in the workspace are filled
* with blocks.
* @param {boolean=} opt_shadowBlocksAreFilled An optional argument controlling
* whether shadow blocks are counted as filled. Defaults to true.
* @return {boolean} True if all inputs are filled, false otherwise.
*/
Blockly.Workspace.prototype.allInputsFilled = function(opt_shadowBlocksAreFilled) {
var blocks = this.getTopBlocks(false);
for (var i = 0, block; block = blocks[i]; i++) {
if (!block.allInputsFilled(opt_shadowBlocksAreFilled)) {
return false;
}
}
return true;
};
/**
* Return the variable map that contains "potential" variables. These exist in
* the flyout but not in the workspace.
* @return {?Blockly.VariableMap} The potential variable map.
* @package
*/
Blockly.Workspace.prototype.getPotentialVariableMap = function() {
return this.potentialVariableMap_;
};
/**
* Create and store the potential variable map for this workspace.
* @package
*/
Blockly.Workspace.prototype.createPotentialVariableMap = function() {
this.potentialVariableMap_ = new Blockly.VariableMap(this);
};
/**
* Return the map of all variables on the workspace.
* @return {?Blockly.VariableMap} The variable map.
* @package
*/
Blockly.Workspace.prototype.getVariableMap = function() {
return this.variableMap_;
};
/**
* Database of all workspaces.
* @private
*/
Blockly.Workspace.WorkspaceDB_ = Object.create(null);
/**
* Find the workspace with the specified ID.
* @param {string} id ID of workspace to find.
* @return {Blockly.Workspace} The sought after workspace or null if not found.
*/
Blockly.Workspace.getById = function(id) {
return Blockly.Workspace.WorkspaceDB_[id] || null;
};
// Export symbols that would otherwise be renamed by Closure compiler.
Blockly.Workspace.prototype['clear'] = Blockly.Workspace.prototype.clear;
Blockly.Workspace.prototype['clearUndo'] =
Blockly.Workspace.prototype.clearUndo;
Blockly.Workspace.prototype['addChangeListener'] =
Blockly.Workspace.prototype.addChangeListener;
Blockly.Workspace.prototype['removeChangeListener'] =
Blockly.Workspace.prototype.removeChangeListener;

View File

@@ -0,0 +1,170 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Object in charge of loading, storing, and playing audio for a
* workspace.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.WorkspaceAudio');
goog.require('goog.userAgent');
/**
* Class for loading, storing, and playing audio for a workspace.
* @param {Blockly.WorkspaceSvg} parentWorkspace The parent of the workspace
* this audio object belongs to, or null.
* @constructor
*/
Blockly.WorkspaceAudio = function(parentWorkspace) {
/**
* The parent of the workspace this object belongs to, or null. May be
* checked for sounds that this object can't find.
* @type {Blockly.WorkspaceSvg}
* @private
*/
this.parentWorkspace_ = parentWorkspace;
/**
* Database of pre-loaded sounds.
* @private
* @const
*/
this.SOUNDS_ = Object.create(null);
};
/**
* Time that the last sound was played.
* @type {Date}
* @private
*/
Blockly.WorkspaceAudio.prototype.lastSound_ = null;
/**
* Dispose of this audio manager.
* @package
*/
Blockly.WorkspaceAudio.prototype.dispose = function() {
this.parentWorkspace_ = null;
this.SOUNDS_ = null;
};
/**
* Load an audio file. Cache it, ready for instantaneous playing.
* @param {!Array.<string>} filenames List of file types in decreasing order of
* preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav']
* Filenames include path from Blockly's root. File extensions matter.
* @param {string} name Name of sound.
* @package
*/
Blockly.WorkspaceAudio.prototype.load = function(filenames, name) {
if (!filenames.length) {
return;
}
try {
var audioTest = new window['Audio']();
} catch (e) {
// No browser support for Audio.
// IE can throw an error even if the Audio object exists.
return;
}
var sound;
for (var i = 0; i < filenames.length; i++) {
var filename = filenames[i];
var ext = filename.match(/\.(\w+)$/);
if (ext && audioTest.canPlayType('audio/' + ext[1])) {
// Found an audio format we can play.
sound = new window['Audio'](filename);
break;
}
}
if (sound && sound.play) {
this.SOUNDS_[name] = sound;
}
};
/**
* Preload all the audio files so that they play quickly when asked for.
* @package
*/
Blockly.WorkspaceAudio.prototype.preload = function() {
for (var name in this.SOUNDS_) {
var sound = this.SOUNDS_[name];
sound.volume = 0.01;
var playPromise = sound.play();
// Edge does not return a promise, so we need to check.
if (playPromise) {
// If we don't wait for the play request to complete before calling pause() we will get an exception:
// Uncaught (in promise) DOMException: The play() request was interrupted by a call to pause().
// See more: https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
playPromise.then(sound.pause).catch(function() {
// Play without user interaction was prevented.
});
} else {
sound.pause();
}
// iOS can only process one sound at a time. Trying to load more than one
// corrupts the earlier ones. Just load one and leave the others uncached.
if (goog.userAgent.IPAD || goog.userAgent.IPHONE) {
break;
}
}
};
/**
* Play a named sound at specified volume. If volume is not specified,
* use full volume (1).
* @param {string} name Name of sound.
* @param {number=} opt_volume Volume of sound (0-1).
*/
Blockly.WorkspaceAudio.prototype.play = function(name, opt_volume) {
var sound = this.SOUNDS_[name];
if (sound) {
// Don't play one sound on top of another.
var now = new Date;
if (this.lastSound_ != null &&
now - this.lastSound_ < Blockly.SOUND_LIMIT) {
return;
}
this.lastSound_ = now;
var mySound;
var ie9 = goog.userAgent.DOCUMENT_MODE &&
goog.userAgent.DOCUMENT_MODE === 9;
if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) {
// Creating a new audio node causes lag in IE9, Android and iPad. Android
// and IE9 refetch the file from the server, iPad uses a singleton audio
// node which must be deleted and recreated for each new audio tag.
mySound = sound;
} else {
mySound = sound.cloneNode();
}
mySound.volume = (opt_volume === undefined ? 1 : opt_volume);
mySound.play();
} else if (this.parentWorkspace_) {
// Maybe a workspace on a lower level knows about this sound.
this.parentWorkspace_.getAudioManager().play(name, opt_volume);
}
};

View File

@@ -0,0 +1,426 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Object representing a code comment on the workspace.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.WorkspaceComment');
goog.require('Blockly.Events.CommentChange');
goog.require('Blockly.Events.CommentCreate');
goog.require('Blockly.Events.CommentDelete');
goog.require('Blockly.Events.CommentMove');
goog.require('goog.math.Coordinate');
/**
* Class for a workspace comment.
* @param {!Blockly.Workspace} workspace The block's workspace.
* @param {string} content The content of this workspace comment.
* @param {number} height Height of the comment.
* @param {number} width Width of the comment.
* @param {boolean} minimized Whether this comment is in the minimized state
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
* create a new ID. If the ID conflicts with an in-use ID, a new one will
* be generated.
* @constructor
*/
Blockly.WorkspaceComment = function(workspace, content, height, width, minimized, opt_id) {
/** @type {string} */
this.id = (opt_id && !workspace.getCommentById(opt_id)) ?
opt_id : Blockly.utils.genUid();
workspace.addTopComment(this);
/**
* The comment's position in workspace units. (0, 0) is at the workspace's
* origin; scale does not change this value.
* @type {!goog.math.Coordinate}
* @protected
*/
this.xy_ = new goog.math.Coordinate(0, 0);
/**
* The comment's height in workspace units. Scale does not change this value.
* @type {number}
* @private
*/
this.height_ = height;
/**
* The comment's width in workspace units. Scale does not change this value.
* @type {number}
* @private
*/
this.width_ = width;
/**
* The comment's minimized state.
* @type{boolean}
* @private
*/
this.isMinimized_ = minimized;
/**
* @type {!Blockly.Workspace}
*/
this.workspace = workspace;
/**
* @protected
* @type {boolean}
*/
this.RTL = workspace.RTL;
/**
* @type {boolean}
* @private
*/
this.deletable_ = true;
/**
* @type {boolean}
* @private
*/
this.movable_ = true;
/**
* @protected
* @type {!string}
*/
this.content_ = content;
/**
* @package
* @type {boolean}
*/
this.isComment = true;
Blockly.WorkspaceComment.fireCreateEvent(this);
};
/**
* Maximum lable length (actual label length will include
* one additional character, the ellipsis).
* @private
*/
Blockly.WorkspaceComment.MAX_LABEL_LENGTH = 12;
/**
* Dispose of this comment.
* @package
*/
Blockly.WorkspaceComment.prototype.dispose = function() {
if (!this.workspace) {
// The comment has already been deleted.
return;
}
if (Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.CommentDelete(this));
}
// Remove from the list of top comments and the comment database.
this.workspace.removeTopComment(this);
this.workspace = null;
};
// Height, width, x, and y are all stored on even non-rendered comments, to
// preserve state if you pass the contents through a headless workspace.
/**
* Get comment height.
* @return {number} comment height.
* @package
*/
Blockly.WorkspaceComment.prototype.getHeight = function() {
return this.height_;
};
/**
* Set comment height.
* @param {number} height comment height.
* @package
*/
Blockly.WorkspaceComment.prototype.setHeight = function(height) {
this.height_ = height;
};
/**
* Get comment width.
* @return {number} comment width.
* @package
*/
Blockly.WorkspaceComment.prototype.getWidth = function() {
return this.width_;
};
/**
* Set comment width.
* @param {number} width comment width.
* @package
*/
Blockly.WorkspaceComment.prototype.setWidth = function(width) {
this.width_ = width;
};
/**
* Get the height and width of this comment.
* @return {{height: number, width: number}} The height and width of this comment;
* these numbers do not change as the workspace scales.
*/
Blockly.WorkspaceComment.prototype.getHeightWidth = function() {
return {height: this.height_, width: this.width_};
};
/**
* Get stored location.
* @return {!goog.math.Coordinate} The comment's stored location. This is not
* valid if the comment is currently being dragged.
* @package
*/
Blockly.WorkspaceComment.prototype.getXY = function() {
return this.xy_.clone();
};
/**
* Move a comment by a relative offset.
* @param {number} dx Horizontal offset, in workspace units.
* @param {number} dy Vertical offset, in workspace units.
* @package
*/
Blockly.WorkspaceComment.prototype.moveBy = function(dx, dy) {
var event = new Blockly.Events.CommentMove(this);
this.xy_.translate(dx, dy);
event.recordNew();
Blockly.Events.fire(event);
};
/**
* Get whether this comment is deletable or not.
* @return {boolean} True if deletable.
* @package
*/
Blockly.WorkspaceComment.prototype.isDeletable = function() {
return this.deletable_ &&
!(this.workspace && this.workspace.options.readOnly);
};
/**
* Set whether this comment is deletable or not.
* @param {boolean} deletable True if deletable.
* @package
*/
Blockly.WorkspaceComment.prototype.setDeletable = function(deletable) {
this.deletable_ = deletable;
};
/**
* Get whether this comment is movable or not.
* @return {boolean} True if movable.
* @package
*/
Blockly.WorkspaceComment.prototype.isMovable = function() {
return this.movable_ &&
!(this.workspace && this.workspace.options.readOnly);
};
/**
* Set whether this comment is movable or not.
* @param {boolean} movable True if movable.
* @package
*/
Blockly.WorkspaceComment.prototype.setMovable = function(movable) {
this.movable_ = movable;
};
/**
* Returns this comment's text.
* @return {string} Comment text.
* @package
*/
Blockly.WorkspaceComment.prototype.getText = function() {
return this.content_;
};
/**
* Set this comment's text content.
* @param {string} text Comment text.
* @package
*/
Blockly.WorkspaceComment.prototype.setText = function(text) {
if (this.content_ != text) {
Blockly.Events.fire(new Blockly.Events.CommentChange(
this, {text: this.content_}, {text: text}));
this.content_ = text;
}
};
/**
* Check whether this comment is currently minimized.
* @return {boolean} True if minimized
* @package
*/
Blockly.WorkspaceComment.prototype.isMinimized = function() {
return this.isMinimized_;
};
/**
* Encode a comment subtree as XML with XY coordinates.
* @param {boolean=} opt_noId True if the encoder should skip the comment id.
* @return {!Element} Tree of XML elements.
* @package
*/
Blockly.WorkspaceComment.prototype.toXmlWithXY = function(opt_noId) {
var element = this.toXml(opt_noId);
element.setAttribute('x', Math.round(this.xy_.x));
element.setAttribute('y', Math.round(this.xy_.y));
element.setAttribute('h', this.height_);
element.setAttribute('w', this.width_);
return element;
};
/**
* Get the truncated text for this comment to display in the minimized
* top bar.
* @return {string} The truncated comment text
* @package
*/
Blockly.WorkspaceComment.prototype.getLabelText = function() {
if (this.content_.length > Blockly.WorkspaceComment.MAX_LABEL_LENGTH) {
if (this.RTL) {
return '\u2026' + this.content_.slice(0, Blockly.WorkspaceComment.MAX_LABEL_LENGTH);
}
return this.content_.slice(0, Blockly.WorkspaceComment.MAX_LABEL_LENGTH) + '\u2026';
} else {
return this.content_;
}
};
/**
* Encode a comment subtree as XML, but don't serialize the XY coordinates or
* width and height. If you need that additional information use toXmlWithXY.
* @param {boolean=} opt_noId True if the encoder should skip the comment id.
* @return {!Element} Tree of XML elements.
* @package
*/
Blockly.WorkspaceComment.prototype.toXml = function(opt_noId) {
var commentElement = goog.dom.createDom('comment');
if (!opt_noId) {
commentElement.setAttribute('id', this.id);
}
if (this.isMinimized_) {
commentElement.setAttribute('minimized', true);
}
commentElement.textContent = this.getText();
return commentElement;
};
/**
* Fire a create event for the given workspace comment, if comments are enabled.
* @param {!Blockly.WorkspaceComment} comment The comment that was just created.
* @package
*/
Blockly.WorkspaceComment.fireCreateEvent = function(comment) {
if (Blockly.Events.isEnabled()) {
var existingGroup = Blockly.Events.getGroup();
if (!existingGroup) {
Blockly.Events.setGroup(true);
}
try {
Blockly.Events.fire(new Blockly.Events.CommentCreate(comment));
} finally {
if (!existingGroup) {
Blockly.Events.setGroup(false);
}
}
}
};
/**
* Decode an XML comment tag and create a comment on the workspace.
* @param {!Element} xmlComment XML comment element.
* @param {!Blockly.Workspace} workspace The workspace.
* @return {!Blockly.WorkspaceComment} The created workspace comment.
* @package
*/
Blockly.WorkspaceComment.fromXml = function(xmlComment, workspace) {
var info = Blockly.WorkspaceComment.parseAttributes(xmlComment);
var comment = new Blockly.WorkspaceComment(
workspace, info.content, info.h, info.w, info.minimized, info.id);
if (!isNaN(info.x) && !isNaN(info.y)) {
comment.moveBy(info.x, info.y);
}
Blockly.WorkspaceComment.fireCreateEvent(comment);
return comment;
};
/**
* Decode an XML comment tag and return the results in an object.
* @param {!Element} xml XML comment element.
* @return {!Object} An object containing the information about the comment.
* @package
*/
Blockly.WorkspaceComment.parseAttributes = function(xml) {
var xmlH = xml.getAttribute('h');
var xmlW = xml.getAttribute('w');
return {
/* @type {string} */
id: xml.getAttribute('id'),
/**
* The height of the comment in workspace units, or 100 if not specified.
* @type {number}
*/
h: xmlH ? parseInt(xmlH, 10) : 100,
/**
* The width of the comment in workspace units, or 100 if not specified.
* @type {number}
*/
w: xmlW ? parseInt(xmlW, 10) : 100,
/**
* The x position of the comment in workspace coordinates, or NaN if not
* specified in the XML.
* @type {number}
*/
x: parseInt(xml.getAttribute('x'), 10),
/**
* The y position of the comment in workspace coordinates, or NaN if not
* specified in the XML.
* @type {number}
*/
y: parseInt(xml.getAttribute('y'), 10),
/**
* Whether this comment is minimized. Defaults to false if not specified in
* the XML.
* @type {boolean}
*/
minimized: xml.getAttribute('minimized') == 'true' || false,
/* @type {string} */
content: xml.textContent
};
};

View File

@@ -0,0 +1,706 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Methods for rendering a workspace comment as SVG
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.WorkspaceCommentSvg.render');
goog.require('Blockly.WorkspaceCommentSvg');
/**
* Radius of the border around the comment.
* @type {number}
* @const
* @private
*/
Blockly.WorkspaceCommentSvg.BORDER_WIDTH = 1;
/**
* Size of the resize icon.
* @type {number}
* @const
* @private
*/
Blockly.WorkspaceCommentSvg.RESIZE_SIZE = 16;
/**
* Offset from the foreignobject edge to the textarea edge.
* @type {number}
* @const
* @private
*/
Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET = 12;
/**
* The height of the comment top bar.
* @package
*/
Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT = 32;
/**
* The size of the minimize arrow icon in the comment top bar.
* @private
*/
Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE = 32;
/**
* The size of the delete icon in the comment top bar.
* @private
*/
Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE = 32;
/**
* The inset for the top bar icons.
* @private
*/
Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET = 0;
/**
* The bottom corner padding of the resize handle touch target.
* Extends slightly outside the comment box.
* @private
*/
Blockly.WorkspaceCommentSvg.RESIZE_CORNER_PAD = 4;
/**
* The top/side padding around resize handle touch target.
* Extends about one extra "diagonal" above resize handle.
* @private
*/
Blockly.WorkspaceCommentSvg.RESIZE_OUTER_PAD = 8;
/**
* Width that a minimized comment should have.
* @private
*/
Blockly.WorkspaceCommentSvg.MINIMIZE_WIDTH = 200;
/**
* Returns a bounding box describing the dimensions of this comment.
* @return {!{height: number, width: number}} Object with height and width
* properties in workspace units.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getHeightWidth = function() {
return { width: this.getWidth(), height: this.getHeight() };
};
/**
* Renders the workspace comment.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.render = function() {
if (this.rendered_) {
return;
}
var size = this.getHeightWidth();
// Add text area
this.commentEditor_ = this.createEditor_();
this.svgGroup_.appendChild(this.commentEditor_);
this.createCommentTopBar_();
// Add the resize icon
this.addResizeDom_();
// Show / hide relevant things based on minimized state
if (this.isMinimized()) {
this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-up.svg');
this.commentEditor_.setAttribute('display', 'none');
this.resizeGroup_.setAttribute('display', 'none');
} else {
this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-down.svg');
this.topBarLabel_.setAttribute('display', 'none');
}
this.setSize(size.width, size.height);
// Set the content
this.textarea_.value = this.content_;
this.rendered_ = true;
if (this.resizeGroup_) {
Blockly.bindEventWithChecks_(
this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_);
Blockly.bindEventWithChecks_(
this.resizeGroup_, 'mouseup', this, this.resizeMouseUp_);
}
Blockly.bindEventWithChecks_(
this.minimizeArrow_, 'mousedown', this, this.minimizeArrowMouseDown_, true);
Blockly.bindEventWithChecks_(
this.minimizeArrow_, 'mouseout', this, this.minimizeArrowMouseOut_, true);
Blockly.bindEventWithChecks_(
this.minimizeArrow_, 'mouseup', this, this.minimizeArrowMouseUp_, true);
Blockly.bindEventWithChecks_(
this.deleteIcon_, 'mousedown', this, this.deleteMouseDown_, true);
Blockly.bindEventWithChecks_(
this.deleteIcon_, 'mouseout', this, this.deleteMouseOut_, true);
Blockly.bindEventWithChecks_(
this.deleteIcon_, 'mouseup', this, this.deleteMouseUp_, true);
};
/**
* Create the text area for the comment.
* @return {!Element} The top-level node of the editor.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.createEditor_ = function() {
this.foreignObject_ = Blockly.utils.createSvgElement(
'foreignObject',
{
'x': Blockly.WorkspaceCommentSvg.BORDER_WIDTH,
'y': Blockly.WorkspaceCommentSvg.BORDER_WIDTH + Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT,
'class': 'scratchCommentForeignObject'
},
null);
var body = document.createElementNS(Blockly.HTML_NS, 'body');
body.setAttribute('xmlns', Blockly.HTML_NS);
body.className = 'blocklyMinimalBody scratchCommentBody';
var textarea = document.createElementNS(Blockly.HTML_NS, 'textarea');
textarea.className = 'scratchCommentTextarea scratchCommentText';
textarea.setAttribute('dir', this.RTL ? 'RTL' : 'LTR');
textarea.setAttribute('placeholder', Blockly.Msg.WORKSPACE_COMMENT_DEFAULT_TEXT);
body.appendChild(textarea);
this.textarea_ = textarea;
this.textarea_.style.margin = (Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET) + 'px';
this.foreignObject_.appendChild(body);
Blockly.bindEventWithChecks_(textarea, 'mousedown', this, function(e) {
e.stopPropagation(); // Propagation causes preventDefault from workspace handler
}, true, true);
// Don't zoom with mousewheel.
Blockly.bindEventWithChecks_(textarea, 'wheel', this, function(e) {
if (!e.ctrlKey && textarea.clientHeight !== textarea.scrollHeight) {
e.stopPropagation();
}
});
Blockly.bindEventWithChecks_(textarea, 'change', this, function(_e) {
if (this.text_ != textarea.value) {
this.setText(textarea.value);
}
});
this.labelText_ = this.getLabelText();
return this.foreignObject_;
};
/**
* Add the resize icon to the DOM
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.addResizeDom_ = function() {
this.resizeGroup_ = Blockly.utils.createSvgElement(
'g',
{
'class': this.RTL ? 'scratchCommentResizeSW' : 'scratchCommentResizeSE'
},
this.svgGroup_);
var resizeSize = Blockly.WorkspaceCommentSvg.RESIZE_SIZE;
var outerPad = Blockly.ScratchBubble.RESIZE_OUTER_PAD;
var cornerPad = Blockly.ScratchBubble.RESIZE_CORNER_PAD;
// Build an (invisible) triangle that will catch resizes. It is padded on the
// top/left by outerPad, and padded down/right by cornerPad.
Blockly.utils.createSvgElement('polygon',
{
'points': [
-outerPad, resizeSize + cornerPad,
resizeSize + cornerPad, resizeSize + cornerPad,
resizeSize + cornerPad, -outerPad
].join(' ')
},
this.resizeGroup_);
Blockly.utils.createSvgElement(
'line',
{
'class': 'blocklyResizeLine',
'x1': resizeSize / 3, 'y1': resizeSize - 1,
'x2': resizeSize - 1, 'y2': resizeSize / 3
}, this.resizeGroup_);
Blockly.utils.createSvgElement(
'line',
{
'class': 'blocklyResizeLine',
'x1': resizeSize * 2 / 3, 'y1': resizeSize - 1,
'x2': resizeSize - 1, 'y2': resizeSize * 2 / 3
}, this.resizeGroup_);
};
/**
* Create the comment top bar and its contents.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.createCommentTopBar_ = function() {
this.svgHandleTarget_ = Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyDraggable scratchCommentTopBar',
'rx': Blockly.WorkspaceCommentSvg.BORDER_WIDTH,
'ry': Blockly.WorkspaceCommentSvg.BORDER_WIDTH,
'height': Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT
}, this.svgGroup_);
this.createTopBarIcons_();
this.createTopBarLabel_();
};
/**
* Create the comment top bar label. This is the truncated comment text
* that shows when comment is minimized.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.createTopBarLabel_ = function() {
this.topBarLabel_ = Blockly.utils.createSvgElement('text',
{
'class': 'scratchCommentText',
'x': this.width_ / 2,
'y': (Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT / 2) + Blockly.WorkspaceCommentSvg.BORDER_WIDTH,
'text-anchor': 'middle',
'dominant-baseline': 'middle'
}, this.svgGroup_);
var labelTextNode = document.createTextNode(this.labelText_);
this.topBarLabel_.appendChild(labelTextNode);
};
/**
* Create the minimize toggle and delete icons that in the comment top bar.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.createTopBarIcons_ = function() {
var topBarMiddleY = (Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT / 2) +
Blockly.WorkspaceCommentSvg.BORDER_WIDTH;
// Minimize Toggle Icon in Comment Top Bar
var xInset = Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET;
this.minimizeArrow_ = Blockly.utils.createSvgElement('image',
{
'x': xInset,
'y': topBarMiddleY - Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE / 2,
'width': Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE,
'height': Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE
}, this.svgGroup_);
// Delete Icon in Comment Top Bar
this.deleteIcon_ = Blockly.utils.createSvgElement('image',
{
'x': xInset,
'y': topBarMiddleY - Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE / 2,
'width': Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE,
'height': Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE
}, this.svgGroup_);
this.deleteIcon_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'delete-x.svg');
};
/**
* Handle a mouse-down on bubble's minimize icon.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.minimizeArrowMouseDown_ = function(e) {
// Set a property to indicate that this minimize arrow icon had a mouse down
// event. This property will get reset if the mouse leaves the icon, or when
// a mouse up event occurs on this icon.
this.shouldToggleMinimize_ = true;
e.stopPropagation();
};
/**
* Handle a mouse-out on bubble's minimize icon.
* @param {!Event} _e Mouse out event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.minimizeArrowMouseOut_ = function(_e) {
// If the mouse leaves the minimize arrow icon, make sure the
// shouldToggleMinimize_ property gets reset.
this.shouldToggleMinimize_ = false;
};
/**
* Handle a mouse-up on bubble's minimize icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.minimizeArrowMouseUp_ = function(e) {
// First check if this is the icon that had a mouse down event on it and that
// the mouse never left the icon.
if (this.shouldToggleMinimize_) {
this.shouldToggleMinimize = false;
this.toggleMinimize_();
}
e.stopPropagation();
};
/**
* Handle a mouse-down on bubble's minimize icon.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.deleteMouseDown_ = function(e) {
// Set a property to indicate that this delete icon had a mouse down event.
// This property will get reset if the mouse leaves the icon, or when
// a mouse up event occurs on this icon.
this.shouldDelete_ = true;
e.stopPropagation();
};
/**
* Handle a mouse-out on bubble's minimize icon.
* @param {!Event} _e Mouse out event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.deleteMouseOut_ = function(_e) {
// If the mouse leaves the delete icon, reset the shouldDelete_ property.
this.shouldDelete_ = false;
};
/**
* Handle a mouse-up on bubble's delete icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.deleteMouseUp_ = function(e) {
// First check that this same icon had a mouse down event on it and that the
// mouse never left the icon.
if (this.shouldDelete_) {
this.dispose();
}
e.stopPropagation();
};
/**
* Handle a mouse-down on comment's resize corner.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.resizeMouseDown_ = function(e) {
this.resizeStartSize_ = {width: this.width_, height: this.height_};
this.unbindDragEvents_();
this.workspace.setResizesEnabled(false);
if (Blockly.utils.isRightButton(e)) {
// No right-click.
e.stopPropagation();
return;
}
// Left-click (or middle click)
this.workspace.startDrag(e, new goog.math.Coordinate(
this.workspace.RTL ? -this.width_ : this.width_, this.height_));
this.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(
document, 'mouseup', this, this.resizeMouseUp_);
this.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(
document, 'mousemove', this, this.resizeMouseMove_);
Blockly.hideChaff();
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
};
/**
* Set the apperance of the workspace comment bubble to the minimized or full size
* appearance. In the minimized state, the comment should only have the top bar
* displayed, with the minimize icon swapped to the minimized state, and
* truncated comment text is shown in the middle of the top bar. There should be
* no resize handle when the workspace comment is in its minimized state.
* @param {boolean} minimize Whether the bubble should be minimized
* @param {?string} labelText Optional label text for the comment top bar
* when it is minimized.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.setRenderedMinimizeState_ = function(minimize, labelText) {
if (minimize) {
// Change minimize icon
this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-up.svg');
// Hide text area
this.commentEditor_.setAttribute('display', 'none');
// Hide resize handle if it exists
if (this.resizeGroup_) {
this.resizeGroup_.setAttribute('display', 'none');
}
if (labelText && this.labelText_ != labelText) {
// Update label and display
// TODO is there a better way to do this?
this.topBarLabel_.textContent = labelText;
}
Blockly.utils.removeAttribute(this.topBarLabel_, 'display');
} else {
// Change minimize icon
this.minimizeArrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'comment-arrow-down.svg');
// Hide label
this.topBarLabel_.setAttribute('display', 'none');
// Show text area
Blockly.utils.removeAttribute(this.commentEditor_, 'display');
// Display resize handle if it exists
if (this.resizeGroup_) {
Blockly.utils.removeAttribute(this.resizeGroup_, 'display');
}
}
};
/**
* Stop binding to the global mouseup and mousemove events.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.unbindDragEvents_ = function() {
if (this.onMouseUpWrapper_) {
Blockly.unbindEvent_(this.onMouseUpWrapper_);
this.onMouseUpWrapper_ = null;
}
if (this.onMouseMoveWrapper_) {
Blockly.unbindEvent_(this.onMouseMoveWrapper_);
this.onMouseMoveWrapper_ = null;
}
};
/*
* Handle a mouse-up event while dragging a comment's border or resize handle.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.resizeMouseUp_ = function(/*e*/) {
Blockly.Touch.clearTouchIdentifier();
this.unbindDragEvents_();
var oldHW = this.resizeStartSize_;
this.resizeStartSize_ = null;
if (this.width_ == oldHW.width && this.height_ == oldHW.height) {
return;
}
// Fire a change event for the new width/height after
// resize mouse up
Blockly.Events.fire(new Blockly.Events.CommentChange(
this, {width: oldHW.width , height: oldHW.height},
{width: this.width_, height: this.height_}));
this.workspace.setResizesEnabled(true);
};
/**
* Resize this comment to follow the mouse.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.resizeMouseMove_ = function(e) {
this.autoLayout_ = false;
var newXY = this.workspace.moveDrag(e);
// The call to setSize below emits a CommentChange event,
// but we don't want multiple CommentChange events to be
// emitted while the user is still in the process of resizing
// the comment, so disable events here. The event is emitted in
// resizeMouseUp_.
var disabled = false;
if (Blockly.Events.isEnabled()) {
Blockly.Events.disable();
disabled = true;
}
this.setSize(this.RTL ? -newXY.x : newXY.x, newXY.y);
if (disabled) {
Blockly.Events.enable();
}
};
/**
* Callback function triggered when the comment has resized.
* Resize the text area accordingly.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.resizeComment_ = function() {
var doubleBorderWidth = 2 * Blockly.WorkspaceCommentSvg.BORDER_WIDTH;
var topOffset = Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT;
var textOffset = Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET * 2;
this.foreignObject_.setAttribute('width',
this.width_ - doubleBorderWidth);
this.foreignObject_.setAttribute('height',
this.height_ - doubleBorderWidth - topOffset);
if (this.RTL) {
this.foreignObject_.setAttribute('x',
-this.width_);
}
this.textarea_.style.width =
(this.width_ - textOffset) + 'px';
this.textarea_.style.height =
(this.height_ - doubleBorderWidth - textOffset - topOffset) + 'px';
};
/**
* Set size
* @param {number} width width of the container
* @param {number} height height of the container
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setSize = function(width, height) {
var oldWidth = this.width_;
var oldHeight = this.height_;
var doubleBorderWidth = 2 * Blockly.WorkspaceCommentSvg.BORDER_WIDTH;
if (this.isMinimized_) {
width = Blockly.WorkspaceCommentSvg.MINIMIZE_WIDTH;
height = Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT;
} else {
// Minimum size of a 'full size' (not minimized) comment.
width = Math.max(width, doubleBorderWidth + 50);
height = Math.max(height, doubleBorderWidth + 20 + Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT);
// Note we are only updating this.width_ or this.height_ here
// and not in the case above, because when we're minimizing a comment,
// we want to keep track of the width/height of the maximized comment
this.width_ = width;
this.height_ = height;
Blockly.Events.fire(new Blockly.Events.CommentChange(this,
{width: oldWidth, height: oldHeight},
{width: this.width_, height: this.height_}));
}
this.svgRect_.setAttribute('width', width);
this.svgRect_.setAttribute('height', height);
this.svgHandleTarget_.setAttribute('width', width);
this.svgHandleTarget_.setAttribute('height', Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT);
if (this.RTL) {
this.minimizeArrow_.setAttribute('x', width -
(Blockly.WorkspaceCommentSvg.MINIMIZE_ICON_SIZE) -
Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET);
this.deleteIcon_.setAttribute('x', (-width +
Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET));
this.svgRect_.setAttribute('transform', 'scale(-1 1)');
this.svgHandleTarget_.setAttribute('transform', 'scale(-1 1)');
this.svgHandleTarget_.setAttribute('transform', 'translate(' + -width + ', 1)');
this.minimizeArrow_.setAttribute('transform', 'translate(' + -width + ', 1)');
this.deleteIcon_.setAttribute('tranform', 'translate(' + -width + ', 1)');
this.topBarLabel_.setAttribute('transform', 'translate(' + -width + ', 1)');
} else {
this.deleteIcon_.setAttribute('x', width -
Blockly.WorkspaceCommentSvg.DELETE_ICON_SIZE -
Blockly.WorkspaceCommentSvg.TOP_BAR_ICON_INSET);
}
var resizeSize = Blockly.WorkspaceCommentSvg.RESIZE_SIZE;
if (this.resizeGroup_) {
if (this.RTL) {
// Mirror the resize group.
this.resizeGroup_.setAttribute('transform', 'translate(' +
(-width + doubleBorderWidth + resizeSize) + ',' +
(height - doubleBorderWidth - resizeSize) + ') scale(-1 1)');
} else {
this.resizeGroup_.setAttribute('transform', 'translate(' +
(width - doubleBorderWidth - resizeSize) + ',' +
(height - doubleBorderWidth - resizeSize) + ')');
}
}
if (this.isMinimized_) {
this.topBarLabel_.setAttribute('x', width / 2);
this.topBarLabel_.setAttribute('y', height / 2);
}
// Allow the contents to resize.
this.resizeComment_();
};
/**
* Toggle the minimization state of this comment.
* @private
*/
Blockly.WorkspaceComment.prototype.toggleMinimize_ = function() {
this.setMinimized(!this.isMinimized_);
};
/**
* Set the minimized state for this comment. If the comment is rendered,
* change the appearance of the comment accordingly.
* @param {boolean} minimize Whether the comment should be minimized
* @package
*/
Blockly.WorkspaceComment.prototype.setMinimized = function(minimize) {
if (this.isMinimized_ == minimize) {
return;
}
Blockly.Events.fire(new Blockly.Events.CommentChange(this,
{minimized: this.isMinimized_}, {minimized: minimize}));
this.isMinimized_ = minimize;
if (minimize) {
if (this.rendered_) {
this.setRenderedMinimizeState_(true, this.getLabelText());
}
this.setSize(Blockly.WorkspaceCommentSvg.MINIMIZE_WIDTH,
Blockly.WorkspaceCommentSvg.TOP_BAR_HEIGHT);
} else {
if (this.rendered_) {
this.setRenderedMinimizeState_(false);
}
this.setText(this.content_);
this.setSize(this.width_, this.height_);
}
};
/**
* Dispose of any rendered comment components.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.disposeInternal_ = function() {
this.textarea_ = null;
this.foreignObject_ = null;
this.svgRect_ = null;
this.svgHandleTarget_ = null;
};
/**
* Set the focus on the text area.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setFocus = function() {
var comment = this;
this.focused_ = true;
comment.textarea_.focus();
// Defer CSS changes.
setTimeout(function() {
comment.addFocus();
Blockly.utils.addClass(
comment.svgHandleTarget_, 'scratchCommentHandleTargetFocused');
}, 0);
};
/**
* Remove focus from the text area.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.blurFocus = function() {
var comment = this;
this.focused_ = false;
comment.textarea_.blur();
// Defer CSS changes.
setTimeout(function() {
if (comment.svgGroup_) { // Could have been deleted in the meantime
comment.removeFocus();
Blockly.utils.removeClass(
comment.svgHandleTarget_, 'scratchCommentHandleTargetFocused');
}
}, 0);
};

View File

@@ -0,0 +1,609 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Object representing a code comment on a rendered workspace.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.WorkspaceCommentSvg');
goog.require('Blockly.Events.CommentCreate');
goog.require('Blockly.Events.CommentDelete');
goog.require('Blockly.Events.CommentMove');
goog.require('Blockly.WorkspaceComment');
/**
* Class for a workspace comment's SVG representation.
* @param {!Blockly.Workspace} workspace The block's workspace.
* @param {string} content The content of this workspace comment.
* @param {number} height Height of the comment.
* @param {number} width Width of the comment.
* @param {boolean} minimized Whether this comment is minimized.
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
* create a new ID.
* @extends {Blockly.WorkspaceComment}
* @constructor
*/
Blockly.WorkspaceCommentSvg = function(workspace, content, height, width, minimized,
opt_id) {
// Create core elements for the block.
/**
* @type {SVGElement}
* @private
*/
this.svgGroup_ = Blockly.utils.createSvgElement(
'g', {}, null);
this.svgGroup_.translate_ = '';
this.svgRect_ = Blockly.utils.createSvgElement(
'rect',
{
'class': 'scratchCommentRect scratchWorkspaceCommentBorder',
'x': 0,
'y': 0,
'rx': 4 * Blockly.WorkspaceCommentSvg.BORDER_WIDTH,
'ry': 4 * Blockly.WorkspaceCommentSvg.BORDER_WIDTH
});
this.svgGroup_.appendChild(this.svgRect_);
/**
* Whether the comment is rendered onscreen and is a part of the DOM.
* @type {boolean}
* @private
*/
this.rendered_ = false;
/**
* 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.
* @type {boolean}
* @private
*/
this.useDragSurface_ =
Blockly.utils.is3dSupported() && !!workspace.blockDragSurface_;
Blockly.WorkspaceCommentSvg.superClass_.constructor.call(this,
workspace, content, height, width, minimized, opt_id);
this.render();
}; goog.inherits(Blockly.WorkspaceCommentSvg, Blockly.WorkspaceComment);
/**
* The width and height to use to size a workspace comment when it is first
* added, before it has been edited by the user.
* @type {number}
* @package
*/
Blockly.WorkspaceCommentSvg.DEFAULT_SIZE = 200;
/**
* Dispose of this comment.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.dispose = function() {
if (!this.workspace) {
// The comment has already been deleted.
return;
}
// If this comment is being deleted, unlink the mouse events.
if (Blockly.selected == this) {
this.unselect();
this.workspace.cancelCurrentGesture();
}
if (Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.CommentDelete(this));
}
goog.dom.removeNode(this.svgGroup_);
// Sever JavaScript to DOM connections.
this.svgGroup_ = null;
this.svgRect_ = null;
// Dispose of any rendered components
this.disposeInternal_();
Blockly.Events.disable();
Blockly.WorkspaceCommentSvg.superClass_.dispose.call(this);
Blockly.Events.enable();
};
/**
* Create and initialize the SVG representation of a workspace comment.
* May be called more than once.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.initSvg = function() {
goog.asserts.assert(this.workspace.rendered, 'Workspace is headless.');
if (!this.workspace.options.readOnly && !this.eventsInit_) {
Blockly.bindEventWithChecks_(
this.svgHandleTarget_, 'mousedown', this, this.pathMouseDown_);
}
this.eventsInit_ = true;
this.updateMovable();
if (!this.getSvgRoot().parentNode) {
this.workspace.getBubbleCanvas().appendChild(this.getSvgRoot());
}
};
/**
* Handle a mouse-down on an SVG comment.
* @param {!Event} e Mouse down event or touch start event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.pathMouseDown_ = function(e) {
var gesture = this.workspace.getGesture(e);
if (gesture) {
gesture.handleBubbleStart(e, this);
}
};
/**
* Show the context menu for this workspace comment.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.showContextMenu_ = function(e) {
if (this.workspace.options.readOnly) {
return;
}
// Save the current workspace comment in a variable for use in closures.
var comment = this;
var menuOptions = [];
if (this.isDeletable() && this.isMovable()) {
menuOptions.push(Blockly.ContextMenu.commentDuplicateOption(comment));
menuOptions.push(Blockly.ContextMenu.commentDeleteOption(comment));
}
Blockly.ContextMenu.show(e, menuOptions, this.RTL);
};
/**
* Select this comment. Highlight it visually.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.select = function() {
if (Blockly.selected == this) {
return;
}
var oldId = null;
if (Blockly.selected) {
oldId = Blockly.selected.id;
// Unselect any previously selected block or comment.
Blockly.Events.disable();
try {
Blockly.selected.unselect();
} finally {
Blockly.Events.enable();
}
}
var event = new Blockly.Events.Ui(null, 'selected', oldId, this.id);
event.workspaceId = this.workspace.id;
Blockly.Events.fire(event);
Blockly.selected = this;
this.addSelect();
};
/**
* Unselect this comment. Remove its highlighting.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.unselect = function() {
if (Blockly.selected != this) {
return;
}
var event = new Blockly.Events.Ui(null, 'selected', this.id, null);
event.workspaceId = this.workspace.id;
Blockly.Events.fire(event);
Blockly.selected = null;
this.removeSelect();
};
/**
* Select this comment. Highlight it visually.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.addSelect = function() {
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklySelected');
this.setFocus();
};
/**
* Unselect this comment. Remove its highlighting.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.removeSelect = function() {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklySelected');
this.blurFocus();
};
/**
* Focus this comment. Highlight it visually.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.addFocus = function() {
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyFocused');
};
/**
* Unfocus this comment. Remove its highlighting.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.removeFocus = function() {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyFocused');
};
/**
* Return the coordinates of the top-left corner of this comment relative to the
* drawing surface's origin (0,0), in workspace units.
* If the comment is on the workspace, (0, 0) is the origin of the workspace
* coordinate system.
* This does not change with workspace scale.
* @return {!goog.math.Coordinate} Object with .x and .y properties in
* workspace coordinates.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getRelativeToSurfaceXY = function() {
var x = 0;
var y = 0;
var dragSurfaceGroup = this.useDragSurface_ ?
this.workspace.blockDragSurface_.getGroup() : null;
var element = this.getSvgRoot();
if (element) {
do {
// Loop through this comment and every parent.
var xy = Blockly.utils.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.blockDragSurface_.getCurrentBlock() == element) {
var surfaceTranslation =
this.workspace.blockDragSurface_.getSurfaceTranslation();
x += surfaceTranslation.x;
y += surfaceTranslation.y;
}
element = element.parentNode;
} while (element && element != this.workspace.getBubbleCanvas() &&
element != dragSurfaceGroup);
}
this.xy_ = new goog.math.Coordinate(x, y);
return this.xy_;
};
/**
* Move a comment by a relative offset.
* @param {number} dx Horizontal offset, in workspace units.
* @param {number} dy Vertical offset, in workspace units.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.moveBy = function(dx, dy) {
var event = new Blockly.Events.CommentMove(this);
// TODO: Do I need to look up the relative to surface XY position here?
var xy = this.getRelativeToSurfaceXY();
this.translate(xy.x + dx, xy.y + dy);
event.recordNew();
Blockly.Events.fire(event);
this.workspace.resizeContents();
};
/**
* Transforms a comment by setting the translation on the transform attribute
* of the block's SVG.
* @param {number} x The x coordinate of the translation in workspace units.
* @param {number} y The y coordinate of the translation in workspace units.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.translate = function(x, y) {
this.xy_ = new goog.math.Coordinate(x, y);
this.getSvgRoot().setAttribute('transform',
'translate(' + x + ',' + y + ')');
};
/**
* 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.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.moveToDragSurface_ = function() {
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.
var xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.workspace.blockDragSurface_.translateSurface(xy.x, xy.y);
// Execute the move on the top-level SVG component
this.workspace.blockDragSurface_.setBlocksAndShow(this.getSvgRoot());
};
/**
* Move this comment back to the workspace block canvas.
* Generally should be called at the same time as setDragging(false).
* Does nothing if useDragSurface_ is false.
* @param {!goog.math.Coordinate} newXY The position the comment should take on
* on the workspace canvas, in workspace coordinates.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.moveOffDragSurface_ = function(newXY) {
if (!this.useDragSurface_) {
return;
}
// Translate to current position, turning off 3d.
this.translate(newXY.x, newXY.y);
this.workspace.blockDragSurface_.clearAndHide(this.workspace.getCanvas());
};
/**
* Move this comment during a drag, taking into account whether we are using a
* drag surface to translate blocks.
* @param {?Blockly.BlockDragSurfaceSvg} dragSurface The surface that carries
* rendered items during a drag, or null if no drag surface is in use.
* @param {!goog.math.Coordinate} newLoc The location to translate to, in
* workspace coordinates.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.moveDuringDrag = function(dragSurface, newLoc) {
if (dragSurface) {
dragSurface.translateSurface(newLoc.x, newLoc.y);
} else {
this.svgGroup_.translate_ = 'translate(' + newLoc.x + ',' + newLoc.y + ')';
this.svgGroup_.setAttribute('transform',
this.svgGroup_.translate_ + this.svgGroup_.skew_);
}
};
/**
* Move the bubble group to the specified location in workspace coordinates.
* @param {number} x The x position to move to.
* @param {number} y The y position to move to.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.moveTo = function(x, y) {
this.translate(x, y);
};
/**
* Clear the comment of transform="..." attributes.
* Used when the comment is switching from 3d to 2d transform or vice versa.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.clearTransformAttributes_ = function() {
Blockly.utils.removeAttribute(this.getSvgRoot(), 'transform');
};
/**
* Return the rendered size of the comment or the stored size if the comment is
* not rendered. This differs from getHeightWidth in the behavior of rendered
* minimized comments. This function reports the actual size of the minimized
* comment instead of the full sized comment height/width.
* @return {!{height: number, width: number}} Object with height and width
* properties in workspace units.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getBubbleSize = function() {
if (this.rendered_) {
return {
width: parseInt(this.svgRect_.getAttribute('width')),
height: parseInt(this.svgRect_.getAttribute('height'))
};
} else {
this.getHeightWidth();
}
};
/**
* Returns the coordinates of a bounding box describing the dimensions of this
* comment.
* Coordinate system: workspace coordinates.
* @return {!{topLeft: goog.math.Coordinate, bottomRight: goog.math.Coordinate}}
* Object with top left and bottom right coordinates of the bounding box.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getBoundingRectangle = function() {
var blockXY = this.getRelativeToSurfaceXY();
var commentBounds = this.getHeightWidth();
var topLeft;
var bottomRight;
if (this.RTL) {
topLeft = new goog.math.Coordinate(blockXY.x - (commentBounds.width),
blockXY.y);
// Add the width of the tab/puzzle piece knob to the x coordinate
// since X is the corner of the rectangle, not the whole puzzle piece.
bottomRight = new goog.math.Coordinate(blockXY.x,
blockXY.y + commentBounds.height);
} else {
// Subtract the width of the tab/puzzle piece knob to the x coordinate
// since X is the corner of the rectangle, not the whole puzzle piece.
topLeft = new goog.math.Coordinate(blockXY.x, blockXY.y);
bottomRight = new goog.math.Coordinate(blockXY.x + commentBounds.width,
blockXY.y + commentBounds.height);
}
return {topLeft: topLeft, bottomRight: bottomRight};
};
/**
* Add or remove the UI indicating if this comment is movable or not.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.updateMovable = function() {
if (this.isMovable()) {
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable');
} else {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable');
}
};
/**
* Set whether this comment is movable or not.
* @param {boolean} movable True if movable.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setMovable = function(movable) {
Blockly.WorkspaceCommentSvg.superClass_.setMovable.call(this, movable);
this.updateMovable();
};
/**
* Recursively adds or removes the dragging class to this node and its children.
* @param {boolean} adding True if adding, false if removing.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setDragging = function(adding) {
if (adding) {
var group = this.getSvgRoot();
group.translate_ = '';
group.skew_ = '';
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDragging');
} else {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDragging');
}
};
/**
* Return the root node of the SVG or null if none exists.
* @return {Element} The root SVG node (probably a group).
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getSvgRoot = function() {
return this.svgGroup_;
};
/**
* Returns this comment's text.
* @return {string} Comment text.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getText = function() {
return this.textarea_ ? this.textarea_.value : this.content_;
};
/**
* Set this comment's text.
* @param {string} text Comment text.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setText = function(text) {
Blockly.WorkspaceCommentSvg.superClass_.setText.call(this, text);
if (this.textarea_) {
this.textarea_.value = text;
}
};
/**
* Update the cursor over this comment by adding or removing a class.
* @param {boolean} enable True if the delete cursor should be shown, false
* otherwise.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setDeleteStyle = function(enable) {
if (enable) {
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggingDelete');
} else {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggingDelete');
}
};
Blockly.WorkspaceCommentSvg.prototype.setAutoLayout = function() {
// NOP for compatibility with the bubble dragger.
};
/**
* Decode an XML comment tag and create a rendered comment on the workspace.
* @param {!Element} xmlComment XML comment element.
* @param {!Blockly.Workspace} workspace The workspace.
* @param {number=} opt_wsWidth The width of the workspace, which is used to
* position comments correctly in RTL.
* @return {!Blockly.WorkspaceCommentSvg} The created workspace comment.
* @package
*/
Blockly.WorkspaceCommentSvg.fromXml = function(xmlComment, workspace,
opt_wsWidth) {
Blockly.Events.disable();
try {
var info = Blockly.WorkspaceComment.parseAttributes(xmlComment);
var comment = new Blockly.WorkspaceCommentSvg(workspace,
info.content, info.h, info.w, info.minimized, info.id);
if (workspace.rendered) {
comment.initSvg();
comment.render(false);
}
// Position the comment correctly, taking into account the width of a
// rendered RTL workspace.
if (!isNaN(info.x) && !isNaN(info.y)) {
if (workspace.RTL) {
var wsWidth = opt_wsWidth || workspace.getWidth();
comment.moveBy(wsWidth - info.x, info.y);
} else {
comment.moveBy(info.x, info.y);
}
}
} finally {
Blockly.Events.enable();
}
Blockly.WorkspaceComment.fireCreateEvent(comment);
return comment;
};
/**
* Encode a comment subtree as XML with XY coordinates.
* @param {boolean=} opt_noId True if the encoder should skip the comment id.
* @return {!Element} Tree of XML elements.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.toXmlWithXY = function(opt_noId) {
var width; // Not used in LTR.
if (this.workspace.RTL) {
// Here be performance dragons: This calls getMetrics().
width = this.workspace.getWidth();
}
var element = this.toXml(opt_noId);
var xy = this.getRelativeToSurfaceXY();
element.setAttribute('x',
Math.round(this.workspace.RTL ? width - xy.x : xy.x));
element.setAttribute('y', Math.round(xy.y));
element.setAttribute('h', this.getHeight());
element.setAttribute('w', this.getWidth());
return element;
};

View File

@@ -0,0 +1,195 @@
/**
* @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.
* <svg class="blocklyWsDragSurface" style=transform:translate3d(...)>
* <g class="blocklyBlockCanvas"></g>
* <g class="blocklyBubbleCanvas">/g>
* </svg>
*/
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 blocklyOverflowVisible'
}, 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.
var fixedX = x.toFixed(0);
var fixedY = y.toFixed(0);
this.SVG_.style.display = 'block';
Blockly.utils.setCssTransform(
this.SVG_, 'translate3d(' + fixedX + 'px, ' + fixedY + 'px, 0px)');
};
/**
* 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) {
if (!newSurface) {
throw 'Couldn\'t clear and hide the drag surface: missing new surface.';
}
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.');
Blockly.utils.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 {!Element} blockCanvas The block canvas <g> element from the workspace.
* @param {!Element} bubbleCanvas The <g> 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';
};

View File

@@ -0,0 +1,132 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Methods for dragging a workspace visually.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.WorkspaceDragger');
goog.require('goog.math.Coordinate');
goog.require('goog.asserts');
/**
* 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.
* @param {!Blockly.WorkspaceSvg} workspace The workspace to drag.
* @constructor
*/
Blockly.WorkspaceDragger = function(workspace) {
/**
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* The workspace's metrics object at the beginning of the drag. Contains size
* and position metrics of a workspace.
* Coordinate system: pixel coordinates.
* @type {!Object}
* @private
*/
this.startDragMetrics_ = workspace.getMetrics();
/**
* The scroll position of the workspace at the beginning of the drag.
* Coordinate system: pixel coordinates.
* @type {!goog.math.Coordinate}
* @private
*/
this.startScrollXY_ = new goog.math.Coordinate(
workspace.scrollX, workspace.scrollY);
};
/**
* Sever all links from this object.
* @package
*/
Blockly.WorkspaceDragger.prototype.dispose = function() {
this.workspace_ = null;
};
/**
* Start dragging the workspace.
* @package
*/
Blockly.WorkspaceDragger.prototype.startDrag = function() {
if (Blockly.selected) {
Blockly.selected.unselect();
}
this.workspace_.setupDragSurface();
};
/**
* Finish dragging the workspace and put everything back where it belongs.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel coordinates.
* @package
*/
Blockly.WorkspaceDragger.prototype.endDrag = function(currentDragDeltaXY) {
// Make sure everything is up to date.
this.drag(currentDragDeltaXY);
this.workspace_.resetDragSurface();
};
/**
* Move the workspace based on the most recent mouse movements.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel coordinates.
* @package
*/
Blockly.WorkspaceDragger.prototype.drag = function(currentDragDeltaXY) {
var metrics = this.startDragMetrics_;
var newXY = goog.math.Coordinate.sum(this.startScrollXY_, currentDragDeltaXY);
// Bound the new XY based on workspace bounds.
var x = Math.min(newXY.x, -metrics.contentLeft);
var y = Math.min(newXY.y, -metrics.contentTop);
x = Math.max(x, metrics.viewWidth - metrics.contentLeft -
metrics.contentWidth);
y = Math.max(y, metrics.viewHeight - metrics.contentTop -
metrics.contentHeight);
x = -x - metrics.contentLeft;
y = -y - metrics.contentTop;
this.updateScroll_(x, y);
};
/**
* Move the scrollbars to drag the workspace.
* x and y are in pixels.
* @param {number} x The new x position to move the scrollbar to.
* @param {number} y The new y position to move the scrollbar to.
* @private
*/
Blockly.WorkspaceDragger.prototype.updateScroll_ = function(x, y) {
this.workspace_.scrollbar.set(x, y);
};

File diff suppressed because it is too large Load Diff

919
scratch-blocks/core/xml.js Normal file
View File

@@ -0,0 +1,919 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 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 XML reader and writer.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* @name Blockly.Xml
* @namespace
**/
goog.provide('Blockly.Xml');
goog.require('Blockly.Events.BlockCreate');
goog.require('Blockly.Events.VarCreate');
goog.require('goog.asserts');
goog.require('goog.dom');
/**
* Encode a block tree as XML.
* @param {!Blockly.Workspace} workspace The workspace containing blocks.
* @param {boolean=} opt_noId True if the encoder should skip the block IDs.
* @return {!Element} XML document.
*/
Blockly.Xml.workspaceToDom = function(workspace, opt_noId) {
var xml = goog.dom.createDom('xml');
xml.appendChild(Blockly.Xml.variablesToDom(workspace.getAllVariables()));
var comments = workspace.getTopComments(true).filter(function(topComment) {
return topComment instanceof Blockly.WorkspaceComment;
});
for (var i = 0, comment; comment = comments[i]; i++) {
xml.appendChild(comment.toXmlWithXY(opt_noId));
}
var blocks = workspace.getTopBlocks(true);
for (var i = 0, block; block = blocks[i]; i++) {
xml.appendChild(Blockly.Xml.blockToDomWithXY(block, opt_noId));
}
return xml;
};
/**
* Encode a list of variables as XML.
* @param {!Array.<!Blockly.VariableModel>} variableList List of all variable
* models.
* @return {!Element} List of XML elements.
*/
Blockly.Xml.variablesToDom = function(variableList) {
var variables = goog.dom.createDom('variables');
for (var i = 0, variable; variable = variableList[i]; i++) {
var element = goog.dom.createDom('variable', null, variable.name);
element.setAttribute('type', variable.type);
element.setAttribute('id', variable.getId());
element.setAttribute('islocal', variable.isLocal);
element.setAttribute('isCloud', variable.isCloud);
variables.appendChild(element);
}
return variables;
};
/**
* Encode a block subtree as XML with XY coordinates.
* @param {!Blockly.Block} block The root block to encode.
* @param {boolean=} opt_noId True if the encoder should skip the block ID.
* @return {!Element} Tree of XML elements.
*/
Blockly.Xml.blockToDomWithXY = function(block, opt_noId) {
var width; // Not used in LTR.
if (block.workspace.RTL) {
width = block.workspace.getWidth();
}
var element = Blockly.Xml.blockToDom(block, opt_noId);
var xy = block.getRelativeToSurfaceXY();
element.setAttribute('x',
Math.round(block.workspace.RTL ? width - xy.x : xy.x));
element.setAttribute('y', Math.round(xy.y));
return element;
};
/**
* Encode a variable field as XML.
* @param {!Blockly.FieldVariable} field The field to encode.
* @return {?Element} XML element, or null if the field did not need to be
* serialized.
* @private
*/
Blockly.Xml.fieldToDomVariable_ = function(field) {
var id = field.getValue();
// The field had not been initialized fully before being serialized.
// This can happen if a block is created directly through a call to
// workspace.newBlock instead of from XML.
// The new block will be serialized for the first time when firing a block
// creation event.
if (id == null) {
field.initModel();
id = field.getValue();
}
// Get the variable directly from the field, instead of doing a lookup. This
// will work even if the variable has already been deleted. This can happen
// because the flyout defers deleting blocks until the next time the flyout is
// opened.
var variable = field.getVariable();
if (!variable) {
throw Error('Tried to serialize a variable field with no variable.');
}
var container = goog.dom.createDom('field', null, variable.name);
container.setAttribute('name', field.name);
container.setAttribute('id', variable.getId());
container.setAttribute('variabletype', variable.type);
return container;
};
/**
* Encode a field as XML.
* @param {!Blockly.Field} field The field to encode.
* @param {!Blockly.Workspace} workspace The workspace that the field is in.
* @return {?Element} XML element, or null if the field did not need to be
* serialized.
* @private
*/
Blockly.Xml.fieldToDom_ = function(field) {
if (field.name && field.SERIALIZABLE) {
if (field.referencesVariables()) {
return Blockly.Xml.fieldToDomVariable_(field);
} else {
var container = goog.dom.createDom('field', null, field.getValue());
container.setAttribute('name', field.name);
return container;
}
}
return null;
};
/**
* Encode all of a block's fields as XML and attach them to the given tree of
* XML elements.
* @param {!Blockly.Block} block A block with fields to be encoded.
* @param {!Element} element The XML element to which the field DOM should be
* attached.
* @private
*/
Blockly.Xml.allFieldsToDom_ = function(block, element) {
for (var i = 0, input; input = block.inputList[i]; i++) {
for (var j = 0, field; field = input.fieldRow[j]; j++) {
var fieldDom = Blockly.Xml.fieldToDom_(field);
if (fieldDom) {
element.appendChild(fieldDom);
}
}
}
};
/**
* Encode a block subtree as XML.
* @param {!Blockly.Block} block The root block to encode.
* @param {boolean=} opt_noId True if the encoder should skip the block ID.
* @return {!Element} Tree of XML elements.
*/
Blockly.Xml.blockToDom = function(block, opt_noId) {
var element = goog.dom.createDom(block.isShadow() ? 'shadow' : 'block');
element.setAttribute('type', block.type);
if (!opt_noId) {
element.setAttribute('id', block.id);
}
if (block.mutationToDom) {
// Custom data for an advanced block.
var mutation = block.mutationToDom();
if (mutation && (mutation.hasChildNodes() || mutation.hasAttributes())) {
element.appendChild(mutation);
}
}
Blockly.Xml.allFieldsToDom_(block, element);
Blockly.Xml.scratchCommentToDom_(block, element);
if (block.data) {
var dataElement = goog.dom.createDom('data', null, block.data);
element.appendChild(dataElement);
}
for (var i = 0, input; input = block.inputList[i]; i++) {
var container;
var empty = true;
if (input.type == Blockly.DUMMY_INPUT) {
continue;
} else {
var childBlock = input.connection.targetBlock();
if (input.type == Blockly.INPUT_VALUE) {
container = goog.dom.createDom('value');
} else if (input.type == Blockly.NEXT_STATEMENT) {
container = goog.dom.createDom('statement');
}
var shadow = input.connection.getShadowDom();
if (shadow && (!childBlock || !childBlock.isShadow())) {
var shadowClone = Blockly.Xml.cloneShadow_(shadow);
// Remove the ID from the shadow dom clone if opt_noId
// is specified to true.
if (opt_noId && shadowClone.getAttribute('id')) {
shadowClone.removeAttribute('id');
}
container.appendChild(shadowClone);
}
if (childBlock) {
container.appendChild(Blockly.Xml.blockToDom(childBlock, opt_noId));
empty = false;
}
}
container.setAttribute('name', input.name);
if (!empty) {
element.appendChild(container);
}
}
if (block.inputsInlineDefault != block.inputsInline) {
element.setAttribute('inline', block.inputsInline);
}
if (block.isCollapsed()) {
element.setAttribute('collapsed', true);
}
if (block.disabled) {
element.setAttribute('disabled', true);
}
if (!block.isDeletable() && !block.isShadow()) {
element.setAttribute('deletable', false);
}
if (!block.isMovable() && !block.isShadow()) {
element.setAttribute('movable', false);
}
if (!block.isEditable()) {
element.setAttribute('editable', false);
}
var nextBlock = block.getNextBlock();
if (nextBlock) {
var container = goog.dom.createDom('next', null,
Blockly.Xml.blockToDom(nextBlock, opt_noId));
element.appendChild(container);
}
var shadow = block.nextConnection && block.nextConnection.getShadowDom();
if (shadow && (!nextBlock || !nextBlock.isShadow())) {
container.appendChild(Blockly.Xml.cloneShadow_(shadow));
}
return element;
};
/**
* Encode a ScratchBlockComment as XML.
* @param {!Blockly.ScratchBlockComment} block The block possibly containing
* a comment to encode.
* @param {!Element} element The XML element to which the comment should
* encoding should be attached.
* @private
*/
Blockly.Xml.scratchCommentToDom_ = function(block, element) {
var commentText = block.getCommentText();
if (commentText) {
var commentElement = goog.dom.createDom('comment', null, commentText);
if (typeof block.comment == 'object') {
commentElement.setAttribute('id', block.comment.id);
commentElement.setAttribute('pinned', block.comment.isVisible());
var hw;
if (block.comment instanceof Blockly.ScratchBlockComment) {
hw = block.comment.getHeightWidth();
} else {
hw = block.comment.getBubbleSize();
}
commentElement.setAttribute('h', hw.height);
commentElement.setAttribute('w', hw.width);
var xy = block.comment.getXY();
commentElement.setAttribute('x',
Math.round(block.workspace.RTL ? block.workspace.getWidth() - xy.x - hw.width :
xy.x));
commentElement.setAttribute('y', xy.y);
commentElement.setAttribute('minimized', block.comment.isMinimized());
}
element.appendChild(commentElement);
}
};
/**
* Deeply clone the shadow's DOM so that changes don't back-wash to the block.
* @param {!Element} shadow A tree of XML elements.
* @return {!Element} A tree of XML elements.
* @private
*/
Blockly.Xml.cloneShadow_ = function(shadow) {
shadow = shadow.cloneNode(true);
// Walk the tree looking for whitespace. Don't prune whitespace in a tag.
var node = shadow;
var textNode;
while (node) {
if (node.firstChild) {
node = node.firstChild;
} else {
while (node && !node.nextSibling) {
textNode = node;
node = node.parentNode;
if (textNode.nodeType == 3 && textNode.data.trim() == '' &&
node.firstChild != textNode) {
// Prune whitespace after a tag.
goog.dom.removeNode(textNode);
}
}
if (node) {
textNode = node;
node = node.nextSibling;
if (textNode.nodeType == 3 && textNode.data.trim() == '') {
// Prune whitespace before a tag.
goog.dom.removeNode(textNode);
}
}
}
}
return shadow;
};
/**
* Converts a DOM structure into plain text.
* Currently the text format is fairly ugly: all one line with no whitespace.
* @param {!Element} dom A tree of XML elements.
* @return {string} Text representation.
*/
Blockly.Xml.domToText = function(dom) {
var oSerializer = new XMLSerializer();
return oSerializer.serializeToString(dom);
};
/**
* Converts a DOM structure into properly indented text.
* @param {!Element} dom A tree of XML elements.
* @return {string} Text representation.
*/
Blockly.Xml.domToPrettyText = function(dom) {
// This function is not guaranteed to be correct for all XML.
// But it handles the XML that Blockly generates.
var blob = Blockly.Xml.domToText(dom);
// Place every open and close tag on its own line.
var lines = blob.split('<');
// Indent every line.
var indent = '';
for (var i = 1; i < lines.length; i++) {
var line = lines[i];
if (line[0] == '/') {
indent = indent.substring(2);
}
lines[i] = indent + '<' + line;
if (line[0] != '/' && line.slice(-2) != '/>') {
indent += ' ';
}
}
// Pull simple tags back together.
// E.g. <foo></foo>
var text = lines.join('\n');
text = text.replace(/(<(\w+)\b[^>]*>[^\n]*)\n *<\/\2>/g, '$1</$2>');
// Trim leading blank line.
return text.replace(/^\n/, '');
};
/**
* Converts plain text into a DOM structure.
* Throws an error if XML doesn't parse.
* @param {string} text Text representation.
* @return {!Element} A tree of XML elements.
*/
Blockly.Xml.textToDom = function(text) {
var oParser = new DOMParser();
var dom = oParser.parseFromString(text, 'text/xml');
// The DOM should have one and only one top-level node, an XML tag.
if (!dom || !dom.firstChild ||
dom.firstChild.nodeName.toLowerCase() != 'xml' ||
dom.firstChild !== dom.lastChild) {
// Whatever we got back from the parser is not XML.
goog.asserts.fail('Blockly.Xml.textToDom did not obtain a valid XML tree.');
}
return dom.firstChild;
};
/**
* Clear the given workspace then decode an XML DOM and
* create blocks on the workspace.
* @param {!Element} xml XML DOM.
* @param {!Blockly.Workspace} workspace The workspace.
* @return {Array.<string>} An array containing new block ids.
*/
Blockly.Xml.clearWorkspaceAndLoadFromXml = function(xml, workspace) {
workspace.setResizesEnabled(false);
workspace.setToolboxRefreshEnabled(false);
workspace.clear();
var blockIds = Blockly.Xml.domToWorkspace(xml, workspace);
workspace.setResizesEnabled(true);
workspace.setToolboxRefreshEnabled(true);
return blockIds;
};
/**
* Decode an XML DOM and create blocks on the workspace.
* @param {!Element} xml XML DOM.
* @param {!Blockly.Workspace} workspace The workspace.
* @return {Array.<string>} An array containing new block IDs.
*/
Blockly.Xml.domToWorkspace = function(xml, workspace) {
if (xml instanceof Blockly.Workspace) {
var swap = xml;
xml = workspace;
workspace = swap;
console.warn('Deprecated call to Blockly.Xml.domToWorkspace, ' +
'swap the arguments.');
}
var width; // Not used in LTR.
if (workspace.RTL) {
width = workspace.getWidth();
}
var newBlockIds = []; // A list of block IDs added by this call.
Blockly.Field.startCache();
// Safari 7.1.3 is known to provide node lists with extra references to
// children beyond the lists' length. Trust the length, do not use the
// looping pattern of checking the index for an object.
var childCount = xml.childNodes.length;
var existingGroup = Blockly.Events.getGroup();
if (!existingGroup) {
Blockly.Events.setGroup(true);
}
// Disable workspace resizes as an optimization.
if (workspace.setResizesEnabled) {
workspace.setResizesEnabled(false);
}
var variablesFirst = true;
try {
for (var i = 0; i < childCount; i++) {
var xmlChild = xml.childNodes[i];
var name = xmlChild.nodeName.toLowerCase();
if (name == 'block' ||
(name == 'shadow' && !Blockly.Events.recordUndo)) {
// Allow top-level shadow blocks if recordUndo is disabled since
// that means an undo is in progress. Such a block is expected
// to be moved to a nested destination in the next operation.
var block = Blockly.Xml.domToBlock(xmlChild, workspace);
newBlockIds.push(block.id);
var blockX = xmlChild.hasAttribute('x') ?
parseInt(xmlChild.getAttribute('x'), 10) : 10;
var blockY = xmlChild.hasAttribute('y') ?
parseInt(xmlChild.getAttribute('y'), 10) : 10;
if (!isNaN(blockX) && !isNaN(blockY)) {
block.moveBy(workspace.RTL ? width - blockX : blockX, blockY);
if (block.comment && typeof block.comment === 'object') {
var commentXY = block.comment.getXY();
var commentWidth = block.comment.getBubbleSize().width;
block.comment.moveTo(block.workspace.RTL ? width - commentXY.x - commentWidth : commentXY.x, commentXY.y);
}
}
variablesFirst = false;
} else if (name == 'shadow') {
goog.asserts.fail('Shadow block cannot be a top-level block.');
variablesFirst = false;
} else if (name == 'comment') {
if (workspace.rendered) {
Blockly.WorkspaceCommentSvg.fromXml(xmlChild, workspace, width);
} else {
Blockly.WorkspaceComment.fromXml(xmlChild, workspace);
}
} else if (name == 'variables') {
if (variablesFirst) {
Blockly.Xml.domToVariables(xmlChild, workspace);
} else {
throw Error('\'variables\' tag must exist once before block and ' +
'shadow tag elements in the workspace XML, but it was found in ' +
'another location.');
}
variablesFirst = false;
}
}
} finally {
if (!existingGroup) {
Blockly.Events.setGroup(false);
}
Blockly.Field.stopCache();
}
// Re-enable workspace resizing.
if (workspace.setResizesEnabled) {
workspace.setResizesEnabled(true);
}
return newBlockIds;
};
/**
* Decode an XML DOM and create blocks on the workspace. Position the new
* blocks immediately below prior blocks, aligned by their starting edge.
* @param {!Element} xml The XML DOM.
* @param {!Blockly.Workspace} workspace The workspace to add to.
* @return {Array.<string>} An array containing new block IDs.
*/
Blockly.Xml.appendDomToWorkspace = function(xml, workspace) {
var bbox; // Bounding box of the current blocks.
// First check if we have a workspaceSvg, otherwise the blocks have no shape
// and the position does not matter.
if (workspace.hasOwnProperty('scale')) {
var savetab = Blockly.BlockSvg.TAB_WIDTH;
try {
Blockly.BlockSvg.TAB_WIDTH = 0;
bbox = workspace.getBlocksBoundingBox();
} finally {
Blockly.BlockSvg.TAB_WIDTH = savetab;
}
}
// Load the new blocks into the workspace and get the IDs of the new blocks.
var newBlockIds = Blockly.Xml.domToWorkspace(xml,workspace);
if (bbox && bbox.height) { // check if any previous block
var offsetY = 0; // offset to add to y of the new block
var offsetX = 0;
var farY = bbox.y + bbox.height; //bottom position
var topX = bbox.x; // x of bounding box
// check position of the new blocks
var newX = Infinity; // x of top corner
var newY = Infinity; // y of top corner
for (var i = 0; i < newBlockIds.length; i++) {
var blockXY = workspace.getBlockById(newBlockIds[i]).getRelativeToSurfaceXY();
if (blockXY.y < newY) {
newY = blockXY.y;
}
if (blockXY.x < newX) { //if we align also on x
newX = blockXY.x;
}
}
offsetY = farY - newY + Blockly.BlockSvg.SEP_SPACE_Y;
offsetX = topX - newX;
// move the new blocks to append them at the bottom
var width; // Not used in LTR.
if (workspace.RTL) {
width = workspace.getWidth();
}
for (var i = 0; i < newBlockIds.length; i++) {
var block = workspace.getBlockById(newBlockIds[i]);
block.moveBy(workspace.RTL ? width - offsetX : offsetX, offsetY);
}
}
return newBlockIds;
};
/**
* Decode an XML block tag and create a block (and possibly sub blocks) on the
* workspace.
* @param {!Element} xmlBlock XML block element.
* @param {!Blockly.Workspace} workspace The workspace.
* @return {!Blockly.Block} The root block created.
*/
Blockly.Xml.domToBlock = function(xmlBlock, workspace) {
if (xmlBlock instanceof Blockly.Workspace) {
var swap = xmlBlock;
xmlBlock = workspace;
workspace = swap;
console.warn('Deprecated call to Blockly.Xml.domToBlock, ' +
'swap the arguments.');
}
// Create top-level block.
Blockly.Events.disable();
var variablesBeforeCreation = workspace.getAllVariables();
try {
var topBlock = Blockly.Xml.domToBlockHeadless_(xmlBlock, workspace);
// Generate list of all blocks.
var blocks = topBlock.getDescendants(false);
if (workspace.rendered) {
// Hide connections to speed up assembly.
topBlock.setConnectionsHidden(true);
// Render each block.
for (var i = blocks.length - 1; i >= 0; i--) {
blocks[i].initSvg();
}
for (var i = blocks.length - 1; i >= 0; i--) {
blocks[i].render(false);
}
// Populating the connection database may be deferred until after the
// blocks have rendered.
if (!workspace.isFlyout) {
setTimeout(function() {
if (topBlock.workspace) { // Check that the block hasn't been deleted.
topBlock.setConnectionsHidden(false);
}
}, 1);
}
topBlock.updateDisabled();
// Allow the scrollbars to resize and move based on the new contents.
// TODO(@picklesrus): #387. Remove when domToBlock avoids resizing.
workspace.resizeContents();
} else {
for (var i = blocks.length - 1; i >= 0; i--) {
blocks[i].initModel();
}
}
} finally {
Blockly.Events.enable();
}
if (Blockly.Events.isEnabled()) {
var newVariables = Blockly.Variables.getAddedVariables(workspace,
variablesBeforeCreation);
// Fire a VarCreate event for each (if any) new variable created.
for (var i = 0; i < newVariables.length; i++) {
var thisVariable = newVariables[i];
Blockly.Events.fire(new Blockly.Events.VarCreate(thisVariable));
}
// Block events come after var events, in case they refer to newly created
// variables.
Blockly.Events.fire(new Blockly.Events.BlockCreate(topBlock));
}
return topBlock;
};
/**
* Decode an XML list of variables and add the variables to the workspace.
* @param {!Element} xmlVariables List of XML variable elements.
* @param {!Blockly.Workspace} workspace The workspace to which the variable
* should be added.
*/
Blockly.Xml.domToVariables = function(xmlVariables, workspace) {
for (var i = 0, xmlChild; xmlChild = xmlVariables.children[i]; i++) {
var type = xmlChild.getAttribute('type');
var id = xmlChild.getAttribute('id');
var isLocal = xmlChild.getAttribute('islocal') == 'true';
var isCloud = xmlChild.getAttribute('iscloud') == 'true';
var name = xmlChild.textContent;
if (typeof(type) === undefined || type === null) {
throw Error('Variable with id, ' + id + ' is without a type');
}
workspace.createVariable(name, type, id, isLocal, isCloud);
}
};
/**
* Decode an XML block tag and create a block (and possibly sub blocks) on the
* workspace.
* @param {!Element} xmlBlock XML block element.
* @param {!Blockly.Workspace} workspace The workspace.
* @return {!Blockly.Block} The root block created.
* @private
*/
Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) {
var block = null;
var prototypeName = xmlBlock.getAttribute('type');
goog.asserts.assert(
prototypeName, 'Block type unspecified: %s', xmlBlock.outerHTML);
var id = xmlBlock.getAttribute('id');
block = workspace.newBlock(prototypeName, id);
var blockChild = null;
for (var i = 0, xmlChild; xmlChild = xmlBlock.childNodes[i]; i++) {
if (xmlChild.nodeType == 3) {
// Ignore any text at the <block> level. It's all whitespace anyway.
continue;
}
var input;
// Find any enclosed blocks or shadows in this tag.
var childBlockElement = null;
var childShadowElement = null;
for (var j = 0, grandchild; grandchild = xmlChild.childNodes[j]; j++) {
if (grandchild.nodeType == 1) {
if (grandchild.nodeName.toLowerCase() == 'block') {
childBlockElement = /** @type {!Element} */ (grandchild);
} else if (grandchild.nodeName.toLowerCase() == 'shadow') {
childShadowElement = /** @type {!Element} */ (grandchild);
}
}
}
// Use the shadow block if there is no child block.
if (!childBlockElement && childShadowElement) {
childBlockElement = childShadowElement;
}
var name = xmlChild.getAttribute('name');
switch (xmlChild.nodeName.toLowerCase()) {
case 'mutation':
// Custom data for an advanced block.
if (block.domToMutation) {
block.domToMutation(xmlChild);
if (block.initSvg) {
// Mutation may have added some elements that need initializing.
block.initSvg();
}
}
break;
case 'comment':
var commentId = xmlChild.getAttribute('id');
var bubbleX = parseInt(xmlChild.getAttribute('x'), 10);
var bubbleY = parseInt(xmlChild.getAttribute('y'), 10);
var minimized = xmlChild.getAttribute('minimized') || false;
// Note bubbleX and bubbleY can be NaN, but the ScratchBlockComment
// constructor will handle that.
block.setCommentText(xmlChild.textContent, commentId, bubbleX, bubbleY,
minimized == 'true');
var visible = xmlChild.getAttribute('pinned');
if (visible && !block.isInFlyout) {
// Give the renderer a millisecond to render and position the block
// before positioning the comment bubble.
setTimeout(function() {
if (block.comment && block.comment.setVisible) {
block.comment.setVisible(visible == 'true');
}
}, 1);
}
var bubbleW = parseInt(xmlChild.getAttribute('w'), 10);
var bubbleH = parseInt(xmlChild.getAttribute('h'), 10);
if (!isNaN(bubbleW) && !isNaN(bubbleH) &&
block.comment && block.comment.setVisible) {
if (block.comment instanceof Blockly.ScratchBlockComment) {
block.comment.setSize(bubbleW, bubbleH);
} else {
block.comment.setBubbleSize(bubbleW, bubbleH);
}
}
break;
case 'data':
block.data = xmlChild.textContent;
break;
case 'title':
// Titles were renamed to field in December 2013.
// Fall through.
case 'field':
Blockly.Xml.domToField_(block, name, xmlChild);
break;
case 'value':
case 'statement':
input = block.getInput(name);
if (!input) {
console.warn('Ignoring non-existent input ' + name + ' in block ' +
prototypeName);
break;
}
if (childShadowElement) {
input.connection.setShadowDom(childShadowElement);
}
if (childBlockElement) {
blockChild = Blockly.Xml.domToBlockHeadless_(childBlockElement,
workspace);
if (blockChild.outputConnection) {
input.connection.connect(blockChild.outputConnection);
} else if (blockChild.previousConnection) {
input.connection.connect(blockChild.previousConnection);
} else {
goog.asserts.fail(
'Child block does not have output or previous statement.');
}
}
break;
case 'next':
if (childShadowElement && block.nextConnection) {
block.nextConnection.setShadowDom(childShadowElement);
}
if (childBlockElement) {
goog.asserts.assert(block.nextConnection,
'Next statement does not exist.');
// If there is more than one XML 'next' tag.
goog.asserts.assert(!block.nextConnection.isConnected(),
'Next statement is already connected.');
blockChild = Blockly.Xml.domToBlockHeadless_(childBlockElement,
workspace);
goog.asserts.assert(blockChild.previousConnection,
'Next block does not have previous statement.');
block.nextConnection.connect(blockChild.previousConnection);
}
break;
default:
// Unknown tag; ignore. Same principle as HTML parsers.
console.warn('Ignoring unknown tag: ' + xmlChild.nodeName);
}
}
var inline = xmlBlock.getAttribute('inline');
if (inline) {
block.setInputsInline(inline == 'true');
}
var disabled = xmlBlock.getAttribute('disabled');
if (disabled) {
block.setDisabled(disabled == 'true' || disabled == 'disabled');
}
var deletable = xmlBlock.getAttribute('deletable');
if (deletable) {
block.setDeletable(deletable == 'true');
}
var movable = xmlBlock.getAttribute('movable');
if (movable) {
block.setMovable(movable == 'true');
}
var editable = xmlBlock.getAttribute('editable');
if (editable) {
block.setEditable(editable == 'true');
}
var collapsed = xmlBlock.getAttribute('collapsed');
if (collapsed) {
block.setCollapsed(collapsed == 'true');
}
if (xmlBlock.nodeName.toLowerCase() == 'shadow') {
// Ensure all children are also shadows.
var children = block.getChildren(false);
for (var i = 0, child; child = children[i]; i++) {
goog.asserts.assert(
child.isShadow(), 'Shadow block not allowed non-shadow child.');
}
block.setShadow(true);
}
return block;
};
/**
* Decode an XML variable field tag and set the value of that field.
* @param {!Blockly.Workspace} workspace The workspace that is currently being
* deserialized.
* @param {!Element} xml The field tag to decode.
* @param {string} text The text content of the XML tag.
* @param {!Blockly.FieldVariable} field The field on which the value will be
* set.
* @private
*/
Blockly.Xml.domToFieldVariable_ = function(workspace, xml, text, field) {
var type = xml.getAttribute('variabletype') || '';
// TODO (fenichel): Does this need to be explicit or not?
if (type == '\'\'') {
type = '';
}
var variable;
// This check ensures that there is not both a potential variable and a real
// variable with the same name and type.
if (!workspace.getPotentialVariableMap() && !workspace.isFlyout &&
workspace.getFlyout()) {
var flyoutWs = workspace.getFlyout().getWorkspace();
variable = Blockly.Variables.realizePotentialVar(text, type, flyoutWs, true);
}
if (!variable) {
variable = Blockly.Variables.getOrCreateVariablePackage(workspace, xml.id,
text, type);
}
// This should never happen :)
if (type != null && type !== variable.type) {
throw Error('Serialized variable type with id \'' +
variable.getId() + '\' had type ' + variable.type + ', and ' +
'does not match variable field that references it: ' +
Blockly.Xml.domToText(xml) + '.');
}
field.setValue(variable.getId());
};
/**
* Decode an XML field tag and set the value of that field on the given block.
* @param {!Blockly.Block} block The block that is currently being deserialized.
* @param {string} fieldName The name of the field on the block.
* @param {!Element} xml The field tag to decode.
* @private
*/
Blockly.Xml.domToField_ = function(block, fieldName, xml) {
var field = block.getField(fieldName);
if (!field) {
console.warn('Ignoring non-existent field ' + fieldName + ' in block ' +
block.type);
return;
}
var workspace = block.workspace;
var text = xml.textContent;
if (field.referencesVariables()) {
Blockly.Xml.domToFieldVariable_(workspace, xml, text, field);
} else {
field.setValue(text);
}
};
/**
* Remove any 'next' block (statements in a stack).
* @param {!Element} xmlBlock XML block element.
*/
Blockly.Xml.deleteNext = function(xmlBlock) {
for (var i = 0, child; child = xmlBlock.childNodes[i]; i++) {
if (child.nodeName.toLowerCase() == 'next') {
xmlBlock.removeChild(child);
break;
}
}
};
// Export symbols that would otherwise be renamed by Closure compiler.
if (!goog.global['Blockly']) {
goog.global['Blockly'] = {};
}
if (!goog.global['Blockly']['Xml']) {
goog.global['Blockly']['Xml'] = {};
}
goog.global['Blockly']['Xml']['domToText'] = Blockly.Xml.domToText;
goog.global['Blockly']['Xml']['domToWorkspace'] = Blockly.Xml.domToWorkspace;
goog.global['Blockly']['Xml']['textToDom'] = Blockly.Xml.textToDom;
goog.global['Blockly']['Xml']['workspaceToDom'] = Blockly.Xml.workspaceToDom;
goog.global['Blockly']['Xml']['clearWorkspaceAndLoadFromXml'] =
Blockly.Xml.clearWorkspaceAndLoadFromXml;

View File

@@ -0,0 +1,301 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2015 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 Object representing a zoom icons.
* @author carloslfu@gmail.com (Carlos Galarza)
*/
'use strict';
goog.provide('Blockly.ZoomControls');
goog.require('Blockly.Touch');
goog.require('goog.dom');
/**
* Class for a zoom controls.
* @param {!Blockly.Workspace} workspace The workspace to sit in.
* @constructor
*/
Blockly.ZoomControls = function(workspace) {
this.workspace_ = workspace;
};
/**
* Zoom in icon path.
* @type {string}
* @private
*/
Blockly.ZoomControls.prototype.ZOOM_IN_PATH_ = 'zoom-in.svg';
/**
* Zoom out icon path.
* @type {string}
* @private
*/
Blockly.ZoomControls.prototype.ZOOM_OUT_PATH_ = 'zoom-out.svg';
/**
* Zoom reset icon path.
* @type {string}
* @private
*/
Blockly.ZoomControls.prototype.ZOOM_RESET_PATH_ = 'zoom-reset.svg';
/**
* Width of the zoom controls.
* @type {number}
* @private
*/
Blockly.ZoomControls.prototype.WIDTH_ = 36;
/**
* Height of the zoom controls.
* @type {number}
* @private
*/
Blockly.ZoomControls.prototype.HEIGHT_ = 124;
/**
* Distance between each zoom control.
* @type {number}
* @private
*/
Blockly.ZoomControls.prototype.MARGIN_BETWEEN_ = 8;
/**
* Distance between zoom controls and bottom edge of workspace.
* @type {number}
* @private
*/
Blockly.ZoomControls.prototype.MARGIN_BOTTOM_ = 12;
/**
* Distance between zoom controls and right edge of workspace.
* @type {number}
* @private
*/
Blockly.ZoomControls.prototype.MARGIN_SIDE_ = 12;
/**
* The SVG group containing the zoom controls.
* @type {Element}
* @private
*/
Blockly.ZoomControls.prototype.svgGroup_ = null;
/**
* Left coordinate of the zoom controls.
* @type {number}
* @private
*/
Blockly.ZoomControls.prototype.left_ = 0;
/**
* Top coordinate of the zoom controls.
* @type {number}
* @private
*/
Blockly.ZoomControls.prototype.top_ = 0;
/**
* Create the zoom controls.
* @return {!Element} The zoom controls SVG group.
*/
Blockly.ZoomControls.prototype.createDom = function() {
this.svgGroup_ =
Blockly.utils.createSvgElement('g', {'class': 'blocklyZoom'}, null);
this.createZoomOutSvg_();
this.createZoomInSvg_();
this.createZoomResetSvg_();
return this.svgGroup_;
};
/**
* Initialize the zoom controls.
* @param {number} bottom Distance from workspace bottom to bottom of controls.
* @return {number} Distance from workspace bottom to the top of controls.
*/
Blockly.ZoomControls.prototype.init = function(bottom) {
this.bottom_ = this.MARGIN_BOTTOM_ + bottom;
return this.bottom_ + this.HEIGHT_;
};
/**
* Dispose of this zoom controls.
* Unlink from all DOM elements to prevent memory leaks.
*/
Blockly.ZoomControls.prototype.dispose = function() {
if (this.svgGroup_) {
goog.dom.removeNode(this.svgGroup_);
this.svgGroup_ = null;
}
this.workspace_ = null;
};
/**
* Move the zoom controls to the bottom-right corner.
*/
Blockly.ZoomControls.prototype.position = function() {
var metrics = this.workspace_.getMetrics();
if (!metrics) {
// There are no metrics available (workspace is probably not visible).
return;
}
if (this.workspace_.RTL) {
this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
this.left_ += metrics.flyoutWidth;
if (this.workspace_.toolbox_) {
this.left_ += metrics.absoluteLeft;
}
}
} else {
this.left_ = metrics.viewWidth + metrics.absoluteLeft -
this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
this.left_ -= metrics.flyoutWidth;
}
}
this.top_ = metrics.viewHeight + metrics.absoluteTop -
this.HEIGHT_ - this.bottom_;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
this.top_ -= metrics.flyoutHeight;
}
this.svgGroup_.setAttribute('transform',
'translate(' + this.left_ + ',' + this.top_ + ')');
};
/**
* Create the zoom in icon and its event handler.
* The Scratch Blocks implementation of this function is different from the
* Blockly implementation.
* @private
*/
Blockly.ZoomControls.prototype.createZoomOutSvg_ = function() {
/* This markup will be generated and added to the "blocklyZoom" group:
<image width="36" height="36" y="44" xlink:href="../media/zoom-out.svg">
</image>
*/
var ws = this.workspace_;
/**
* Zoom out control.
* @type {SVGElement}
*/
var zoomoutSvg = Blockly.utils.createSvgElement(
'image',
{
'width': this.WIDTH_,
'height': this.WIDTH_,
'y': (this.WIDTH_ * 1) + (this.MARGIN_BETWEEN_ * 1)
},
this.svgGroup_
);
zoomoutSvg.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
ws.options.pathToMedia + this.ZOOM_OUT_PATH_);
// Attach listener.
Blockly.bindEventWithChecks_(zoomoutSvg, 'mousedown', null, function(e) {
ws.markFocused();
ws.zoomCenter(-1);
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
e.stopPropagation(); // Don't start a workspace scroll.
e.preventDefault(); // Stop double-clicking from selecting text.
});
};
/**
* Create the zoom out icon and its event handler.
* The Scratch Blocks implementation of this function is different from the
* Blockly implementation.
* @private
*/
Blockly.ZoomControls.prototype.createZoomInSvg_ = function() {
/* This markup will be generated and added to the "blocklyZoom" group:
<image width="36" height="36" y="0" xlink:href="../media/zoom-in.svg">
</image>
*/
var ws = this.workspace_;
/**
* Zoom in control.
* @type {SVGElement}
*/
var zoominSvg = Blockly.utils.createSvgElement(
'image',
{
'width': this.WIDTH_,
'height': this.WIDTH_,
'y': 0
},
this.svgGroup_
);
zoominSvg.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
ws.options.pathToMedia + this.ZOOM_IN_PATH_);
// Attach listener.
Blockly.bindEventWithChecks_(zoominSvg, 'mousedown', null, function(e) {
ws.markFocused();
ws.zoomCenter(1);
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
e.stopPropagation(); // Don't start a workspace scroll.
e.preventDefault(); // Stop double-clicking from selecting text.
});
};
/**
* Create the zoom reset icon and its event handler.
* The Scratch Blocks implementation of this function is different from the
* Blockly implementation.
* @private
*/
Blockly.ZoomControls.prototype.createZoomResetSvg_ = function() {
/* This markup will be generated and added to the "blocklyZoom" group:
<image width="36" height="36" y="88" xlink:href="../media/zoom-reset.svg">
</image>
*/
var ws = this.workspace_;
/**
* Zoom reset control.
* @type {SVGElement}
*/
var zoomresetSvg = Blockly.utils.createSvgElement(
'image',
{
'width': this.WIDTH_,
'height': this.WIDTH_,
'y': (this.WIDTH_ * 2) + (this.MARGIN_BETWEEN_ * 2)
},
this.svgGroup_
);
zoomresetSvg.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
ws.options.pathToMedia + this.ZOOM_RESET_PATH_);
// Attach event listeners.
Blockly.bindEventWithChecks_(zoomresetSvg, 'mousedown', null, function(e) {
ws.markFocused();
ws.setScale(ws.options.zoomOptions.startScale);
ws.scrollCenter();
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
e.stopPropagation(); // Don't start a workspace scroll.
e.preventDefault(); // Stop double-clicking from selecting text.
});
};