mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-06-05 09:24:48 -04:00
244 lines
8.3 KiB
JavaScript
244 lines
8.3 KiB
JavaScript
/**
|
|
* @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;
|
|
};
|
|
|
|
/**
|
|
* Encode a string's HTML entities.
|
|
* E.g., <a> -> <a>
|
|
* Does not exist in Blockly, but needed in scratch-blocks
|
|
* @param {string} rawStr Unencoded raw string to encode.
|
|
* @return {string} String with HTML entities encoded.
|
|
* @package
|
|
*/
|
|
Blockly.scratchBlocksUtils.encodeEntities = function(rawStr) {
|
|
// CC-BY-SA https://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
|
|
return rawStr.replace(/[\u00A0-\u9999<>&]/gim, function(i) {
|
|
return '&#' + i.charCodeAt(0) + ';';
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
};
|
|
};
|