/** * @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 if * a JavaScript framework such as Closure were used. * @author fraser@google.com (Neil Fraser) */ 'use strict'; goog.provide('Blockly.utils'); /** * 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.} Opaque data that can be passed to unbindEvent_. * @private */ Blockly.bindEvent_ = function(node, name, thisObject, func) { var wrapFunc = function(e) { func.call(thisObject, e); }; 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.apply(thisObject, arguments); // 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.} 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; }; /** * Fire a synthetic event synchronously. * @param {!EventTarget} node The event's target node. * @param {string} eventName Name of event (e.g. 'click'). */ Blockly.fireUiEventNow = function(node, eventName) { // Remove the event from the anti-duplicate database. var list = Blockly.fireUiEvent.DB_[eventName]; if (list) { var i = list.indexOf(node); if (i != -1) { list.splice(i, 1); } } // Fire the event in a browser-compatible way. if (document.createEvent) { // W3 var evt = document.createEvent('UIEvents'); evt.initEvent(eventName, true, true); // event type, bubbling, cancelable node.dispatchEvent(evt); } else if (document.createEventObject) { // MSIE var evt = document.createEventObject(); node.fireEvent('on' + eventName, evt); } else { throw 'FireEvent: No event creation mechanism.'; } }; /** * Fire a synthetic event asynchronously. Groups of simultaneous events (e.g. * a tree of blocks being deleted) are merged into one event. * @param {!EventTarget} node The event's target node. * @param {string} eventName Name of event (e.g. 'click'). */ Blockly.fireUiEvent = function(node, eventName) { var list = Blockly.fireUiEvent.DB_[eventName]; if (list) { if (list.indexOf(node) != -1) { // This event is already scheduled to fire. return; } list.push(node); } else { Blockly.fireUiEvent.DB_[eventName] = [node]; } var fire = function() { Blockly.fireUiEventNow(node, eventName); }; setTimeout(fire, 0); }; /** * Database of upcoming firing event types. * Used to fire only one event after multiple changes. * @type {!Object} * @private */ Blockly.fireUiEvent.DB_ = {}; /** * 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(); }; /** * Return the coordinates of the top-left corner of this element relative to * its parent. * @param {!Element} element Element to find the coordinates of. * @return {!Object} Object with .x and .y properties. * @private */ Blockly.getRelativeXY_ = function(element) { var xy = {x: 0, y: 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'); // 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)'. var r = transform && transform.match(/translate\(\s*([-\d.]+)([ ,]\s*([-\d.]+)\s*\))?/); if (r) { xy.x += parseInt(r[1], 10); if (r[3]) { xy.y += parseInt(r[3], 10); } } return xy; }; /** * Return the absolute coordinates of the top-left corner of this element. * The origin (0,0) is the top-left corner of the Blockly svg. * @param {!Element} element Element to find the coordinates of. * @return {!Object} Object with .x and .y properties. * @private */ Blockly.getSvgXY_ = function(element) { var x = 0; var y = 0; do { // Loop through this block and every parent. var xy = Blockly.getRelativeXY_(element); x += xy.x; y += xy.y; element = element.parentNode; } while (element && element != Blockly.svg); return {x: x, y: y}; }; /** * Return the absolute coordinates of the top-left corner of this element. * The origin (0,0) is the top-left corner of the page body. * @param {!Element} element Element to find the coordinates of. * @return {!Object} Object with .x and .y properties. * @private */ Blockly.getAbsoluteXY_ = function(element) { var xy = Blockly.getSvgXY_(element); return Blockly.convertCoordinates(xy.x, xy.y, false); }; /** * Helper method for creating SVG elements. * @param {string} name Element's tag name. * @param {!Object} attrs Dictionary of attribute names and values. * @param {Element=} opt_parent Optional parent on which to append the element. * @return {!SVGElement} Newly created SVG element. */ Blockly.createSvgElement = function(name, attrs, opt_parent) { 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 (opt_parent) { opt_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) { // Control-clicking in WebKit on Mac OS X fails to change button to 2. return e.button == 2 || e.ctrlKey; }; /** * Convert between HTML coordinates and SVG coordinates. * @param {number} x X input coordinate. * @param {number} y Y input coordinate. * @param {boolean} toSvg True to convert to SVG coordinates. * False to convert to mouse/HTML coordinates. * @return {!Object} Object with x and y properties in output coordinates. */ Blockly.convertCoordinates = function(x, y, toSvg) { if (toSvg) { x -= window.scrollX || window.pageXOffset; y -= window.scrollY || window.pageYOffset; } var svgPoint = Blockly.svg.createSVGPoint(); svgPoint.x = x; svgPoint.y = y; var matrix = Blockly.svg.getScreenCTM(); if (toSvg) { matrix = matrix.inverse(); } var xy = svgPoint.matrixTransform(matrix); if (!toSvg) { xy.x += window.scrollX || window.pageXOffset; xy.y += window.scrollY || window.pageYOffset; } return xy; }; /** * 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. * @return {!Object} Object with .x and .y properties. */ Blockly.mouseToSvg = function(e) { var scrollX = window.scrollX || window.pageXOffset; var scrollY = window.scrollY || window.pageYOffset; return Blockly.convertCoordinates(e.clientX + scrollX, e.clientY + scrollY, true); }; /** * Given an array of strings, return the length of the shortest one. * @param {!Array.} 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.} 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.} 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*$/); };