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:
1837
scratch-blocks/core/block.js
Normal file
1837
scratch-blocks/core/block.js
Normal file
File diff suppressed because it is too large
Load Diff
107
scratch-blocks/core/block_animations.js
Normal file
107
scratch-blocks/core/block_animations.js
Normal 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() {
|
||||
};
|
||||
299
scratch-blocks/core/block_drag_surface.js
Normal file
299
scratch-blocks/core/block_drag_surface.js
Normal 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';
|
||||
};
|
||||
424
scratch-blocks/core/block_dragger.js
Normal file
424
scratch-blocks/core/block_dragger.js
Normal 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));
|
||||
}
|
||||
};
|
||||
531
scratch-blocks/core/block_events.js
Normal file
531
scratch-blocks/core/block_events.js
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
892
scratch-blocks/core/block_render_svg_horizontal.js
Normal file
892
scratch-blocks/core/block_render_svg_horizontal.js
Normal 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);
|
||||
}
|
||||
};
|
||||
1734
scratch-blocks/core/block_render_svg_vertical.js
Normal file
1734
scratch-blocks/core/block_render_svg_vertical.js
Normal file
File diff suppressed because it is too large
Load Diff
1355
scratch-blocks/core/block_svg.js
Normal file
1355
scratch-blocks/core/block_svg.js
Normal file
File diff suppressed because it is too large
Load Diff
618
scratch-blocks/core/blockly.js
Normal file
618
scratch-blocks/core/blockly.js
Normal 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;
|
||||
37
scratch-blocks/core/blocks.js
Normal file
37
scratch-blocks/core/blocks.js
Normal 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);
|
||||
664
scratch-blocks/core/bubble.js
Normal file
664
scratch-blocks/core/bubble.js
Normal 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;
|
||||
};
|
||||
285
scratch-blocks/core/bubble_dragger.js
Normal file
285
scratch-blocks/core/bubble_dragger.js
Normal 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());
|
||||
};
|
||||
177
scratch-blocks/core/colours.js
Normal file
177
scratch-blocks/core/colours.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
293
scratch-blocks/core/comment.js
Normal file
293
scratch-blocks/core/comment.js
Normal 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);
|
||||
};
|
||||
539
scratch-blocks/core/comment_events.js
Normal file
539
scratch-blocks/core/comment_events.js
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
776
scratch-blocks/core/connection.js
Normal file
776
scratch-blocks/core/connection.js
Normal 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();
|
||||
};
|
||||
300
scratch-blocks/core/connection_db.js
Normal file
300
scratch-blocks/core/connection_db.js
Normal 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;
|
||||
};
|
||||
408
scratch-blocks/core/constants.js
Normal file
408
scratch-blocks/core/constants.js
Normal 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",
|
||||
};
|
||||
534
scratch-blocks/core/contextmenu.js
Normal file
534
scratch-blocks/core/contextmenu.js
Normal 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
1356
scratch-blocks/core/css.js
Normal file
File diff suppressed because it is too large
Load Diff
490
scratch-blocks/core/data_category.js
Normal file
490
scratch-blocks/core/data_category.js
Normal 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);
|
||||
};
|
||||
260
scratch-blocks/core/dragged_connection_manager.js
Normal file
260
scratch-blocks/core/dragged_connection_manager.js
Normal 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_;
|
||||
};
|
||||
408
scratch-blocks/core/dropdowndiv.js
Normal file
408
scratch-blocks/core/dropdowndiv.js
Normal 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;
|
||||
}
|
||||
};
|
||||
429
scratch-blocks/core/events.js
Normal file
429
scratch-blocks/core/events.js
Normal 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();
|
||||
}
|
||||
};
|
||||
113
scratch-blocks/core/events_abstract.js
Normal file
113
scratch-blocks/core/events_abstract.js
Normal 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;
|
||||
};
|
||||
450
scratch-blocks/core/extensions.js
Normal file
450
scratch-blocks/core/extensions.js
Normal 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_);
|
||||
810
scratch-blocks/core/field.js
Normal file
810
scratch-blocks/core/field.js
Normal 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;
|
||||
};
|
||||
398
scratch-blocks/core/field_angle.js
Normal file
398
scratch-blocks/core/field_angle.js
Normal 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);
|
||||
133
scratch-blocks/core/field_checkbox.js
Normal file
133
scratch-blocks/core/field_checkbox.js
Normal 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);
|
||||
253
scratch-blocks/core/field_colour.js
Normal file
253
scratch-blocks/core/field_colour.js
Normal 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);
|
||||
387
scratch-blocks/core/field_colour_slider.js
Normal file
387
scratch-blocks/core/field_colour_slider.js
Normal 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);
|
||||
353
scratch-blocks/core/field_date.js
Normal file
353
scratch-blocks/core/field_date.js
Normal 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);
|
||||
447
scratch-blocks/core/field_dropdown.js
Normal file
447
scratch-blocks/core/field_dropdown.js
Normal 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);
|
||||
309
scratch-blocks/core/field_iconmenu.js
Normal file
309
scratch-blocks/core/field_iconmenu.js
Normal 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);
|
||||
200
scratch-blocks/core/field_image.js
Normal file
200
scratch-blocks/core/field_image.js
Normal 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);
|
||||
136
scratch-blocks/core/field_label.js
Normal file
136
scratch-blocks/core/field_label.js
Normal 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);
|
||||
125
scratch-blocks/core/field_label_serializable.js
Normal file
125
scratch-blocks/core/field_label_serializable.js
Normal 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);
|
||||
566
scratch-blocks/core/field_matrix.js
Normal file
566
scratch-blocks/core/field_matrix.js
Normal 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);
|
||||
850
scratch-blocks/core/field_note.js
Normal file
850
scratch-blocks/core/field_note.js
Normal 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);
|
||||
366
scratch-blocks/core/field_number.js
Normal file
366
scratch-blocks/core/field_number.js
Normal 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);
|
||||
77
scratch-blocks/core/field_numberdropdown.js
Normal file
77
scratch-blocks/core/field_numberdropdown.js
Normal 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);
|
||||
164
scratch-blocks/core/field_textdropdown.js
Normal file
164
scratch-blocks/core/field_textdropdown.js
Normal 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);
|
||||
675
scratch-blocks/core/field_textinput.js
Normal file
675
scratch-blocks/core/field_textinput.js
Normal 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);
|
||||
105
scratch-blocks/core/field_textinput_removable.js
Normal file
105
scratch-blocks/core/field_textinput_removable.js
Normal 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);
|
||||
385
scratch-blocks/core/field_variable.js
Normal file
385
scratch-blocks/core/field_variable.js
Normal 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);
|
||||
185
scratch-blocks/core/field_variable_getter.js
Normal file
185
scratch-blocks/core/field_variable_getter.js
Normal 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);
|
||||
161
scratch-blocks/core/field_vertical_separator.js
Normal file
161
scratch-blocks/core/field_vertical_separator.js
Normal 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);
|
||||
935
scratch-blocks/core/flyout_base.js
Normal file
935
scratch-blocks/core/flyout_base.js
Normal 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);
|
||||
};
|
||||
324
scratch-blocks/core/flyout_button.js
Normal file
324
scratch-blocks/core/flyout_button.js
Normal 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);
|
||||
}
|
||||
};
|
||||
83
scratch-blocks/core/flyout_dragger.js
Normal file
83
scratch-blocks/core/flyout_dragger.js
Normal 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);
|
||||
}
|
||||
};
|
||||
159
scratch-blocks/core/flyout_extension_category_header.js
Normal file
159
scratch-blocks/core/flyout_extension_category_header.js
Normal 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;
|
||||
};
|
||||
475
scratch-blocks/core/flyout_horizontal.js
Normal file
475
scratch-blocks/core/flyout_horizontal.js
Normal 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();
|
||||
}
|
||||
};
|
||||
770
scratch-blocks/core/flyout_vertical.js
Normal file
770
scratch-blocks/core/flyout_vertical.js
Normal 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;
|
||||
};
|
||||
426
scratch-blocks/core/generator.js
Normal file
426
scratch-blocks/core/generator.js
Normal 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;
|
||||
};
|
||||
1010
scratch-blocks/core/gesture.js
Normal file
1010
scratch-blocks/core/gesture.js
Normal file
File diff suppressed because it is too large
Load Diff
227
scratch-blocks/core/grid.js
Normal file
227
scratch-blocks/core/grid.js
Normal 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
205
scratch-blocks/core/icon.js
Normal 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_;
|
||||
};
|
||||
496
scratch-blocks/core/inject.js
Normal file
496
scratch-blocks/core/inject.js
Normal 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);
|
||||
};
|
||||
285
scratch-blocks/core/input.js
Normal file
285
scratch-blocks/core/input.js
Normal 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);
|
||||
}
|
||||
};
|
||||
678
scratch-blocks/core/insertion_marker_manager.js
Normal file
678
scratch-blocks/core/insertion_marker_manager.js
Normal 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 ****/
|
||||
102
scratch-blocks/core/intersection_observer.js
Normal file
102
scratch-blocks/core/intersection_observer.js
Normal 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);
|
||||
}
|
||||
};
|
||||
62
scratch-blocks/core/msg.js
Normal file
62
scratch-blocks/core/msg.js
Normal 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'
|
||||
};
|
||||
426
scratch-blocks/core/mutator.js
Normal file
426
scratch-blocks/core/mutator.js
Normal 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;
|
||||
198
scratch-blocks/core/names.js
Normal file
198
scratch-blocks/core/names.js
Normal 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();
|
||||
};
|
||||
244
scratch-blocks/core/options.js
Normal file
244
scratch-blocks/core/options.js
Normal 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;
|
||||
};
|
||||
739
scratch-blocks/core/procedures.js
Normal file
739
scratch-blocks/core/procedures.js
Normal 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;
|
||||
}
|
||||
};
|
||||
417
scratch-blocks/core/rendered_connection.js
Normal file
417
scratch-blocks/core/rendered_connection.js
Normal 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_();
|
||||
}
|
||||
};
|
||||
646
scratch-blocks/core/scratch_block_comment.js
Normal file
646
scratch-blocks/core/scratch_block_comment.js
Normal 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();
|
||||
};
|
||||
229
scratch-blocks/core/scratch_blocks_utils.js
Normal file
229
scratch-blocks/core/scratch_blocks_utils.js
Normal 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);
|
||||
};
|
||||
};
|
||||
699
scratch-blocks/core/scratch_bubble.js
Normal file
699
scratch-blocks/core/scratch_bubble.js
Normal 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;
|
||||
};
|
||||
131
scratch-blocks/core/scratch_events.js
Normal file
131
scratch-blocks/core/scratch_events.js
Normal 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'];
|
||||
};
|
||||
85
scratch-blocks/core/scratch_msgs.js
Normal file
85
scratch-blocks/core/scratch_msgs.js
Normal 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;
|
||||
};
|
||||
875
scratch-blocks/core/scrollbar.js
Normal file
875
scratch-blocks/core/scrollbar.js
Normal 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;
|
||||
}
|
||||
};
|
||||
803
scratch-blocks/core/toolbox.js
Normal file
803
scratch-blocks/core/toolbox.js
Normal 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';
|
||||
}
|
||||
};
|
||||
337
scratch-blocks/core/tooltip.js
Normal file
337
scratch-blocks/core/tooltip.js
Normal 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';
|
||||
};
|
||||
226
scratch-blocks/core/touch.js
Normal file
226
scratch-blocks/core/touch.js
Normal 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;
|
||||
};
|
||||
343
scratch-blocks/core/trashcan.js
Normal file
343
scratch-blocks/core/trashcan.js
Normal 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.');
|
||||
};
|
||||
91
scratch-blocks/core/ui_events.js
Normal file
91
scratch-blocks/core/ui_events.js
Normal 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'];
|
||||
};
|
||||
68
scratch-blocks/core/ui_menu_utils.js
Normal file
68
scratch-blocks/core/ui_menu_utils.js
Normal 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;
|
||||
};
|
||||
825
scratch-blocks/core/utils.js
Normal file
825
scratch-blocks/core/utils.js
Normal 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;
|
||||
};
|
||||
259
scratch-blocks/core/variable_events.js
Normal file
259
scratch-blocks/core/variable_events.js
Normal 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);
|
||||
}
|
||||
};
|
||||
415
scratch-blocks/core/variable_map.js
Normal file
415
scratch-blocks/core/variable_map.js
Normal 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;
|
||||
};
|
||||
116
scratch-blocks/core/variable_model.js
Normal file
116
scratch-blocks/core/variable_model.js
Normal 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);
|
||||
};
|
||||
674
scratch-blocks/core/variables.js
Normal file
674
scratch-blocks/core/variables.js
Normal 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;
|
||||
};
|
||||
199
scratch-blocks/core/warning.js
Normal file
199
scratch-blocks/core/warning.js
Normal 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);
|
||||
};
|
||||
344
scratch-blocks/core/widgetdiv.js
Normal file
344
scratch-blocks/core/widgetdiv.js
Normal 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;
|
||||
}
|
||||
};
|
||||
673
scratch-blocks/core/workspace.js
Normal file
673
scratch-blocks/core/workspace.js
Normal 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;
|
||||
170
scratch-blocks/core/workspace_audio.js
Normal file
170
scratch-blocks/core/workspace_audio.js
Normal 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);
|
||||
}
|
||||
};
|
||||
426
scratch-blocks/core/workspace_comment.js
Normal file
426
scratch-blocks/core/workspace_comment.js
Normal 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
|
||||
};
|
||||
};
|
||||
706
scratch-blocks/core/workspace_comment_render_svg.js
Normal file
706
scratch-blocks/core/workspace_comment_render_svg.js
Normal 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);
|
||||
};
|
||||
609
scratch-blocks/core/workspace_comment_svg.js
Normal file
609
scratch-blocks/core/workspace_comment_svg.js
Normal 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;
|
||||
};
|
||||
195
scratch-blocks/core/workspace_drag_surface_svg.js
Normal file
195
scratch-blocks/core/workspace_drag_surface_svg.js
Normal 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';
|
||||
};
|
||||
132
scratch-blocks/core/workspace_dragger.js
Normal file
132
scratch-blocks/core/workspace_dragger.js
Normal 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);
|
||||
};
|
||||
2381
scratch-blocks/core/workspace_svg.js
Normal file
2381
scratch-blocks/core/workspace_svg.js
Normal file
File diff suppressed because it is too large
Load Diff
919
scratch-blocks/core/xml.js
Normal file
919
scratch-blocks/core/xml.js
Normal 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;
|
||||
301
scratch-blocks/core/zoom_controls.js
Normal file
301
scratch-blocks/core/zoom_controls.js
Normal 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.
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user