mirror of
https://github.com/google/blockly.git
synced 2026-01-08 01:20:12 +01:00
There are only 10 instances of ++x in our codebase, compared with over 500 instances of x++. The stlye guide has no opinion on which to use, nor do I. But the lack of consistency was making regex searches for bugs more difficult.
693 lines
23 KiB
JavaScript
693 lines
23 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2012 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Utility methods.
|
|
* These methods are not specific to Blockly, and could be factored out into
|
|
* a JavaScript framework such as Closure.
|
|
*/
|
|
'use strict';
|
|
|
|
/**
|
|
* Utility methods.
|
|
* These methods are not specific to Blockly, and could be factored out into
|
|
* a JavaScript framework such as Closure.
|
|
* @namespace Blockly.utils
|
|
*/
|
|
goog.module('Blockly.utils');
|
|
|
|
const Msg = goog.require('Blockly.Msg');
|
|
const aria = goog.require('Blockly.utils.aria');
|
|
const browserEvents = goog.require('Blockly.browserEvents');
|
|
const colourUtils = goog.require('Blockly.utils.colour');
|
|
const deprecation = goog.require('Blockly.utils.deprecation');
|
|
const dom = goog.require('Blockly.utils.dom');
|
|
const global = goog.require('Blockly.utils.global');
|
|
const idGenerator = goog.require('Blockly.utils.idGenerator');
|
|
const internalConstants = goog.require('Blockly.internalConstants');
|
|
const math = goog.require('Blockly.utils.math');
|
|
const object = goog.require('Blockly.utils.object');
|
|
const stringUtils = goog.require('Blockly.utils.string');
|
|
const style = goog.require('Blockly.utils.style');
|
|
const svgPaths = goog.require('Blockly.utils.svgPaths');
|
|
const toolbox = goog.require('Blockly.utils.toolbox');
|
|
const userAgent = goog.require('Blockly.utils.userAgent');
|
|
const xmlUtils = goog.require('Blockly.utils.xml');
|
|
/* eslint-disable-next-line no-unused-vars */
|
|
const {Block} = goog.requireType('Blockly.Block');
|
|
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
|
|
const {KeyCodes} = goog.require('Blockly.utils.KeyCodes');
|
|
const {Metrics} = goog.require('Blockly.utils.Metrics');
|
|
const {Rect} = goog.require('Blockly.utils.Rect');
|
|
const {Size} = goog.require('Blockly.utils.Size');
|
|
const {Svg} = goog.require('Blockly.utils.Svg');
|
|
/* eslint-disable-next-line no-unused-vars */
|
|
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
|
|
|
|
|
|
exports.aria = aria;
|
|
exports.colour = colourUtils;
|
|
exports.Coordinate = Coordinate;
|
|
exports.deprecation = deprecation;
|
|
exports.dom = dom;
|
|
exports.global = global.globalThis;
|
|
exports.idGenerator = idGenerator;
|
|
exports.KeyCodes = KeyCodes;
|
|
exports.math = math;
|
|
exports.Metrics = Metrics;
|
|
exports.object = object;
|
|
exports.Rect = Rect;
|
|
exports.Size = Size;
|
|
exports.string = stringUtils;
|
|
exports.style = style;
|
|
exports.Svg = Svg;
|
|
exports.svgPaths = svgPaths;
|
|
exports.toolbox = toolbox;
|
|
exports.userAgent = userAgent;
|
|
exports.xml = xmlUtils;
|
|
|
|
/**
|
|
* Halts the propagation of the event without doing anything else.
|
|
* @param {!Event} e An event.
|
|
* @deprecated
|
|
* @alias Blockly.utils.noEvent
|
|
*/
|
|
const noEvent = function(e) {
|
|
deprecation.warn('Blockly.utils.noEvent', 'September 2021', 'September 2022');
|
|
// This event has been handled. No need to bubble up to the document.
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
exports.noEvent = noEvent;
|
|
|
|
/**
|
|
* Returns true if this event is targeting a text input widget?
|
|
* @param {!Event} e An event.
|
|
* @return {boolean} True if text input.
|
|
* @deprecated Use Blockly.browserEvents.isTargetInput instead.
|
|
* @alias Blockly.utils.isTargetInput
|
|
*/
|
|
const isTargetInput = function(e) {
|
|
deprecation.warn(
|
|
'Blockly.utils.isTargetInput', 'September 2021', 'September 2022',
|
|
'Blockly.browserEvents.isTargetInput');
|
|
return browserEvents.isTargetInput(e);
|
|
};
|
|
exports.isTargetInput = isTargetInput;
|
|
|
|
/**
|
|
* 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 {!Coordinate} Object with .x and .y properties.
|
|
* @alias Blockly.utils.getRelativeXY
|
|
*/
|
|
const getRelativeXY = function(element) {
|
|
const xy = new Coordinate(0, 0);
|
|
// First, check for x and y attributes.
|
|
const x = element.getAttribute('x');
|
|
if (x) {
|
|
xy.x = parseInt(x, 10);
|
|
}
|
|
const y = element.getAttribute('y');
|
|
if (y) {
|
|
xy.y = parseInt(y, 10);
|
|
}
|
|
// Second, check for transform="translate(...)" attribute.
|
|
const transform = element.getAttribute('transform');
|
|
const r = transform && transform.match(getRelativeXY.XY_REGEX_);
|
|
if (r) {
|
|
xy.x += Number(r[1]);
|
|
if (r[3]) {
|
|
xy.y += Number(r[3]);
|
|
}
|
|
}
|
|
|
|
// Then check for style = transform: translate(...) or translate3d(...)
|
|
const style = element.getAttribute('style');
|
|
if (style && style.indexOf('translate') > -1) {
|
|
const styleComponents = style.match(getRelativeXY.XY_STYLE_REGEX_);
|
|
if (styleComponents) {
|
|
xy.x += Number(styleComponents[1]);
|
|
if (styleComponents[3]) {
|
|
xy.y += Number(styleComponents[3]);
|
|
}
|
|
}
|
|
}
|
|
return xy;
|
|
};
|
|
exports.getRelativeXY = getRelativeXY;
|
|
|
|
/**
|
|
* 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 {!Coordinate} Object with .x and .y properties.
|
|
* @alias Blockly.utils.getInjectionDivXY_
|
|
*/
|
|
const getInjectionDivXY = function(element) {
|
|
let x = 0;
|
|
let y = 0;
|
|
while (element) {
|
|
const xy = getRelativeXY(element);
|
|
x = x + xy.x;
|
|
y = y + xy.y;
|
|
const classes = element.getAttribute('class') || '';
|
|
if ((' ' + classes + ' ').indexOf(' injectionDiv ') !== -1) {
|
|
break;
|
|
}
|
|
element = /** @type {!Element} */ (element.parentNode);
|
|
}
|
|
return new Coordinate(x, y);
|
|
};
|
|
exports.getInjectionDivXY_ = getInjectionDivXY;
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
getRelativeXY.XY_REGEX_ = /translate\(\s*([-+\d.e]+)([ ,]\s*([-+\d.e]+)\s*)?/;
|
|
|
|
/**
|
|
* Static regex to pull the x,y values out of a translate() or translate3d()
|
|
* style property.
|
|
* Accounts for same exceptions as XY_REGEX_.
|
|
* @type {!RegExp}
|
|
* @private
|
|
*/
|
|
getRelativeXY.XY_STYLE_REGEX_ =
|
|
/transform:\s*translate(?:3d)?\(\s*([-+\d.e]+)\s*px([ ,]\s*([-+\d.e]+)\s*px)?/;
|
|
|
|
/**
|
|
* Returns true this event is a right-click.
|
|
* @param {!Event} e Mouse event.
|
|
* @return {boolean} True if right-click.
|
|
* @deprecated Use Blockly.browserEvents.isRightButton instead.
|
|
* @alias Blockly.utils.isRightButton
|
|
*/
|
|
const isRightButton = function(e) {
|
|
deprecation.warn(
|
|
'Blockly.utils.isRightButton', 'September 2021', 'September 2022',
|
|
'Blockly.browserEvents.isRightButton');
|
|
return browserEvents.isRightButton(e);
|
|
};
|
|
exports.isRightButton = isRightButton;
|
|
|
|
/**
|
|
* Returns 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.
|
|
* @deprecated Use Blockly.browserEvents.mouseToSvg instead;
|
|
* @alias Blockly.utils.mouseToSvg
|
|
*/
|
|
const mouseToSvg = function(e, svg, matrix) {
|
|
deprecation.warn(
|
|
'Blockly.utils.mouseToSvg', 'September 2021', 'September 2022',
|
|
'Blockly.browserEvents.mouseToSvg');
|
|
return browserEvents.mouseToSvg(e, svg, matrix);
|
|
};
|
|
exports.mouseToSvg = mouseToSvg;
|
|
|
|
/**
|
|
* Returns the scroll delta of a mouse event in pixel units.
|
|
* @param {!Event} e Mouse event.
|
|
* @return {{x: number, y: number}} Scroll delta object with .x and .y
|
|
* properties.
|
|
* @deprecated Use Blockly.browserEvents.getScrollDeltaPixels instead.
|
|
* @alias Blockly.utils.getScrollDeltaPixels
|
|
*/
|
|
const getScrollDeltaPixels = function(e) {
|
|
deprecation.warn(
|
|
'Blockly.utils.getScrollDeltaPixels', 'September 2021', 'September 2022',
|
|
'Blockly.browserEvents.getScrollDeltaPixels');
|
|
return browserEvents.getScrollDeltaPixels(e);
|
|
};
|
|
exports.getScrollDeltaPixels = getScrollDeltaPixels;
|
|
|
|
/**
|
|
* 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
|
|
* 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.
|
|
* @alias Blockly.utils.tokenizeInterpolation
|
|
*/
|
|
const tokenizeInterpolation = function(message) {
|
|
return tokenizeInterpolation_(message, true);
|
|
};
|
|
exports.tokenizeInterpolation = tokenizeInterpolation;
|
|
|
|
/**
|
|
* 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 Msg['MY_MSG'].
|
|
* @param {string|?} message Message, which may be a string that contains
|
|
* string table references.
|
|
* @return {string} String with message references replaced.
|
|
* @alias Blockly.utils.replaceMessageReferences
|
|
*/
|
|
const replaceMessageReferences = function(message) {
|
|
if (typeof message !== 'string') {
|
|
return message;
|
|
}
|
|
const interpolatedResult = tokenizeInterpolation_(message, false);
|
|
// When parseInterpolationTokens === false, interpolatedResult should be at
|
|
// most length 1.
|
|
return interpolatedResult.length ? String(interpolatedResult[0]) : '';
|
|
};
|
|
exports.replaceMessageReferences = replaceMessageReferences;
|
|
|
|
/**
|
|
* Validates that any %{MSG_KEY} references in the message refer to keys of
|
|
* the 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.
|
|
* @alias Blockly.utils.checkMessageReferences
|
|
*/
|
|
const checkMessageReferences = function(message) {
|
|
let validSoFar = true;
|
|
|
|
const msgTable = Msg;
|
|
|
|
// TODO (#1169): Implement support for other string tables,
|
|
// prefixes other than BKY_.
|
|
const m = message.match(/%{BKY_[A-Z]\w*}/ig);
|
|
for (let i = 0; i < m.length; i++) {
|
|
const msgKey = m[i].toUpperCase();
|
|
if (msgTable[msgKey.slice(6, -1)] === undefined) {
|
|
console.warn('No message string for ' + m[i] + ' in ' + message);
|
|
validSoFar = false; // Continue to report other errors.
|
|
}
|
|
}
|
|
|
|
return validSoFar;
|
|
};
|
|
exports.checkMessageReferences = checkMessageReferences;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
const tokenizeInterpolation_ = function(message, parseInterpolationTokens) {
|
|
const tokens = [];
|
|
const 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.
|
|
let state = 0;
|
|
const buffer = [];
|
|
let number = null;
|
|
for (let i = 0; i < chars.length; i++) {
|
|
const c = chars[i];
|
|
if (state === 0) {
|
|
if (c === '%') {
|
|
const 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;
|
|
const 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 {
|
|
const rawKey = buffer.join('');
|
|
if (/[A-Z]\w*/i.test(rawKey)) { // Strict matching
|
|
// Found a valid string key. Attempt case insensitive match.
|
|
const 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.
|
|
const bklyKey = stringUtils.startsWith(keyUpper, 'BKY_') ?
|
|
keyUpper.substring(4) :
|
|
null;
|
|
if (bklyKey && bklyKey in Msg) {
|
|
const rawValue = Msg[bklyKey];
|
|
if (typeof rawValue === 'string') {
|
|
// Attempt to dereference substrings, too, appending to the end.
|
|
Array.prototype.push.apply(
|
|
tokens,
|
|
tokenizeInterpolation_(rawValue, parseInterpolationTokens));
|
|
} 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.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let text = buffer.join('');
|
|
if (text) {
|
|
tokens.push(text);
|
|
}
|
|
|
|
// Merge adjacent text tokens into a single string.
|
|
const mergedTokens = [];
|
|
buffer.length = 0;
|
|
for (let 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.
|
|
* @return {string} A globally unique ID string.
|
|
* @deprecated Use Blockly.utils.idGenerator.genUid instead.
|
|
* @alias Blockly.utils.genUid
|
|
*/
|
|
const genUid = function() {
|
|
deprecation.warn(
|
|
'Blockly.utils.genUid', 'September 2021', 'September 2022',
|
|
'Blockly.utils.idGenerator.genUid');
|
|
return idGenerator.genUid();
|
|
};
|
|
exports.genUid = genUid;
|
|
|
|
/**
|
|
* Check if 3D transforms are supported by adding an element
|
|
* and attempting to set the property.
|
|
* @return {boolean} True if 3D transforms are supported.
|
|
* @alias Blockly.utils.is3dSupported
|
|
*/
|
|
const is3dSupported = function() {
|
|
if (is3dSupported.cached_ !== undefined) {
|
|
return is3dSupported.cached_;
|
|
}
|
|
// CC-BY-SA Lorenzo Polidori
|
|
// stackoverflow.com/questions/5661671/detecting-transform-translate3d-support
|
|
if (!global.globalThis['getComputedStyle']) {
|
|
return false;
|
|
}
|
|
|
|
const el = document.createElement('p');
|
|
let has3d = 'none';
|
|
const 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 (let t in transforms) {
|
|
if (el.style[t] !== undefined) {
|
|
el.style[t] = 'translate3d(1px,1px,1px)';
|
|
const computedStyle = global.globalThis['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);
|
|
is3dSupported.cached_ = has3d !== 'none';
|
|
return is3dSupported.cached_;
|
|
};
|
|
exports.is3dSupported = is3dSupported;
|
|
|
|
/**
|
|
* 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).
|
|
* @alias Blockly.utils.runAfterPageLoad
|
|
*/
|
|
const runAfterPageLoad = function(fn) {
|
|
if (typeof document !== 'object') {
|
|
throw Error('runAfterPageLoad() requires browser document.');
|
|
}
|
|
if (document.readyState === 'complete') {
|
|
fn(); // Page has already loaded. Call immediately.
|
|
} else {
|
|
// Poll readyState.
|
|
const readyStateCheckInterval = setInterval(function() {
|
|
if (document.readyState === 'complete') {
|
|
clearInterval(readyStateCheckInterval);
|
|
fn();
|
|
}
|
|
}, 10);
|
|
}
|
|
};
|
|
exports.runAfterPageLoad = runAfterPageLoad;
|
|
|
|
/**
|
|
* Get the position of the current viewport in window coordinates. This takes
|
|
* scroll into account.
|
|
* @return {!Rect} An object containing window width, height, and
|
|
* scroll position in window coordinates.
|
|
* @alias Blockly.utils.getViewportBBox
|
|
* @package
|
|
*/
|
|
const getViewportBBox = function() {
|
|
// Pixels, in window coordinates.
|
|
const scrollOffset = style.getViewportPageOffset();
|
|
return new Rect(
|
|
scrollOffset.y, document.documentElement.clientHeight + scrollOffset.y,
|
|
scrollOffset.x, document.documentElement.clientWidth + scrollOffset.x);
|
|
};
|
|
exports.getViewportBBox = getViewportBBox;
|
|
|
|
/**
|
|
* Removes the first occurrence of a particular value from an array.
|
|
* @param {!Array} arr Array from which to remove value.
|
|
* @param {*} value Value to remove.
|
|
* @return {boolean} True if an element was removed.
|
|
* @alias Blockly.utils.arrayRemove
|
|
* @package
|
|
*/
|
|
const arrayRemove = function(arr, value) {
|
|
const i = arr.indexOf(value);
|
|
if (i === -1) {
|
|
return false;
|
|
}
|
|
arr.splice(i, 1);
|
|
return true;
|
|
};
|
|
exports.arrayRemove = arrayRemove;
|
|
|
|
/**
|
|
* Gets the document scroll distance as a coordinate object.
|
|
* Copied from Closure's goog.dom.getDocumentScroll.
|
|
* @return {!Coordinate} Object with values 'x' and 'y'.
|
|
* @alias Blockly.utils.getDocumentScroll
|
|
*/
|
|
const getDocumentScroll = function() {
|
|
const el = document.documentElement;
|
|
const win = window;
|
|
if (userAgent.IE && win.pageYOffset !== el.scrollTop) {
|
|
// The keyboard on IE10 touch devices shifts the page using the pageYOffset
|
|
// without modifying scrollTop. For this case, we want the body scroll
|
|
// offsets.
|
|
return new Coordinate(el.scrollLeft, el.scrollTop);
|
|
}
|
|
return new Coordinate(
|
|
win.pageXOffset || el.scrollLeft, win.pageYOffset || el.scrollTop);
|
|
};
|
|
exports.getDocumentScroll = getDocumentScroll;
|
|
|
|
/**
|
|
* Get a map of all the block's descendants mapping their type to the number of
|
|
* children with that type.
|
|
* @param {!Block} block The block to map.
|
|
* @param {boolean=} opt_stripFollowing Optionally ignore all following
|
|
* statements (blocks that are not inside a value or statement input
|
|
* of the block).
|
|
* @return {!Object} Map of types to type counts for descendants of the bock.
|
|
* @alias Blockly.utils.getBlockTypeCounts
|
|
*/
|
|
const getBlockTypeCounts = function(block, opt_stripFollowing) {
|
|
const typeCountsMap = Object.create(null);
|
|
const descendants = block.getDescendants(true);
|
|
if (opt_stripFollowing) {
|
|
const nextBlock = block.getNextBlock();
|
|
if (nextBlock) {
|
|
const index = descendants.indexOf(nextBlock);
|
|
descendants.splice(index, descendants.length - index);
|
|
}
|
|
}
|
|
for (let i = 0, checkBlock; (checkBlock = descendants[i]); i++) {
|
|
if (typeCountsMap[checkBlock.type]) {
|
|
typeCountsMap[checkBlock.type]++;
|
|
} else {
|
|
typeCountsMap[checkBlock.type] = 1;
|
|
}
|
|
}
|
|
return typeCountsMap;
|
|
};
|
|
exports.getBlockTypeCounts = getBlockTypeCounts;
|
|
|
|
/**
|
|
* Converts screen coordinates to workspace coordinates.
|
|
* @param {!WorkspaceSvg} ws The workspace to find the coordinates on.
|
|
* @param {!Coordinate} screenCoordinates The screen coordinates to
|
|
* be converted to workspace coordinates
|
|
* @return {!Coordinate} The workspace coordinates.
|
|
* @alias Blockly.utils.screenToWsCoordinates
|
|
* @package
|
|
*/
|
|
const screenToWsCoordinates = function(ws, screenCoordinates) {
|
|
const screenX = screenCoordinates.x;
|
|
const screenY = screenCoordinates.y;
|
|
|
|
const 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.
|
|
const boundingRect = injectionDiv.getBoundingClientRect();
|
|
|
|
// The client coordinates offset by the injection div's upper left corner.
|
|
const clientOffsetPixels =
|
|
new Coordinate(screenX - boundingRect.left, screenY - boundingRect.top);
|
|
|
|
// The offset in pixels between the main workspace's origin and the upper
|
|
// left corner of the injection div.
|
|
const mainOffsetPixels = ws.getOriginOffsetInPixels();
|
|
|
|
// The position of the new comment in pixels relative to the origin of the
|
|
// main workspace.
|
|
const finalOffsetPixels =
|
|
Coordinate.difference(clientOffsetPixels, mainOffsetPixels);
|
|
|
|
// The position in main workspace coordinates.
|
|
const finalOffsetMainWs = finalOffsetPixels.scale(1 / ws.scale);
|
|
return finalOffsetMainWs;
|
|
};
|
|
exports.screenToWsCoordinates = screenToWsCoordinates;
|
|
|
|
/**
|
|
* Parse a block colour from a number or string, as provided in a block
|
|
* definition.
|
|
* @param {number|string} colour HSV hue value (0 to 360), #RRGGBB string,
|
|
* or a message reference string pointing to one of those two values.
|
|
* @return {{hue: ?number, hex: string}} An object containing the colour as
|
|
* a #RRGGBB string, and the hue if the input was an HSV hue value.
|
|
* @throws {Error} If the colour cannot be parsed.
|
|
* @alias Blockly.utils.parseBlockColour
|
|
*/
|
|
const parseBlockColour = function(colour) {
|
|
const dereferenced =
|
|
(typeof colour === 'string') ? replaceMessageReferences(colour) : colour;
|
|
|
|
const hue = Number(dereferenced);
|
|
if (!isNaN(hue) && 0 <= hue && hue <= 360) {
|
|
return {
|
|
hue: hue,
|
|
hex: colourUtils.hsvToHex(
|
|
hue, internalConstants.HSV_SATURATION,
|
|
internalConstants.HSV_VALUE * 255),
|
|
};
|
|
} else {
|
|
const hex = colourUtils.parse(dereferenced);
|
|
if (hex) {
|
|
// Only store hue if colour is set as a hue.
|
|
return {hue: null, hex: hex};
|
|
} else {
|
|
let errorMsg = 'Invalid colour: "' + dereferenced + '"';
|
|
if (colour !== dereferenced) {
|
|
errorMsg += ' (from "' + colour + '")';
|
|
}
|
|
throw Error(errorMsg);
|
|
}
|
|
}
|
|
};
|
|
exports.parseBlockColour = parseBlockColour;
|