summaryrefslogtreecommitdiff
path: root/blockly/core/utils.js
diff options
context:
space:
mode:
authorDavid Barksdale <amatus@amatus.name>2016-09-10 17:58:46 -0500
committerDavid Barksdale <amatus@amatus.name>2016-09-10 18:32:35 -0500
commit475f9f3ac7688e58505690d420cafe6ae8bb8b5f (patch)
treeb1bf14b10751fe4e9e146ad7244ec86bb123893d /blockly/core/utils.js
parent923561056ddb63ce82fd1ec2a5e249bbdae267bf (diff)
Merge blockly sub-tree
Diffstat (limited to 'blockly/core/utils.js')
-rw-r--r--blockly/core/utils.js668
1 files changed, 668 insertions, 0 deletions
diff --git a/blockly/core/utils.js b/blockly/core/utils.js
new file mode 100644
index 0000000..fe13572
--- /dev/null
+++ b/blockly/core/utils.js
@@ -0,0 +1,668 @@
+/**
+ * @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';
+
+goog.provide('Blockly.utils');
+
+goog.require('goog.dom');
+goog.require('goog.events.BrowserFeature');
+goog.require('goog.math.Coordinate');
+goog.require('goog.userAgent');
+
+
+/**
+ * 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.
+ * @private
+ */
+Blockly.addClass_ = function(element, className) {
+ var classes = element.getAttribute('class') || '';
+ if ((' ' + classes + ' ').indexOf(' ' + className + ' ') == -1) {
+ if (classes) {
+ classes += ' ';
+ }
+ element.setAttribute('class', classes + className);
+ }
+};
+
+/**
+ * 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.
+ * @private
+ */
+Blockly.removeClass_ = function(element, className) {
+ var classes = element.getAttribute('class');
+ if ((' ' + classes + ' ').indexOf(' ' + className + ' ') != -1) {
+ 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 {
+ element.removeAttribute('class');
+ }
+ }
+};
+
+/**
+ * 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.
+ * @private
+ */
+Blockly.hasClass_ = function(element, className) {
+ var classes = element.getAttribute('class');
+ return (' ' + classes + ' ').indexOf(' ' + className + ' ') != -1;
+};
+
+/**
+ * Bind an event to a function call.
+ * @param {!Node} 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) {
+ if (thisObject) {
+ var wrapFunc = function(e) {
+ func.call(thisObject, e);
+ };
+ } else {
+ var wrapFunc = func;
+ }
+ node.addEventListener(name, wrapFunc, false);
+ var bindData = [[node, name, wrapFunc]];
+ // Add equivalent touch event.
+ if (name in Blockly.bindEvent_.TOUCH_MAP) {
+ wrapFunc = 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;
+ }
+ func.call(thisObject, e);
+ // Stop the browser from scrolling/zooming the page.
+ e.preventDefault();
+ };
+ for (var i = 0, eventName;
+ eventName = Blockly.bindEvent_.TOUCH_MAP[name][i]; i++) {
+ node.addEventListener(eventName, wrapFunc, false);
+ bindData.push([node, eventName, wrapFunc]);
+ }
+ }
+ return bindData;
+};
+
+/**
+ * The TOUCH_MAP lookup dictionary specifies additional touch events to fire,
+ * in conjunction with mouse events.
+ * @type {Object}
+ */
+Blockly.bindEvent_.TOUCH_MAP = {};
+if (goog.events.BrowserFeature.TOUCH_ENABLED) {
+ Blockly.bindEvent_.TOUCH_MAP = {
+ 'mousedown': ['touchstart'],
+ 'mousemove': ['touchmove'],
+ 'mouseup': ['touchend', 'touchcancel']
+ };
+}
+
+/**
+ * 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;
+};
+
+/**
+ * Don't do anything for this event, just halt propagation.
+ * @param {!Event} e An event.
+ */
+Blockly.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.
+ * @private
+ */
+Blockly.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.
+ * @private
+ */
+Blockly.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.getRelativeXY_.XY_REGEXP_);
+ if (r) {
+ xy.x += parseFloat(r[1]);
+ if (r[3]) {
+ xy.y += parseFloat(r[3]);
+ }
+ }
+ return xy;
+};
+
+/**
+ * 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.getRelativeXY_.XY_REGEXP_ =
+ /translate\(\s*([-+\d.e]+)([ ,]\s*([-+\d.e]+)\s*\))?/;
+
+/**
+ * Return the absolute coordinates of the top-left corner of this element,
+ * scales that after canvas SVG element, if it's a descendant.
+ * The origin (0,0) is the top-left corner of the Blockly SVG.
+ * @param {!Element} element Element to find the coordinates of.
+ * @param {!Blockly.Workspace} workspace Element must be in this workspace.
+ * @return {!goog.math.Coordinate} Object with .x and .y properties.
+ * @private
+ */
+Blockly.getSvgXY_ = function(element, workspace) {
+ var x = 0;
+ var y = 0;
+ var scale = 1;
+ if (goog.dom.contains(workspace.getCanvas(), element) ||
+ goog.dom.contains(workspace.getBubbleCanvas(), element)) {
+ // Before the SVG canvas, scale the coordinates.
+ scale = workspace.scale;
+ }
+ do {
+ // Loop through this block and every parent.
+ var xy = Blockly.getRelativeXY_(element);
+ if (element == workspace.getCanvas() ||
+ element == workspace.getBubbleCanvas()) {
+ // After the SVG canvas, don't scale the coordinates.
+ scale = 1;
+ }
+ x += xy.x * scale;
+ y += xy.y * scale;
+ element = element.parentNode;
+ } while (element && element != workspace.getParentSvg());
+ return new goog.math.Coordinate(x, y);
+};
+
+/**
+ * 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.
+ * @param {Blockly.Workspace=} opt_workspace Optional workspace for access to
+ * context (scale...).
+ * @return {!SVGElement} Newly created SVG element.
+ */
+Blockly.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.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 {!Object} Object with .x and .y properties.
+ */
+Blockly.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.shortestStringLength = function(array) {
+ if (!array.length) {
+ return 0;
+ }
+ var len = array[0].length;
+ for (var i = 1; i < array.length; i++) {
+ len = Math.min(len, array[i].length);
+ }
+ return len;
+};
+
+/**
+ * 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.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.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.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.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;
+};
+
+/**
+ * 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*$/);
+};
+
+/**
+ * Parse a string with any number of interpolation tokens (%1, %2, ...).
+ * '%' characters may be self-escaped (%%).
+ * @param {string} message Text containing interpolation tokens.
+ * @return {!Array.<string|number>} Array of strings and numbers.
+ */
+Blockly.utils.tokenizeInterpolation = function(message) {
+ 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.
+ 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 == '%') {
+ state = 1; // Start escape.
+ } else {
+ buffer.push(c); // Regular char.
+ }
+ } else if (state == 1) {
+ if (c == '%') {
+ buffer.push(c); // Escaped %: %%
+ state = 0;
+ } else if ('0' <= c && c <= '9') {
+ state = 2;
+ number = c;
+ var text = buffer.join('');
+ if (text) {
+ tokens.push(text);
+ }
+ buffer.length = 0;
+ } else {
+ buffer.push('%', c); // Not an escape: %a
+ 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;
+ }
+ }
+ }
+ var text = buffer.join('');
+ if (text) {
+ tokens.push(text);
+ }
+ return tokens;
+};
+
+/**
+ * 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.genUid = function() {
+ var length = 20;
+ var soupLength = Blockly.genUid.soup_.length;
+ var id = [];
+ for (var i = 0; i < length; i++) {
+ id[i] = Blockly.genUid.soup_.charAt(Math.random() * soupLength);
+ }
+ return id.join('');
+};
+
+/**
+ * Legal characters for the unique ID.
+ * Should be all on a US keyboard. No XML special characters or control codes.
+ * Removed $ due to issue 251.
+ * @private
+ */
+Blockly.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.wrap_line_(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.wrap_line_ = 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('');
+};