mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-06-25 11:40:24 -04:00
937 lines
30 KiB
JavaScript
937 lines
30 KiB
JavaScript
/**
|
|
* @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_2D_REGEX_);
|
|
// Try transform3d if 2d transform wasn't there.
|
|
if (!styleComponents) {
|
|
styleComponents = style.match(Blockly.utils.getRelativeXY.XY_3D_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,z values out of a translate3d() style property.
|
|
* Accounts for same exceptions as XY_REGEXP_.
|
|
* @type {!RegExp}
|
|
* @private
|
|
*/
|
|
Blockly.utils.getRelativeXY.XY_3D_REGEX_ =
|
|
/transform:\s*translate3d\(\s*([-+\d.e]+)px([ ,]\s*([-+\d.e]+)\s*)px([ ,]\s*([-+\d.e]+)\s*)px\)?/;
|
|
|
|
/**
|
|
* Static regex to pull the x,y,z values out of a translate3d() style property.
|
|
* Accounts for same exceptions as XY_REGEXP_.
|
|
* @type {!RegExp}
|
|
* @private
|
|
*/
|
|
Blockly.utils.getRelativeXY.XY_2D_REGEX_ =
|
|
/transform:\s*translate\(\s*([-+\d.e]+)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);
|
|
};
|
|
|
|
/**
|
|
* Given an array of strings, return the length of the shortest one.
|
|
* @param {!Array.<string>} array Array of strings.
|
|
* @return {number} Length of shortest string.
|
|
*/
|
|
Blockly.utils.shortestStringLength = function(array) {
|
|
if (!array.length) {
|
|
return 0;
|
|
}
|
|
return array.reduce(function(a, b) {
|
|
return a.length < b.length ? a : b;
|
|
}).length;
|
|
};
|
|
|
|
/**
|
|
* Given an array of strings, return the length of the common prefix.
|
|
* Words may not be split. Any space after a word is included in the length.
|
|
* @param {!Array.<string>} array Array of strings.
|
|
* @param {number=} opt_shortest Length of shortest string.
|
|
* @return {number} Length of common prefix.
|
|
*/
|
|
Blockly.utils.commonWordPrefix = function(array, opt_shortest) {
|
|
if (!array.length) {
|
|
return 0;
|
|
} else if (array.length == 1) {
|
|
return array[0].length;
|
|
}
|
|
var wordPrefix = 0;
|
|
var max = opt_shortest || Blockly.utils.shortestStringLength(array);
|
|
for (var len = 0; len < max; len++) {
|
|
var letter = array[0][len];
|
|
for (var i = 1; i < array.length; i++) {
|
|
if (letter != array[i][len]) {
|
|
return wordPrefix;
|
|
}
|
|
}
|
|
if (letter == ' ') {
|
|
wordPrefix = len + 1;
|
|
}
|
|
}
|
|
for (var i = 1; i < array.length; i++) {
|
|
var letter = array[i][len];
|
|
if (letter && letter != ' ') {
|
|
return wordPrefix;
|
|
}
|
|
}
|
|
return max;
|
|
};
|
|
|
|
/**
|
|
* Given an array of strings, return the length of the common suffix.
|
|
* Words may not be split. Any space after a word is included in the length.
|
|
* @param {!Array.<string>} array Array of strings.
|
|
* @param {number=} opt_shortest Length of shortest string.
|
|
* @return {number} Length of common suffix.
|
|
*/
|
|
Blockly.utils.commonWordSuffix = function(array, opt_shortest) {
|
|
if (!array.length) {
|
|
return 0;
|
|
} else if (array.length == 1) {
|
|
return array[0].length;
|
|
}
|
|
var wordPrefix = 0;
|
|
var max = opt_shortest || Blockly.utils.shortestStringLength(array);
|
|
for (var len = 0; len < max; len++) {
|
|
var letter = array[0].substr(-len - 1, 1);
|
|
for (var i = 1; i < array.length; i++) {
|
|
if (letter != array[i].substr(-len - 1, 1)) {
|
|
return wordPrefix;
|
|
}
|
|
}
|
|
if (letter == ' ') {
|
|
wordPrefix = len + 1;
|
|
}
|
|
}
|
|
for (var i = 1; i < array.length; i++) {
|
|
var letter = array[i].charAt(array[i].length - len - 1);
|
|
if (letter && letter != ' ') {
|
|
return wordPrefix;
|
|
}
|
|
}
|
|
return max;
|
|
};
|
|
|
|
/**
|
|
* 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() {
|
|
if (Blockly.utils.is3dSupported.cached_ !== undefined) {
|
|
return Blockly.utils.is3dSupported.cached_;
|
|
}
|
|
// CC-BY-SA Lorenzo Polidori
|
|
// stackoverflow.com/questions/5661671/detecting-transform-translate3d-support
|
|
if (!goog.global.getComputedStyle) {
|
|
return false;
|
|
}
|
|
|
|
var el = document.createElement('p');
|
|
var has3d = 'none';
|
|
var transforms = {
|
|
'webkitTransform': '-webkit-transform',
|
|
'OTransform': '-o-transform',
|
|
'msTransform': '-ms-transform',
|
|
'MozTransform': '-moz-transform',
|
|
'transform': 'transform'
|
|
};
|
|
|
|
// Add it to the body to get the computed style.
|
|
document.body.insertBefore(el, null);
|
|
|
|
for (var t in transforms) {
|
|
if (el.style[t] !== undefined) {
|
|
el.style[t] = 'translate3d(1px,1px,1px)';
|
|
var computedStyle = goog.global.getComputedStyle(el);
|
|
if (!computedStyle) {
|
|
// getComputedStyle in Firefox returns null when blockly is loaded
|
|
// inside an iframe with display: none. Returning false and not
|
|
// caching is3dSupported means we try again later. This is most likely
|
|
// when users are interacting with blocks which should mean blockly is
|
|
// visible again.
|
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=548397
|
|
document.body.removeChild(el);
|
|
return false;
|
|
}
|
|
has3d = computedStyle.getPropertyValue(transforms[t]);
|
|
}
|
|
}
|
|
document.body.removeChild(el);
|
|
Blockly.utils.is3dSupported.cached_ = has3d !== 'none';
|
|
return Blockly.utils.is3dSupported.cached_;
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
};
|
|
};
|