refactor: Convert fields to ES6 classes (#5943)

* refactor: Initial test at refactoring fields to ES6

* refact: reorganize text input and descendants to call super first

* refact: run conversion script on text input field and subclasses

* clean: cleanup fields post-conversion script

* refact: reorganize dropdown and variable fields to call super first

* refact: run class conversion script on dropdown and variable

* clean: clean fields post conversion script

* refact: reorganize misc fields to call super first

* refact: run conversion script on misc fields

* clean: cleanup misc fields after conversion

* fix: add setting the value and whatnot back to the base field. Pass sentinel conistently

* format

* refact: work on making debug compiler happy

* clean: finish making debug build happy

* fix: work on making tests happy

* fix: finish making tests happy

* Fix: fixup angle and multiline fields

* clean: format

* fix: move default value back to DEFAULT_VALUE

* fix: change SENTINEL to SKIP_SETUP

* fix: inline docs

* fix: some misc PR comments

* fix: format

* fix: make compiler hapy with new.target

* fix: types in FieldDropdown

* fix: add @final annotations to Field

* feat: move Sentinel to a utils file

* fix: remove ImageProperties from external API

* clean: cleanup chunks and deps
This commit is contained in:
Beka Westberg
2022-02-28 08:59:33 -08:00
committed by GitHub
parent 4201d43d55
commit cb4521b645
18 changed files with 4765 additions and 4496 deletions

View File

@@ -24,6 +24,7 @@ goog.module('Blockly.Extensions');
const parsing = goog.require('Blockly.utils.parsing');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
goog.requireType('Blockly.Mutator');
@@ -454,7 +455,7 @@ exports.buildTooltipForDropdown = buildTooltipForDropdown;
const checkDropdownOptionsInTable = function(block, dropdownName, lookupTable) {
// Validate all dropdown options have values.
const dropdown = block.getField(dropdownName);
if (!dropdown.isOptionListDynamic()) {
if (dropdown instanceof FieldDropdown && !dropdown.isOptionListDynamic()) {
const options = dropdown.getOptions();
for (let i = 0; i < options.length; i++) {
const optionKey = options[i][1]; // label, then value

File diff suppressed because it is too large Load Diff

View File

@@ -21,108 +21,491 @@ const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const math = goog.require('Blockly.utils.math');
const object = goog.require('Blockly.utils.object');
const userAgent = goog.require('Blockly.utils.userAgent');
const {DropDownDiv} = goog.require('Blockly.DropDownDiv');
const {Field} = goog.require('Blockly.Field');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {KeyCodes} = goog.require('Blockly.utils.KeyCodes');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for an editable angle field.
* @param {string|number=} opt_value The initial value of the field. Should cast
* to a number. Defaults to 0.
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a
* validated number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/angle#creation}
* for a list of properties this parameter supports.
* @extends {FieldTextInput}
* @constructor
* @alias Blockly.FieldAngle
*/
const FieldAngle = function(opt_value, opt_validator, opt_config) {
class FieldAngle extends FieldTextInput {
/**
* Should the angle increase as the angle picker is moved clockwise (true)
* or counterclockwise (false)
* @see FieldAngle.CLOCKWISE
* @type {boolean}
* @param {(string|number|!Sentinel)=} opt_value The initial value of
* the field. Should cast to a number. Defaults to 0.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a
* validated number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/angle#creation}
* for a list of properties this parameter supports.
* @alias Blockly.FieldAngle
*/
constructor(opt_value, opt_validator, opt_config) {
super(Field.SKIP_SETUP);
/**
* Should the angle increase as the angle picker is moved clockwise (true)
* or counterclockwise (false)
* @see FieldAngle.CLOCKWISE
* @type {boolean}
* @private
*/
this.clockwise_ = FieldAngle.CLOCKWISE;
/**
* The offset of zero degrees (and all other angles).
* @see FieldAngle.OFFSET
* @type {number}
* @private
*/
this.offset_ = FieldAngle.OFFSET;
/**
* The maximum angle to allow before wrapping.
* @see FieldAngle.WRAP
* @type {number}
* @private
*/
this.wrap_ = FieldAngle.WRAP;
/**
* The amount to round angles to when using a mouse or keyboard nav input.
* @see FieldAngle.ROUND
* @type {number}
* @private
*/
this.round_ = FieldAngle.ROUND;
/**
* The angle picker's SVG element.
* @type {?SVGElement}
* @private
*/
this.editor_ = null;
/**
* The angle picker's gauge path depending on the value.
* @type {?SVGElement}
*/
this.gauge_ = null;
/**
* The angle picker's line drawn representing the value's angle.
* @type {?SVGElement}
*/
this.line_ = null;
/**
* The degree symbol for this field.
* @type {SVGTSpanElement}
* @protected
*/
this.symbol_ = null;
/**
* Wrapper click event data.
* @type {?browserEvents.Data}
* @private
*/
this.clickWrapper_ = null;
/**
* Surface click event data.
* @type {?browserEvents.Data}
* @private
*/
this.clickSurfaceWrapper_ = null;
/**
* Surface mouse move event data.
* @type {?browserEvents.Data}
* @private
*/
this.moveSurfaceWrapper_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
}
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
configure_(config) {
super.configure_(config);
switch (config['mode']) {
case 'compass':
this.clockwise_ = true;
this.offset_ = 90;
break;
case 'protractor':
// This is the default mode, so we could do nothing. But just to
// future-proof, we'll set it anyway.
this.clockwise_ = false;
this.offset_ = 0;
break;
}
// Allow individual settings to override the mode setting.
const clockwise = config['clockwise'];
if (typeof clockwise === 'boolean') {
this.clockwise_ = clockwise;
}
// If these are passed as null then we should leave them on the default.
let offset = config['offset'];
if (offset !== null) {
offset = Number(offset);
if (!isNaN(offset)) {
this.offset_ = offset;
}
}
let wrap = config['wrap'];
if (wrap !== null) {
wrap = Number(wrap);
if (!isNaN(wrap)) {
this.wrap_ = wrap;
}
}
let round = config['round'];
if (round !== null) {
round = Number(round);
if (!isNaN(round)) {
this.round_ = round;
}
}
}
/**
* Create the block UI for this field.
* @package
*/
initView() {
super.initView();
// Add the degree symbol to the left of the number, even in RTL (issue
// #2380)
this.symbol_ = dom.createSvgElement(Svg.TSPAN, {}, null);
this.symbol_.appendChild(document.createTextNode('\u00B0'));
this.textElement_.appendChild(this.symbol_);
}
/**
* Updates the graph when the field rerenders.
* @protected
* @override
*/
render_() {
super.render_();
this.updateGraph_();
}
/**
* Create and show the angle field's editor.
* @param {Event=} opt_e Optional mouse event that triggered the field to
* open, or undefined if triggered programmatically.
* @protected
*/
showEditor_(opt_e) {
// Mobile browsers have issues with in-line textareas (focus & keyboards).
const noFocus = userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD;
super.showEditor_(opt_e, noFocus);
this.dropdownCreate_();
DropDownDiv.getContentDiv().appendChild(this.editor_);
DropDownDiv.setColour(
this.sourceBlock_.style.colourPrimary,
this.sourceBlock_.style.colourTertiary);
DropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
this.updateGraph_();
}
/**
* Create the angle dropdown editor.
* @private
*/
this.clockwise_ = FieldAngle.CLOCKWISE;
dropdownCreate_() {
const svg = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'height': (FieldAngle.HALF * 2) + 'px',
'width': (FieldAngle.HALF * 2) + 'px',
'style': 'touch-action: none',
},
null);
const circle = dom.createSvgElement(
Svg.CIRCLE, {
'cx': FieldAngle.HALF,
'cy': FieldAngle.HALF,
'r': FieldAngle.RADIUS,
'class': 'blocklyAngleCircle',
},
svg);
this.gauge_ =
dom.createSvgElement(Svg.PATH, {'class': 'blocklyAngleGauge'}, svg);
this.line_ = dom.createSvgElement(
Svg.LINE, {
'x1': FieldAngle.HALF,
'y1': FieldAngle.HALF,
'class': 'blocklyAngleLine',
},
svg);
// Draw markers around the edge.
for (let angle = 0; angle < 360; angle += 15) {
dom.createSvgElement(
Svg.LINE, {
'x1': FieldAngle.HALF + FieldAngle.RADIUS,
'y1': FieldAngle.HALF,
'x2': FieldAngle.HALF + FieldAngle.RADIUS -
(angle % 45 === 0 ? 10 : 5),
'y2': FieldAngle.HALF,
'class': 'blocklyAngleMarks',
'transform': 'rotate(' + angle + ',' + FieldAngle.HALF + ',' +
FieldAngle.HALF + ')',
},
svg);
}
// The angle picker is different from other fields in that it updates on
// mousemove even if it's not in the middle of a drag. In future we may
// change this behaviour.
this.clickWrapper_ =
browserEvents.conditionalBind(svg, 'click', this, this.hide_);
// On touch devices, the picker's value is only updated with a drag. Add
// a click handler on the drag surface to update the value if the surface
// is clicked.
this.clickSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'click', this, this.onMouseMove_, true, true);
this.moveSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'mousemove', this, this.onMouseMove_, true, true);
this.editor_ = svg;
}
/**
* The offset of zero degrees (and all other angles).
* @see FieldAngle.OFFSET
* @type {number}
* Disposes of events and DOM-references belonging to the angle editor.
* @private
*/
this.offset_ = FieldAngle.OFFSET;
dropdownDispose_() {
if (this.clickWrapper_) {
browserEvents.unbind(this.clickWrapper_);
this.clickWrapper_ = null;
}
if (this.clickSurfaceWrapper_) {
browserEvents.unbind(this.clickSurfaceWrapper_);
this.clickSurfaceWrapper_ = null;
}
if (this.moveSurfaceWrapper_) {
browserEvents.unbind(this.moveSurfaceWrapper_);
this.moveSurfaceWrapper_ = null;
}
this.gauge_ = null;
this.line_ = null;
}
/**
* The maximum angle to allow before wrapping.
* @see FieldAngle.WRAP
* @type {number}
* Hide the editor.
* @private
*/
this.wrap_ = FieldAngle.WRAP;
hide_() {
DropDownDiv.hideIfOwner(this);
WidgetDiv.hide();
}
/**
* The amount to round angles to when using a mouse or keyboard nav input.
* @see FieldAngle.ROUND
* @type {number}
* Set the angle to match the mouse's position.
* @param {!Event} e Mouse move event.
* @protected
*/
onMouseMove_(e) {
// Calculate angle.
const bBox = this.gauge_.ownerSVGElement.getBoundingClientRect();
const dx = e.clientX - bBox.left - FieldAngle.HALF;
const dy = e.clientY - bBox.top - FieldAngle.HALF;
let angle = Math.atan(-dy / dx);
if (isNaN(angle)) {
// This shouldn't happen, but let's not let this error propagate further.
return;
}
angle = math.toDegrees(angle);
// 0: East, 90: North, 180: West, 270: South.
if (dx < 0) {
angle += 180;
} else if (dy > 0) {
angle += 360;
}
// Do offsetting.
if (this.clockwise_) {
angle = this.offset_ + 360 - angle;
} else {
angle = 360 - (this.offset_ - angle);
}
this.displayMouseOrKeyboardValue_(angle);
}
/**
* Handles and displays values that are input via mouse or arrow key input.
* These values need to be rounded and wrapped before being displayed so
* that the text input's value is appropriate.
* @param {number} angle New angle.
* @private
*/
this.round_ = FieldAngle.ROUND;
FieldAngle.superClass_.constructor.call(
this, opt_value, opt_validator, opt_config);
displayMouseOrKeyboardValue_(angle) {
if (this.round_) {
angle = Math.round(angle / this.round_) * this.round_;
}
angle = this.wrapValue_(angle);
if (angle !== this.value_) {
this.setEditorValue_(angle);
}
}
/**
* The angle picker's SVG element.
* @type {?SVGElement}
* Redraw the graph with the current angle.
* @private
*/
this.editor_ = null;
updateGraph_() {
if (!this.gauge_) {
return;
}
// Always display the input (i.e. getText) even if it is invalid.
let angleDegrees = Number(this.getText()) + this.offset_;
angleDegrees %= 360;
let angleRadians = math.toRadians(angleDegrees);
const path = ['M ', FieldAngle.HALF, ',', FieldAngle.HALF];
let x2 = FieldAngle.HALF;
let y2 = FieldAngle.HALF;
if (!isNaN(angleRadians)) {
const clockwiseFlag = Number(this.clockwise_);
const angle1 = math.toRadians(this.offset_);
const x1 = Math.cos(angle1) * FieldAngle.RADIUS;
const y1 = Math.sin(angle1) * -FieldAngle.RADIUS;
if (clockwiseFlag) {
angleRadians = 2 * angle1 - angleRadians;
}
x2 += Math.cos(angleRadians) * FieldAngle.RADIUS;
y2 -= Math.sin(angleRadians) * FieldAngle.RADIUS;
// Don't ask how the flag calculations work. They just do.
let largeFlag =
Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2);
if (clockwiseFlag) {
largeFlag = 1 - largeFlag;
}
path.push(
' l ', x1, ',', y1, ' A ', FieldAngle.RADIUS, ',', FieldAngle.RADIUS,
' 0 ', largeFlag, ' ', clockwiseFlag, ' ', x2, ',', y2, ' z');
}
this.gauge_.setAttribute('d', path.join(''));
this.line_.setAttribute('x2', x2);
this.line_.setAttribute('y2', y2);
}
/**
* The angle picker's gauge path depending on the value.
* @type {?SVGElement}
* Handle key down to the editor.
* @param {!Event} e Keyboard event.
* @protected
* @override
*/
this.gauge_ = null;
onHtmlInputKeyDown_(e) {
super.onHtmlInputKeyDown_(e);
let multiplier;
if (e.keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL)
multiplier = this.sourceBlock_.RTL ? 1 : -1;
} else if (e.keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL)
multiplier = this.sourceBlock_.RTL ? -1 : 1;
} else if (e.keyCode === KeyCodes.DOWN) {
// decrement
multiplier = -1;
} else if (e.keyCode === KeyCodes.UP) {
// increment
multiplier = 1;
}
if (multiplier) {
const value = /** @type {number} */ (this.getValue());
this.displayMouseOrKeyboardValue_(value + (multiplier * this.round_));
e.preventDefault();
e.stopPropagation();
}
}
/**
* The angle picker's line drawn representing the value's angle.
* @type {?SVGElement}
* Ensure that the input value is a valid angle.
* @param {*=} opt_newValue The input value.
* @return {?number} A valid angle, or null if invalid.
* @protected
* @override
*/
this.line_ = null;
doClassValidation_(opt_newValue) {
const value = Number(opt_newValue);
if (isNaN(value) || !isFinite(value)) {
return null;
}
return this.wrapValue_(value);
}
/**
* Wrapper click event data.
* @type {?browserEvents.Data}
* Wraps the value so that it is in the range (-360 + wrap, wrap).
* @param {number} value The value to wrap.
* @return {number} The wrapped value.
* @private
*/
this.clickWrapper_ = null;
wrapValue_(value) {
value %= 360;
if (value < 0) {
value += 360;
}
if (value > this.wrap_) {
value -= 360;
}
return value;
}
/**
* Surface click event data.
* @type {?browserEvents.Data}
* @private
* Construct a FieldAngle from a JSON arg object.
* @param {!Object} options A JSON object with options (angle).
* @return {!FieldAngle} The new field instance.
* @package
* @nocollapse
* @override
*/
this.clickSurfaceWrapper_ = null;
/**
* Surface mouse move event data.
* @type {?browserEvents.Data}
* @private
*/
this.moveSurfaceWrapper_ = null;
};
object.inherits(FieldAngle, FieldTextInput);
static fromJson(options) {
// `this` might be a subclass of FieldAngle if that class doesn't override
// the static fromJson method.
return new this(options['angle'], undefined, options);
}
}
/**
* The default value for this field.
@@ -131,26 +514,6 @@ object.inherits(FieldAngle, FieldTextInput);
*/
FieldAngle.prototype.DEFAULT_VALUE = 0;
/**
* Construct a FieldAngle from a JSON arg object.
* @param {!Object} options A JSON object with options (angle).
* @return {!FieldAngle} The new field instance.
* @package
* @nocollapse
*/
FieldAngle.fromJson = function(options) {
// `this` might be a subclass of FieldAngle if that class doesn't override
// the static fromJson method.
return new this(options['angle'], undefined, options);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
FieldAngle.prototype.SERIALIZABLE = true;
/**
* The default amount to round angles to when using a mouse or keyboard nav
* input. Must be a positive integer to support keyboard navigation.
@@ -193,349 +556,6 @@ FieldAngle.WRAP = 360;
*/
FieldAngle.RADIUS = FieldAngle.HALF - 1;
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
FieldAngle.prototype.configure_ = function(config) {
FieldAngle.superClass_.configure_.call(this, config);
switch (config['mode']) {
case 'compass':
this.clockwise_ = true;
this.offset_ = 90;
break;
case 'protractor':
// This is the default mode, so we could do nothing. But just to
// future-proof, we'll set it anyway.
this.clockwise_ = false;
this.offset_ = 0;
break;
}
// Allow individual settings to override the mode setting.
const clockwise = config['clockwise'];
if (typeof clockwise === 'boolean') {
this.clockwise_ = clockwise;
}
// If these are passed as null then we should leave them on the default.
let offset = config['offset'];
if (offset !== null) {
offset = Number(offset);
if (!isNaN(offset)) {
this.offset_ = offset;
}
}
let wrap = config['wrap'];
if (wrap !== null) {
wrap = Number(wrap);
if (!isNaN(wrap)) {
this.wrap_ = wrap;
}
}
let round = config['round'];
if (round !== null) {
round = Number(round);
if (!isNaN(round)) {
this.round_ = round;
}
}
};
/**
* Create the block UI for this field.
* @package
*/
FieldAngle.prototype.initView = function() {
FieldAngle.superClass_.initView.call(this);
// Add the degree symbol to the left of the number, even in RTL (issue #2380)
this.symbol_ = dom.createSvgElement(Svg.TSPAN, {}, null);
this.symbol_.appendChild(document.createTextNode('\u00B0'));
this.textElement_.appendChild(this.symbol_);
};
/**
* Updates the graph when the field rerenders.
* @protected
* @override
*/
FieldAngle.prototype.render_ = function() {
FieldAngle.superClass_.render_.call(this);
this.updateGraph_();
};
/**
* Create and show the angle field's editor.
* @param {Event=} opt_e Optional mouse event that triggered the field to open,
* or undefined if triggered programmatically.
* @protected
*/
FieldAngle.prototype.showEditor_ = function(opt_e) {
// Mobile browsers have issues with in-line textareas (focus & keyboards).
const noFocus = userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD;
FieldAngle.superClass_.showEditor_.call(this, opt_e, noFocus);
this.dropdownCreate_();
DropDownDiv.getContentDiv().appendChild(this.editor_);
DropDownDiv.setColour(
this.sourceBlock_.style.colourPrimary,
this.sourceBlock_.style.colourTertiary);
DropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
this.updateGraph_();
};
/**
* Create the angle dropdown editor.
* @private
*/
FieldAngle.prototype.dropdownCreate_ = function() {
const svg = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'height': (FieldAngle.HALF * 2) + 'px',
'width': (FieldAngle.HALF * 2) + 'px',
'style': 'touch-action: none',
},
null);
const circle = dom.createSvgElement(
Svg.CIRCLE, {
'cx': FieldAngle.HALF,
'cy': FieldAngle.HALF,
'r': FieldAngle.RADIUS,
'class': 'blocklyAngleCircle',
},
svg);
this.gauge_ =
dom.createSvgElement(Svg.PATH, {'class': 'blocklyAngleGauge'}, svg);
this.line_ = dom.createSvgElement(
Svg.LINE, {
'x1': FieldAngle.HALF,
'y1': FieldAngle.HALF,
'class': 'blocklyAngleLine',
},
svg);
// Draw markers around the edge.
for (let angle = 0; angle < 360; angle += 15) {
dom.createSvgElement(
Svg.LINE, {
'x1': FieldAngle.HALF + FieldAngle.RADIUS,
'y1': FieldAngle.HALF,
'x2':
FieldAngle.HALF + FieldAngle.RADIUS - (angle % 45 === 0 ? 10 : 5),
'y2': FieldAngle.HALF,
'class': 'blocklyAngleMarks',
'transform': 'rotate(' + angle + ',' + FieldAngle.HALF + ',' +
FieldAngle.HALF + ')',
},
svg);
}
// The angle picker is different from other fields in that it updates on
// mousemove even if it's not in the middle of a drag. In future we may
// change this behaviour.
this.clickWrapper_ =
browserEvents.conditionalBind(svg, 'click', this, this.hide_);
// On touch devices, the picker's value is only updated with a drag. Add
// a click handler on the drag surface to update the value if the surface
// is clicked.
this.clickSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'click', this, this.onMouseMove_, true, true);
this.moveSurfaceWrapper_ = browserEvents.conditionalBind(
circle, 'mousemove', this, this.onMouseMove_, true, true);
this.editor_ = svg;
};
/**
* Disposes of events and DOM-references belonging to the angle editor.
* @private
*/
FieldAngle.prototype.dropdownDispose_ = function() {
if (this.clickWrapper_) {
browserEvents.unbind(this.clickWrapper_);
this.clickWrapper_ = null;
}
if (this.clickSurfaceWrapper_) {
browserEvents.unbind(this.clickSurfaceWrapper_);
this.clickSurfaceWrapper_ = null;
}
if (this.moveSurfaceWrapper_) {
browserEvents.unbind(this.moveSurfaceWrapper_);
this.moveSurfaceWrapper_ = null;
}
this.gauge_ = null;
this.line_ = null;
};
/**
* Hide the editor.
* @private
*/
FieldAngle.prototype.hide_ = function() {
DropDownDiv.hideIfOwner(this);
WidgetDiv.hide();
};
/**
* Set the angle to match the mouse's position.
* @param {!Event} e Mouse move event.
* @protected
*/
FieldAngle.prototype.onMouseMove_ = function(e) {
// Calculate angle.
const bBox = this.gauge_.ownerSVGElement.getBoundingClientRect();
const dx = e.clientX - bBox.left - FieldAngle.HALF;
const dy = e.clientY - bBox.top - FieldAngle.HALF;
let angle = Math.atan(-dy / dx);
if (isNaN(angle)) {
// This shouldn't happen, but let's not let this error propagate further.
return;
}
angle = math.toDegrees(angle);
// 0: East, 90: North, 180: West, 270: South.
if (dx < 0) {
angle += 180;
} else if (dy > 0) {
angle += 360;
}
// Do offsetting.
if (this.clockwise_) {
angle = this.offset_ + 360 - angle;
} else {
angle = 360 - (this.offset_ - angle);
}
this.displayMouseOrKeyboardValue_(angle);
};
/**
* Handles and displays values that are input via mouse or arrow key input.
* These values need to be rounded and wrapped before being displayed so
* that the text input's value is appropriate.
* @param {number} angle New angle.
* @private
*/
FieldAngle.prototype.displayMouseOrKeyboardValue_ = function(angle) {
if (this.round_) {
angle = Math.round(angle / this.round_) * this.round_;
}
angle = this.wrapValue_(angle);
if (angle !== this.value_) {
this.setEditorValue_(angle);
}
};
/**
* Redraw the graph with the current angle.
* @private
*/
FieldAngle.prototype.updateGraph_ = function() {
if (!this.gauge_) {
return;
}
// Always display the input (i.e. getText) even if it is invalid.
let angleDegrees = Number(this.getText()) + this.offset_;
angleDegrees %= 360;
let angleRadians = math.toRadians(angleDegrees);
const path = ['M ', FieldAngle.HALF, ',', FieldAngle.HALF];
let x2 = FieldAngle.HALF;
let y2 = FieldAngle.HALF;
if (!isNaN(angleRadians)) {
const clockwiseFlag = Number(this.clockwise_);
const angle1 = math.toRadians(this.offset_);
const x1 = Math.cos(angle1) * FieldAngle.RADIUS;
const y1 = Math.sin(angle1) * -FieldAngle.RADIUS;
if (clockwiseFlag) {
angleRadians = 2 * angle1 - angleRadians;
}
x2 += Math.cos(angleRadians) * FieldAngle.RADIUS;
y2 -= Math.sin(angleRadians) * FieldAngle.RADIUS;
// Don't ask how the flag calculations work. They just do.
let largeFlag = Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2);
if (clockwiseFlag) {
largeFlag = 1 - largeFlag;
}
path.push(
' l ', x1, ',', y1, ' A ', FieldAngle.RADIUS, ',', FieldAngle.RADIUS,
' 0 ', largeFlag, ' ', clockwiseFlag, ' ', x2, ',', y2, ' z');
}
this.gauge_.setAttribute('d', path.join(''));
this.line_.setAttribute('x2', x2);
this.line_.setAttribute('y2', y2);
};
/**
* Handle key down to the editor.
* @param {!Event} e Keyboard event.
* @protected
* @override
*/
FieldAngle.prototype.onHtmlInputKeyDown_ = function(e) {
FieldAngle.superClass_.onHtmlInputKeyDown_.call(this, e);
let multiplier;
if (e.keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL)
multiplier = this.sourceBlock_.RTL ? 1 : -1;
} else if (e.keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL)
multiplier = this.sourceBlock_.RTL ? -1 : 1;
} else if (e.keyCode === KeyCodes.DOWN) {
// decrement
multiplier = -1;
} else if (e.keyCode === KeyCodes.UP) {
// increment
multiplier = 1;
}
if (multiplier) {
const value = /** @type {number} */ (this.getValue());
this.displayMouseOrKeyboardValue_(value + (multiplier * this.round_));
e.preventDefault();
e.stopPropagation();
}
};
/**
* Ensure that the input value is a valid angle.
* @param {*=} opt_newValue The input value.
* @return {?number} A valid angle, or null if invalid.
* @protected
* @override
*/
FieldAngle.prototype.doClassValidation_ = function(opt_newValue) {
const value = Number(opt_newValue);
if (isNaN(value) || !isFinite(value)) {
return null;
}
return this.wrapValue_(value);
};
/**
* Wraps the value so that it is in the range (-360 + wrap, wrap).
* @param {number} value The value to wrap.
* @return {number} The wrapped value.
* @private
*/
FieldAngle.prototype.wrapValue_ = function(value) {
value %= 360;
if (value < 0) {
value += 360;
}
if (value > this.wrap_) {
value -= 360;
}
return value;
};
/**
* CSS for angle field. See css.js for use.
*/

View File

@@ -17,41 +17,224 @@ goog.module('Blockly.FieldCheckbox');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const {Field} = goog.require('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockChange');
/**
* Class for a checkbox field.
* @param {string|boolean=} opt_value The initial value of the field. Should
* either be 'TRUE', 'FALSE' or a boolean. Defaults to 'FALSE'.
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a value ('TRUE' or 'FALSE') &
* returns a validated value ('TRUE' or 'FALSE'), or null to abort the
* change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/checkbox#creation}
* for a list of properties this parameter supports.
* @extends {Field}
* @constructor
* @alias Blockly.FieldCheckbox
*/
const FieldCheckbox = function(opt_value, opt_validator, opt_config) {
class FieldCheckbox extends Field {
/**
* Character for the check mark. Used to apply a different check mark
* character to individual fields.
* @type {?string}
* @param {(string|boolean|!Sentinel)=} opt_value The initial value of
* the field. Should either be 'TRUE', 'FALSE' or a boolean. Defaults to
* 'FALSE'.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a value ('TRUE' or 'FALSE') &
* returns a validated value ('TRUE' or 'FALSE'), or null to abort the
* change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/checkbox#creation}
* for a list of properties this parameter supports.
* @alias Blockly.FieldCheckbox
*/
constructor(opt_value, opt_validator, opt_config) {
super(Field.SKIP_SETUP);
/**
* Character for the check mark. Used to apply a different check mark
* character to individual fields.
* @type {string}
* @private
*/
this.checkChar_ = FieldCheckbox.CHECK_CHAR;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates editability.
* @type {string}
*/
this.CURSOR = 'default';
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
}
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
configure_(config) {
super.configure_(config);
if (config['checkCharacter']) {
this.checkChar_ = config['checkCharacter'];
}
}
/**
* Saves this field's value.
* @return {*} The boolean value held by this field.
* @override
* @package
*/
saveState() {
const legacyState = this.saveLegacyState(FieldCheckbox);
if (legacyState !== null) {
return legacyState;
}
return this.getValueBoolean();
}
/**
* Create the block UI for this checkbox.
* @package
*/
initView() {
super.initView();
dom.addClass(
/** @type {!SVGTextElement} **/ (this.textElement_), 'blocklyCheckbox');
this.textElement_.style.display = this.value_ ? 'block' : 'none';
}
/**
* @override
*/
render_() {
if (this.textContent_) {
this.textContent_.nodeValue = this.getDisplayText_();
}
this.updateSize_(this.getConstants().FIELD_CHECKBOX_X_OFFSET);
}
/**
* @override
*/
getDisplayText_() {
return this.checkChar_;
}
/**
* Set the character used for the check mark.
* @param {?string} character The character to use for the check mark, or
* null to use the default.
*/
setCheckCharacter(character) {
this.checkChar_ = character || FieldCheckbox.CHECK_CHAR;
this.forceRerender();
}
/**
* Toggle the state of the checkbox on click.
* @protected
*/
showEditor_() {
this.setValue(!this.value_);
}
/**
* Ensure that the input value is valid ('TRUE' or 'FALSE').
* @param {*=} opt_newValue The input value.
* @return {?string} A valid value ('TRUE' or 'FALSE), or null if invalid.
* @protected
*/
doClassValidation_(opt_newValue) {
if (opt_newValue === true || opt_newValue === 'TRUE') {
return 'TRUE';
}
if (opt_newValue === false || opt_newValue === 'FALSE') {
return 'FALSE';
}
return null;
}
/**
* Update the value of the field, and update the checkElement.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a either 'TRUE' or 'FALSE'.
* @protected
*/
doValueUpdate_(newValue) {
this.value_ = this.convertValueToBool_(newValue);
// Update visual.
if (this.textElement_) {
this.textElement_.style.display = this.value_ ? 'block' : 'none';
}
}
/**
* Get the value of this field, either 'TRUE' or 'FALSE'.
* @return {string} The value of this field.
*/
getValue() {
return this.value_ ? 'TRUE' : 'FALSE';
}
/**
* Get the boolean value of this field.
* @return {boolean} The boolean value of this field.
*/
getValueBoolean() {
return /** @type {boolean} */ (this.value_);
}
/**
* Get the text of this field. Used when the block is collapsed.
* @return {string} Text representing the value of this field
* ('true' or 'false').
*/
getText() {
return String(this.convertValueToBool_(this.value_));
}
/**
* Convert a value into a pure boolean.
*
* Converts 'TRUE' to true and 'FALSE' to false correctly, everything else
* is cast to a boolean.
* @param {*} value The value to convert.
* @return {boolean} The converted value.
* @private
*/
this.checkChar_ = null;
convertValueToBool_(value) {
if (typeof value === 'string') {
return value === 'TRUE';
} else {
return !!value;
}
}
FieldCheckbox.superClass_.constructor.call(
this, opt_value, opt_validator, opt_config);
};
object.inherits(FieldCheckbox, Field);
/**
* Construct a FieldCheckbox from a JSON arg object.
* @param {!Object} options A JSON object with options (checked).
* @return {!FieldCheckbox} The new field instance.
* @package
* @nocollapse
*/
static fromJson(options) {
// `this` might be a subclass of FieldCheckbox if that class doesn't
// 'override' the static fromJson method.
return new this(options['checked'], undefined, options);
}
}
/**
* The default value for this field.
@@ -60,19 +243,6 @@ object.inherits(FieldCheckbox, Field);
*/
FieldCheckbox.prototype.DEFAULT_VALUE = false;
/**
* Construct a FieldCheckbox from a JSON arg object.
* @param {!Object} options A JSON object with options (checked).
* @return {!FieldCheckbox} The new field instance.
* @package
* @nocollapse
*/
FieldCheckbox.fromJson = function(options) {
// `this` might be a subclass of FieldCheckbox if that class doesn't override
// the static fromJson method.
return new this(options['checked'], undefined, options);
};
/**
* Default character for the checkmark.
* @type {string}
@@ -80,164 +250,6 @@ FieldCheckbox.fromJson = function(options) {
*/
FieldCheckbox.CHECK_CHAR = '\u2713';
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
FieldCheckbox.prototype.SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates editability.
*/
FieldCheckbox.prototype.CURSOR = 'default';
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
FieldCheckbox.prototype.configure_ = function(config) {
FieldCheckbox.superClass_.configure_.call(this, config);
if (config['checkCharacter']) {
this.checkChar_ = config['checkCharacter'];
}
};
/**
* Saves this field's value.
* @return {*} The boolean value held by this field.
* @override
* @package
*/
FieldCheckbox.prototype.saveState = function() {
const legacyState = this.saveLegacyState(FieldCheckbox);
if (legacyState !== null) {
return legacyState;
}
return this.getValueBoolean();
};
/**
* Create the block UI for this checkbox.
* @package
*/
FieldCheckbox.prototype.initView = function() {
FieldCheckbox.superClass_.initView.call(this);
dom.addClass(
/** @type {!SVGTextElement} **/ (this.textElement_), 'blocklyCheckbox');
this.textElement_.style.display = this.value_ ? 'block' : 'none';
};
/**
* @override
*/
FieldCheckbox.prototype.render_ = function() {
if (this.textContent_) {
this.textContent_.nodeValue = this.getDisplayText_();
}
this.updateSize_(this.getConstants().FIELD_CHECKBOX_X_OFFSET);
};
/**
* @override
*/
FieldCheckbox.prototype.getDisplayText_ = function() {
return this.checkChar_ || FieldCheckbox.CHECK_CHAR;
};
/**
* Set the character used for the check mark.
* @param {?string} character The character to use for the check mark, or
* null to use the default.
*/
FieldCheckbox.prototype.setCheckCharacter = function(character) {
this.checkChar_ = character;
this.forceRerender();
};
/**
* Toggle the state of the checkbox on click.
* @protected
*/
FieldCheckbox.prototype.showEditor_ = function() {
this.setValue(!this.value_);
};
/**
* Ensure that the input value is valid ('TRUE' or 'FALSE').
* @param {*=} opt_newValue The input value.
* @return {?string} A valid value ('TRUE' or 'FALSE), or null if invalid.
* @protected
*/
FieldCheckbox.prototype.doClassValidation_ = function(opt_newValue) {
if (opt_newValue === true || opt_newValue === 'TRUE') {
return 'TRUE';
}
if (opt_newValue === false || opt_newValue === 'FALSE') {
return 'FALSE';
}
return null;
};
/**
* Update the value of the field, and update the checkElement.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a either 'TRUE' or 'FALSE'.
* @protected
*/
FieldCheckbox.prototype.doValueUpdate_ = function(newValue) {
this.value_ = this.convertValueToBool_(newValue);
// Update visual.
if (this.textElement_) {
this.textElement_.style.display = this.value_ ? 'block' : 'none';
}
};
/**
* Get the value of this field, either 'TRUE' or 'FALSE'.
* @return {string} The value of this field.
*/
FieldCheckbox.prototype.getValue = function() {
return this.value_ ? 'TRUE' : 'FALSE';
};
/**
* Get the boolean value of this field.
* @return {boolean} The boolean value of this field.
*/
FieldCheckbox.prototype.getValueBoolean = function() {
return /** @type {boolean} */ (this.value_);
};
/**
* Get the text of this field. Used when the block is collapsed.
* @return {string} Text representing the value of this field
* ('true' or 'false').
*/
FieldCheckbox.prototype.getText = function() {
return String(this.convertValueToBool_(this.value_));
};
/**
* Convert a value into a pure boolean.
*
* Converts 'TRUE' to true and 'FALSE' to false correctly, everything else
* is cast to a boolean.
* @param {*} value The value to convert.
* @return {boolean} The converted value.
* @private
*/
FieldCheckbox.prototype.convertValueToBool_ = function(value) {
if (typeof value === 'string') {
return value === 'TRUE';
} else {
return !!value;
}
};
fieldRegistry.register('field_checkbox', FieldCheckbox);
exports.FieldCheckbox = FieldCheckbox;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -17,108 +17,272 @@ goog.module('Blockly.FieldImage');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
const {Field} = goog.require('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Size} = goog.require('Blockly.utils.Size');
const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for an image on a block.
* @param {string} src The URL of the image.
* @param {!(string|number)} width Width of the image.
* @param {!(string|number)} height Height of the image.
* @param {string=} opt_alt Optional alt text for when block is collapsed.
* @param {function(!FieldImage)=} opt_onClick Optional function to be
* called when the image is clicked. If opt_onClick is defined, opt_alt must
* also be defined.
* @param {boolean=} opt_flipRtl Whether to flip the icon in RTL.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/image#creation}
* for a list of properties this parameter supports.
* @extends {Field}
* @constructor
* @alias Blockly.FieldImage
*/
const FieldImage = function(
src, width, height, opt_alt, opt_onClick, opt_flipRtl, opt_config) {
// Return early.
if (!src) {
throw Error('Src value of an image field is required');
}
src = parsing.replaceMessageReferences(src);
const imageHeight = Number(parsing.replaceMessageReferences(height));
const imageWidth = Number(parsing.replaceMessageReferences(width));
if (isNaN(imageHeight) || isNaN(imageWidth)) {
throw Error(
'Height and width values of an image field must cast to' +
' numbers.');
}
if (imageHeight <= 0 || imageWidth <= 0) {
throw Error(
'Height and width values of an image field must be greater' +
' than 0.');
}
// Initialize configurable properties.
class FieldImage extends Field {
/**
* Whether to flip this image in RTL.
* @type {boolean}
* @private
* @param {string|!Sentinel} src The URL of the image.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {!(string|number)} width Width of the image.
* @param {!(string|number)} height Height of the image.
* @param {string=} opt_alt Optional alt text for when block is collapsed.
* @param {function(!FieldImage)=} opt_onClick Optional function to be
* called when the image is clicked. If opt_onClick is defined, opt_alt
* must also be defined.
* @param {boolean=} opt_flipRtl Whether to flip the icon in RTL.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/image#creation}
* for a list of properties this parameter supports.
* @alias Blockly.FieldImage
*/
this.flipRtl_ = false;
constructor(
src, width, height, opt_alt, opt_onClick, opt_flipRtl, opt_config) {
super(Field.SKIP_SETUP);
/**
* Alt text of this image.
* @type {string}
* @private
*/
this.altText_ = '';
// Return early.
if (!src) {
throw Error('Src value of an image field is required');
}
const imageHeight = Number(parsing.replaceMessageReferences(height));
const imageWidth = Number(parsing.replaceMessageReferences(width));
if (isNaN(imageHeight) || isNaN(imageWidth)) {
throw Error(
'Height and width values of an image field must cast to' +
' numbers.');
}
if (imageHeight <= 0 || imageWidth <= 0) {
throw Error(
'Height and width values of an image field must be greater' +
' than 0.');
}
FieldImage.superClass_.constructor.call(this, src, null, opt_config);
/**
* The size of the area rendered by the field.
* @type {Size}
* @protected
* @override
*/
this.size_ = new Size(imageWidth, imageHeight + FieldImage.Y_PADDING);
if (!opt_config) { // If the config wasn't passed, do old configuration.
this.flipRtl_ = !!opt_flipRtl;
this.altText_ = parsing.replaceMessageReferences(opt_alt) || '';
/**
* Store the image height, since it is different from the field height.
* @type {number}
* @private
*/
this.imageHeight_ = imageHeight;
/**
* The function to be called when this field is clicked.
* @type {?function(!FieldImage)}
* @private
*/
this.clickHandler_ = null;
if (typeof opt_onClick === 'function') {
this.clickHandler_ = opt_onClick;
}
/**
* The rendered field's image element.
* @type {SVGImageElement}
* @private
*/
this.imageElement_ = null;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
* @const
*/
this.EDITABLE = false;
/**
* Used to tell if the field needs to be rendered the next time the block is
* rendered. Image fields are statically sized, and only need to be
* rendered at initialization.
* @type {boolean}
* @protected
*/
this.isDirty_ = false;
/**
* Whether to flip this image in RTL.
* @type {boolean}
* @private
*/
this.flipRtl_ = false;
/**
* Alt text of this image.
* @type {string}
* @private
*/
this.altText_ = '';
if (src === Field.SKIP_SETUP) return;
if (opt_config) {
this.configure_(opt_config);
} else {
this.flipRtl_ = !!opt_flipRtl;
this.altText_ = parsing.replaceMessageReferences(opt_alt) || '';
}
this.setValue(parsing.replaceMessageReferences(src));
}
// Initialize other properties.
/**
* The size of the area rendered by the field.
* @type {Size}
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
this.size_ = new Size(imageWidth, imageHeight + FieldImage.Y_PADDING);
/**
* Store the image height, since it is different from the field height.
* @type {number}
* @private
*/
this.imageHeight_ = imageHeight;
/**
* The function to be called when this field is clicked.
* @type {?function(!FieldImage)}
* @private
*/
this.clickHandler_ = null;
if (typeof opt_onClick === 'function') {
this.clickHandler_ = opt_onClick;
configure_(config) {
super.configure_(config);
this.flipRtl_ = !!config['flipRtl'];
this.altText_ = parsing.replaceMessageReferences(config['alt']) || '';
}
/**
* The rendered field's image element.
* @type {SVGImageElement}
* @private
* Create the block UI for this image.
* @package
*/
this.imageElement_ = null;
};
object.inherits(FieldImage, Field);
initView() {
this.imageElement_ = dom.createSvgElement(
Svg.IMAGE, {
'height': this.imageHeight_ + 'px',
'width': this.size_.width + 'px',
'alt': this.altText_,
},
this.fieldGroup_);
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', /** @type {string} */ (this.value_));
if (this.clickHandler_) {
this.imageElement_.style.cursor = 'pointer';
}
}
/**
* @override
*/
updateSize_() {
// NOP
}
/**
* Ensure that the input value (the source URL) is a string.
* @param {*=} opt_newValue The input value.
* @return {?string} A string, or null if invalid.
* @protected
*/
doClassValidation_(opt_newValue) {
if (typeof opt_newValue !== 'string') {
return null;
}
return opt_newValue;
}
/**
* Update the value of this image field, and update the displayed image.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a string.
* @protected
*/
doValueUpdate_(newValue) {
this.value_ = newValue;
if (this.imageElement_) {
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', String(this.value_));
}
}
/**
* Get whether to flip this image in RTL
* @return {boolean} True if we should flip in RTL.
* @override
*/
getFlipRtl() {
return this.flipRtl_;
}
/**
* Set the alt text of this image.
* @param {?string} alt New alt text.
* @public
*/
setAlt(alt) {
if (alt === this.altText_) {
return;
}
this.altText_ = alt || '';
if (this.imageElement_) {
this.imageElement_.setAttribute('alt', this.altText_);
}
}
/**
* If field click is called, and click handler defined,
* call the handler.
* @protected
*/
showEditor_() {
if (this.clickHandler_) {
this.clickHandler_(this);
}
}
/**
* Set the function that is called when this image is clicked.
* @param {?function(!FieldImage)} func The function that is called
* when the image is clicked, or null to remove.
*/
setOnClickHandler(func) {
this.clickHandler_ = func;
}
/**
* Use the `getText_` developer hook to override the field's text
* representation.
* Return the image alt text instead.
* @return {?string} The image alt text.
* @protected
* @override
*/
getText_() {
return this.altText_;
}
/**
* Construct a FieldImage from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (src, width, height,
* alt, and flipRtl).
* @return {!FieldImage} The new field instance.
* @package
* @nocollapse
*/
static fromJson(options) {
// `this` might be a subclass of FieldImage if that class doesn't override
// the static fromJson method.
return new this(
options['src'], options['width'], options['height'], undefined,
undefined, undefined, options);
}
}
/**
* The default value for this field.
@@ -127,23 +291,6 @@ object.inherits(FieldImage, Field);
*/
FieldImage.prototype.DEFAULT_VALUE = '';
/**
* Construct a FieldImage from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (src, width, height,
* alt, and flipRtl).
* @return {!FieldImage} The new field instance.
* @package
* @nocollapse
*/
FieldImage.fromJson = function(options) {
// `this` might be a subclass of FieldImage if that class doesn't override
// the static fromJson method.
return new this(
options['src'], options['width'], options['height'], undefined, undefined,
undefined, options);
};
/**
* Vertical padding below the image, which is included in the reported height of
* the field.
@@ -152,144 +299,6 @@ FieldImage.fromJson = function(options) {
*/
FieldImage.Y_PADDING = 1;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
FieldImage.prototype.EDITABLE = false;
/**
* Used to tell if the field needs to be rendered the next time the block is
* rendered. Image fields are statically sized, and only need to be
* rendered at initialization.
* @type {boolean}
* @protected
*/
FieldImage.prototype.isDirty_ = false;
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
FieldImage.prototype.configure_ = function(config) {
FieldImage.superClass_.configure_.call(this, config);
this.flipRtl_ = !!config['flipRtl'];
this.altText_ = parsing.replaceMessageReferences(config['alt']) || '';
};
/**
* Create the block UI for this image.
* @package
*/
FieldImage.prototype.initView = function() {
this.imageElement_ = dom.createSvgElement(
Svg.IMAGE, {
'height': this.imageHeight_ + 'px',
'width': this.size_.width + 'px',
'alt': this.altText_,
},
this.fieldGroup_);
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', /** @type {string} */ (this.value_));
if (this.clickHandler_) {
this.imageElement_.style.cursor = 'pointer';
}
};
/**
* @override
*/
FieldImage.prototype.updateSize_ = function() {
// NOP
};
/**
* Ensure that the input value (the source URL) is a string.
* @param {*=} opt_newValue The input value.
* @return {?string} A string, or null if invalid.
* @protected
*/
FieldImage.prototype.doClassValidation_ = function(opt_newValue) {
if (typeof opt_newValue !== 'string') {
return null;
}
return opt_newValue;
};
/**
* Update the value of this image field, and update the displayed image.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a string.
* @protected
*/
FieldImage.prototype.doValueUpdate_ = function(newValue) {
this.value_ = newValue;
if (this.imageElement_) {
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', String(this.value_));
}
};
/**
* Get whether to flip this image in RTL
* @return {boolean} True if we should flip in RTL.
* @override
*/
FieldImage.prototype.getFlipRtl = function() {
return this.flipRtl_;
};
/**
* Set the alt text of this image.
* @param {?string} alt New alt text.
* @public
*/
FieldImage.prototype.setAlt = function(alt) {
if (alt === this.altText_) {
return;
}
this.altText_ = alt || '';
if (this.imageElement_) {
this.imageElement_.setAttribute('alt', this.altText_);
}
};
/**
* If field click is called, and click handler defined,
* call the handler.
* @protected
*/
FieldImage.prototype.showEditor_ = function() {
if (this.clickHandler_) {
this.clickHandler_(this);
}
};
/**
* Set the function that is called when this image is clicked.
* @param {?function(!FieldImage)} func The function that is called
* when the image is clicked, or null to remove.
*/
FieldImage.prototype.setOnClickHandler = function(func) {
this.clickHandler_ = func;
};
/**
* Use the `getText_` developer hook to override the field's text
* representation.
* Return the image alt text instead.
* @return {?string} The image alt text.
* @protected
* @override
*/
FieldImage.prototype.getText_ = function() {
return this.altText_;
};
fieldRegistry.register('field_image', FieldImage);
exports.FieldImage = FieldImage;

View File

@@ -19,39 +19,123 @@ goog.module('Blockly.FieldLabel');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
const {Field} = goog.require('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
/**
* Class for a non-editable, non-serializable text field.
* @param {string=} opt_value The initial value of the field. Should cast to a
* string. Defaults to an empty string if null or undefined.
* @param {string=} opt_class Optional CSS class for the field's text.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label#creation}
* for a list of properties this parameter supports.
* @extends {Field}
* @constructor
* @alias Blockly.FieldLabel
*/
const FieldLabel = function(opt_value, opt_class, opt_config) {
class FieldLabel extends Field {
/**
* The html class name to use for this field.
* @type {?string}
* @private
* @param {(string|!Sentinel)=} opt_value The initial value of the
* field. Should cast to a string. Defaults to an empty string if null or
* undefined.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {string=} opt_class Optional CSS class for the field's text.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label#creation}
* for a list of properties this parameter supports.
* @alias Blockly.FieldLabel
*/
this.class_ = null;
constructor(opt_value, opt_class, opt_config) {
super(Field.SKIP_SETUP);
FieldLabel.superClass_.constructor.call(this, opt_value, null, opt_config);
/**
* The html class name to use for this field.
* @type {?string}
* @private
*/
this.class_ = null;
if (!opt_config) { // If the config was not passed use old configuration.
this.class_ = opt_class || null;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
this.EDITABLE = false;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) {
this.configure_(opt_config);
} else {
this.class_ = opt_class || null;
}
this.setValue(opt_value);
}
};
object.inherits(FieldLabel, Field);
/**
* @override
*/
configure_(config) {
super.configure_(config);
this.class_ = config['class'];
}
/**
* Create block UI for this label.
* @package
*/
initView() {
this.createTextElement_();
if (this.class_) {
dom.addClass(
/** @type {!SVGTextElement} */ (this.textElement_), this.class_);
}
}
/**
* Ensure that the input value casts to a valid string.
* @param {*=} opt_newValue The input value.
* @return {?string} A valid string, or null if invalid.
* @protected
*/
doClassValidation_(opt_newValue) {
if (opt_newValue === null || opt_newValue === undefined) {
return null;
}
return String(opt_newValue);
}
/**
* Set the CSS class applied to the field's textElement_.
* @param {?string} cssClass The new CSS class name, or null to remove.
*/
setClass(cssClass) {
if (this.textElement_) {
// This check isn't necessary, but it's faster than letting removeClass
// figure it out.
if (this.class_) {
dom.removeClass(this.textElement_, this.class_);
}
if (cssClass) {
dom.addClass(this.textElement_, cssClass);
}
}
this.class_ = cssClass;
}
/**
* Construct a FieldLabel from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @return {!FieldLabel} The new field instance.
* @package
* @nocollapse
*/
static fromJson(options) {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldLabel if that class doesn't override
// the static fromJson method.
return new this(text, undefined, options);
}
}
/**
* The default value for this field.
@@ -60,79 +144,6 @@ object.inherits(FieldLabel, Field);
*/
FieldLabel.prototype.DEFAULT_VALUE = '';
/**
* Construct a FieldLabel from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @return {!FieldLabel} The new field instance.
* @package
* @nocollapse
*/
FieldLabel.fromJson = function(options) {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldLabel if that class doesn't override
// the static fromJson method.
return new this(text, undefined, options);
};
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
FieldLabel.prototype.EDITABLE = false;
/**
* @override
*/
FieldLabel.prototype.configure_ = function(config) {
FieldLabel.superClass_.configure_.call(this, config);
this.class_ = config['class'];
};
/**
* Create block UI for this label.
* @package
*/
FieldLabel.prototype.initView = function() {
this.createTextElement_();
if (this.class_) {
dom.addClass(
/** @type {!SVGTextElement} */ (this.textElement_), this.class_);
}
};
/**
* Ensure that the input value casts to a valid string.
* @param {*=} opt_newValue The input value.
* @return {?string} A valid string, or null if invalid.
* @protected
*/
FieldLabel.prototype.doClassValidation_ = function(opt_newValue) {
if (opt_newValue === null || opt_newValue === undefined) {
return null;
}
return String(opt_newValue);
};
/**
* Set the CSS class applied to the field's textElement_.
* @param {?string} cssClass The new CSS class name, or null to remove.
*/
FieldLabel.prototype.setClass = function(cssClass) {
if (this.textElement_) {
// This check isn't necessary, but it's faster than letting removeClass
// figure it out.
if (this.class_) {
dom.removeClass(this.textElement_, this.class_);
}
if (cssClass) {
dom.addClass(this.textElement_, cssClass);
}
}
this.class_ = cssClass;
};
fieldRegistry.register('field_label', FieldLabel);
exports.FieldLabel = FieldLabel;

View File

@@ -30,19 +30,17 @@ const {FieldLabel} = goog.require('Blockly.FieldLabel');
*/
class FieldLabelSerializable extends FieldLabel {
/**
* @param {*} opt_value The initial value of the field. Should cast to a
* @param {string=} opt_value The initial value of the field. Should cast to a
* string. Defaults to an empty string if null or undefined.
* @param {string=} opt_class Optional CSS class for the field's text.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label-serializable#creation}
* for a list of properties this parameter supports.
*
* @alias Blockly.FieldLabelSerializable
*/
constructor(opt_value, opt_class, opt_config) {
const stringValue = opt_value == undefined ? '' : String(opt_value);
super(stringValue, opt_class, opt_config);
super(String(opt_value ?? ''), opt_class, opt_config);
/**
* Editable fields usually show some sort of UI indicating they are

View File

@@ -25,6 +25,8 @@ const userAgent = goog.require('Blockly.utils.userAgent');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {Field} = goog.require('Blockly.Field');
const {KeyCodes} = goog.require('Blockly.utils.KeyCodes');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Svg} = goog.require('Blockly.utils.Svg');
@@ -34,22 +36,24 @@ const {Svg} = goog.require('Blockly.utils.Svg');
*/
class FieldMultilineInput extends FieldTextInput {
/**
* @param {string=} opt_value The initial content of the field. Should cast to
* a
* string. Defaults to an empty string if null or undefined.
* @param {(string|!Sentinel)=} opt_value The initial content of the
* field. Should cast to a string. Defaults to an empty string if null or
* undefined.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns either the accepted text, a replacement
* text, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
* for a list of properties this parameter supports.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
* for a list of properties this parameter supports.
* @alias Blockly.FieldMultilineInput
*/
constructor(opt_value, opt_validator, opt_config) {
const stringValue = opt_value == undefined ? '' : String(opt_value);
super(stringValue, opt_validator, opt_config);
super(Field.SKIP_SETUP);
/**
* The SVG group element that will contain a text element for each text row
@@ -73,17 +77,10 @@ class FieldMultilineInput extends FieldTextInput {
*/
this.isOverflowedY_ = false;
/**
* @type {boolean}
* @private
*/
this.isBeingEdited_ = false;
/**
* @type {boolean}
* @private
*/
this.isTextValid_ = false;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
}
/**

View File

@@ -17,67 +17,316 @@ goog.module('Blockly.FieldNumber');
const aria = goog.require('Blockly.utils.aria');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const object = goog.require('Blockly.utils.object');
const {Field} = goog.require('Blockly.Field');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
/**
* Class for an editable number field.
* @param {string|number=} opt_value The initial value of the field. Should cast
* to a number. Defaults to 0.
* @param {?(string|number)=} opt_min Minimum value.
* @param {?(string|number)=} opt_max Maximum value.
* @param {?(string|number)=} opt_precision Precision for value.
* @param {?Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a validated
* number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/number#creation}
* for a list of properties this parameter supports.
* @extends {FieldTextInput}
* @constructor
* @alias Blockly.FieldNumber
*/
const FieldNumber = function(
opt_value, opt_min, opt_max, opt_precision, opt_validator, opt_config) {
class FieldNumber extends FieldTextInput {
/**
* The minimum value this number field can contain.
* @type {number}
* @protected
* @param {(string|number|!Sentinel)=} opt_value The initial value of
* the field. Should cast to a number. Defaults to 0.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {?(string|number)=} opt_min Minimum value. Will only be used if
* opt_config is not provided.
* @param {?(string|number)=} opt_max Maximum value. Will only be used if
* opt_config is not provided.
* @param {?(string|number)=} opt_precision Precision for value. Will only be
* used if opt_config is not provided.
* @param {?Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a validated
* number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/number#creation}
* for a list of properties this parameter supports.
* @alias Blockly.FieldNumber
*/
this.min_ = -Infinity;
constructor(
opt_value, opt_min, opt_max, opt_precision, opt_validator, opt_config) {
// Pass SENTINEL so that we can define properties before value validation.
super(Field.SKIP_SETUP);
/**
* The minimum value this number field can contain.
* @type {number}
* @protected
*/
this.min_ = -Infinity;
/**
* The maximum value this number field can contain.
* @type {number}
* @protected
*/
this.max_ = Infinity;
/**
* The multiple to which this fields value is rounded.
* @type {number}
* @protected
*/
this.precision_ = 0;
/**
* The number of decimal places to allow, or null to allow any number of
* decimal digits.
* @type {?number}
* @private
*/
this.decimalPlaces_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) {
this.configure_(opt_config);
} else {
this.setConstraints(opt_min, opt_max, opt_precision);
}
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
}
/**
* The maximum value this number field can contain.
* @type {number}
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
this.max_ = Infinity;
configure_(config) {
super.configure_(config);
this.setMinInternal_(config['min']);
this.setMaxInternal_(config['max']);
this.setPrecisionInternal_(config['precision']);
}
/**
* The multiple to which this fields value is rounded.
* @type {number}
* @protected
* Set the maximum, minimum and precision constraints on this field.
* Any of these properties may be undefined or NaN to be disabled.
* Setting precision (usually a power of 10) enforces a minimum step between
* values. That is, the user's value will rounded to the closest multiple of
* precision. The least significant digit place is inferred from the
* precision. Integers values can be enforces by choosing an integer
* precision.
* @param {?(number|string|undefined)} min Minimum value.
* @param {?(number|string|undefined)} max Maximum value.
* @param {?(number|string|undefined)} precision Precision for value.
*/
this.precision_ = 0;
setConstraints(min, max, precision) {
this.setMinInternal_(min);
this.setMaxInternal_(max);
this.setPrecisionInternal_(precision);
this.setValue(this.getValue());
}
/**
* The number of decimal places to allow, or null to allow any number of
* decimal digits.
* @type {?number}
* Sets the minimum value this field can contain. Updates the value to
* reflect.
* @param {?(number|string|undefined)} min Minimum value.
*/
setMin(min) {
this.setMinInternal_(min);
this.setValue(this.getValue());
}
/**
* Sets the minimum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} min Minimum value.
* @private
*/
this.decimalPlaces_ = null;
FieldNumber.superClass_.constructor.call(
this, opt_value, opt_validator, opt_config);
if (!opt_config) { // Only do one kind of configuration or the other.
this.setConstraints(opt_min, opt_max, opt_precision);
setMinInternal_(min) {
if (min == null) {
this.min_ = -Infinity;
} else {
min = Number(min);
if (!isNaN(min)) {
this.min_ = min;
}
}
}
};
object.inherits(FieldNumber, FieldTextInput);
/**
* Returns the current minimum value this field can contain. Default is
* -Infinity.
* @return {number} The current minimum value this field can contain.
*/
getMin() {
return this.min_;
}
/**
* Sets the maximum value this field can contain. Updates the value to
* reflect.
* @param {?(number|string|undefined)} max Maximum value.
*/
setMax(max) {
this.setMaxInternal_(max);
this.setValue(this.getValue());
}
/**
* Sets the maximum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} max Maximum value.
* @private
*/
setMaxInternal_(max) {
if (max == null) {
this.max_ = Infinity;
} else {
max = Number(max);
if (!isNaN(max)) {
this.max_ = max;
}
}
}
/**
* Returns the current maximum value this field can contain. Default is
* Infinity.
* @return {number} The current maximum value this field can contain.
*/
getMax() {
return this.max_;
}
/**
* Sets the precision of this field's value, i.e. the number to which the
* value is rounded. Updates the field to reflect.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
*/
setPrecision(precision) {
this.setPrecisionInternal_(precision);
this.setValue(this.getValue());
}
/**
* Sets the precision of this field's value. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
* @private
*/
setPrecisionInternal_(precision) {
this.precision_ = Number(precision) || 0;
let precisionString = String(this.precision_);
if (precisionString.indexOf('e') !== -1) {
// String() is fast. But it turns .0000001 into '1e-7'.
// Use the much slower toLocaleString to access all the digits.
precisionString =
this.precision_.toLocaleString('en-US', {maximumFractionDigits: 20});
}
const decimalIndex = precisionString.indexOf('.');
if (decimalIndex === -1) {
// If the precision is 0 (float) allow any number of decimals,
// otherwise allow none.
this.decimalPlaces_ = precision ? 0 : null;
} else {
this.decimalPlaces_ = precisionString.length - decimalIndex - 1;
}
}
/**
* Returns the current precision of this field. The precision being the
* number to which the field's value is rounded. A precision of 0 means that
* the value is not rounded.
* @return {number} The number to which this field's value is rounded.
*/
getPrecision() {
return this.precision_;
}
/**
* Ensure that the input value is a valid number (must fulfill the
* constraints placed on the field).
* @param {*=} opt_newValue The input value.
* @return {?number} A valid number, or null if invalid.
* @protected
* @override
*/
doClassValidation_(opt_newValue) {
if (opt_newValue === null) {
return null;
}
// Clean up text.
let newValue = String(opt_newValue);
// TODO: Handle cases like 'ten', '1.203,14', etc.
// 'O' is sometimes mistaken for '0' by inexperienced users.
newValue = newValue.replace(/O/ig, '0');
// Strip out thousands separators.
newValue = newValue.replace(/,/g, '');
// Ignore case of 'Infinity'.
newValue = newValue.replace(/infinity/i, 'Infinity');
// Clean up number.
let n = Number(newValue || 0);
if (isNaN(n)) {
// Invalid number.
return null;
}
// Get the value in range.
n = Math.min(Math.max(n, this.min_), this.max_);
// Round to nearest multiple of precision.
if (this.precision_ && isFinite(n)) {
n = Math.round(n / this.precision_) * this.precision_;
}
// Clean up floating point errors.
if (this.decimalPlaces_ !== null) {
n = Number(n.toFixed(this.decimalPlaces_));
}
return n;
}
/**
* Create the number input editor widget.
* @return {!HTMLElement} The newly created number input editor.
* @protected
* @override
*/
widgetCreate_() {
const htmlInput = super.widgetCreate_();
// Set the accessibility state
if (this.min_ > -Infinity) {
aria.setState(htmlInput, aria.State.VALUEMIN, this.min_);
}
if (this.max_ < Infinity) {
aria.setState(htmlInput, aria.State.VALUEMAX, this.max_);
}
return htmlInput;
}
/**
* Construct a FieldNumber from a JSON arg object.
* @param {!Object} options A JSON object with options (value, min, max, and
* precision).
* @return {!FieldNumber} The new field instance.
* @package
* @nocollapse
* @override
*/
static fromJson(options) {
// `this` might be a subclass of FieldNumber if that class doesn't override
// the static fromJson method.
return new this(
options['value'], undefined, undefined, undefined, undefined, options);
}
}
/**
* The default value for this field.
@@ -86,236 +335,6 @@ object.inherits(FieldNumber, FieldTextInput);
*/
FieldNumber.prototype.DEFAULT_VALUE = 0;
/**
* Construct a FieldNumber from a JSON arg object.
* @param {!Object} options A JSON object with options (value, min, max, and
* precision).
* @return {!FieldNumber} The new field instance.
* @package
* @nocollapse
*/
FieldNumber.fromJson = function(options) {
// `this` might be a subclass of FieldNumber if that class doesn't override
// the static fromJson method.
return new this(
options['value'], undefined, undefined, undefined, undefined, options);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
FieldNumber.prototype.SERIALIZABLE = true;
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
*/
FieldNumber.prototype.configure_ = function(config) {
FieldNumber.superClass_.configure_.call(this, config);
this.setMinInternal_(config['min']);
this.setMaxInternal_(config['max']);
this.setPrecisionInternal_(config['precision']);
};
/**
* Set the maximum, minimum and precision constraints on this field.
* Any of these properties may be undefined or NaN to be disabled.
* Setting precision (usually a power of 10) enforces a minimum step between
* values. That is, the user's value will rounded to the closest multiple of
* precision. The least significant digit place is inferred from the precision.
* Integers values can be enforces by choosing an integer precision.
* @param {?(number|string|undefined)} min Minimum value.
* @param {?(number|string|undefined)} max Maximum value.
* @param {?(number|string|undefined)} precision Precision for value.
*/
FieldNumber.prototype.setConstraints = function(min, max, precision) {
this.setMinInternal_(min);
this.setMaxInternal_(max);
this.setPrecisionInternal_(precision);
this.setValue(this.getValue());
};
/**
* Sets the minimum value this field can contain. Updates the value to reflect.
* @param {?(number|string|undefined)} min Minimum value.
*/
FieldNumber.prototype.setMin = function(min) {
this.setMinInternal_(min);
this.setValue(this.getValue());
};
/**
* Sets the minimum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} min Minimum value.
* @private
*/
FieldNumber.prototype.setMinInternal_ = function(min) {
if (min == null) {
this.min_ = -Infinity;
} else {
min = Number(min);
if (!isNaN(min)) {
this.min_ = min;
}
}
};
/**
* Returns the current minimum value this field can contain. Default is
* -Infinity.
* @return {number} The current minimum value this field can contain.
*/
FieldNumber.prototype.getMin = function() {
return this.min_;
};
/**
* Sets the maximum value this field can contain. Updates the value to reflect.
* @param {?(number|string|undefined)} max Maximum value.
*/
FieldNumber.prototype.setMax = function(max) {
this.setMaxInternal_(max);
this.setValue(this.getValue());
};
/**
* Sets the maximum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} max Maximum value.
* @private
*/
FieldNumber.prototype.setMaxInternal_ = function(max) {
if (max == null) {
this.max_ = Infinity;
} else {
max = Number(max);
if (!isNaN(max)) {
this.max_ = max;
}
}
};
/**
* Returns the current maximum value this field can contain. Default is
* Infinity.
* @return {number} The current maximum value this field can contain.
*/
FieldNumber.prototype.getMax = function() {
return this.max_;
};
/**
* Sets the precision of this field's value, i.e. the number to which the
* value is rounded. Updates the field to reflect.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
*/
FieldNumber.prototype.setPrecision = function(precision) {
this.setPrecisionInternal_(precision);
this.setValue(this.getValue());
};
/**
* Sets the precision of this field's value. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
* @private
*/
FieldNumber.prototype.setPrecisionInternal_ = function(precision) {
this.precision_ = Number(precision) || 0;
let precisionString = String(this.precision_);
if (precisionString.indexOf('e') !== -1) {
// String() is fast. But it turns .0000001 into '1e-7'.
// Use the much slower toLocaleString to access all the digits.
precisionString =
this.precision_.toLocaleString('en-US', {maximumFractionDigits: 20});
}
const decimalIndex = precisionString.indexOf('.');
if (decimalIndex === -1) {
// If the precision is 0 (float) allow any number of decimals,
// otherwise allow none.
this.decimalPlaces_ = precision ? 0 : null;
} else {
this.decimalPlaces_ = precisionString.length - decimalIndex - 1;
}
};
/**
* Returns the current precision of this field. The precision being the
* number to which the field's value is rounded. A precision of 0 means that
* the value is not rounded.
* @return {number} The number to which this field's value is rounded.
*/
FieldNumber.prototype.getPrecision = function() {
return this.precision_;
};
/**
* Ensure that the input value is a valid number (must fulfill the
* constraints placed on the field).
* @param {*=} opt_newValue The input value.
* @return {?number} A valid number, or null if invalid.
* @protected
* @override
*/
FieldNumber.prototype.doClassValidation_ = function(opt_newValue) {
if (opt_newValue === null) {
return null;
}
// Clean up text.
let newValue = String(opt_newValue);
// TODO: Handle cases like 'ten', '1.203,14', etc.
// 'O' is sometimes mistaken for '0' by inexperienced users.
newValue = newValue.replace(/O/ig, '0');
// Strip out thousands separators.
newValue = newValue.replace(/,/g, '');
// Ignore case of 'Infinity'.
newValue = newValue.replace(/infinity/i, 'Infinity');
// Clean up number.
let n = Number(newValue || 0);
if (isNaN(n)) {
// Invalid number.
return null;
}
// Get the value in range.
n = Math.min(Math.max(n, this.min_), this.max_);
// Round to nearest multiple of precision.
if (this.precision_ && isFinite(n)) {
n = Math.round(n / this.precision_) * this.precision_;
}
// Clean up floating point errors.
if (this.decimalPlaces_ !== null) {
n = Number(n.toFixed(this.decimalPlaces_));
}
return n;
};
/**
* Create the number input editor widget.
* @return {!HTMLElement} The newly created number input editor.
* @protected
* @override
*/
FieldNumber.prototype.widgetCreate_ = function() {
const htmlInput = FieldNumber.superClass_.widgetCreate_.call(this);
// Set the accessibility state
if (this.min_ > -Infinity) {
aria.setState(htmlInput, aria.State.VALUEMIN, this.min_);
}
if (this.max_ < Infinity) {
aria.setState(htmlInput, aria.State.VALUEMAX, this.max_);
}
return htmlInput;
};
fieldRegistry.register('field_number', FieldNumber);
exports.FieldNumber = FieldNumber;

File diff suppressed because it is too large Load Diff

View File

@@ -19,16 +19,18 @@ const Variables = goog.require('Blockly.Variables');
const Xml = goog.require('Blockly.Xml');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const internalConstants = goog.require('Blockly.internalConstants');
const object = goog.require('Blockly.utils.object');
const parsing = goog.require('Blockly.utils.parsing');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {Field} = goog.require('Blockly.Field');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
/* eslint-disable-next-line no-unused-vars */
const {MenuItem} = goog.requireType('Blockly.MenuItem');
/* eslint-disable-next-line no-unused-vars */
const {Menu} = goog.requireType('Blockly.Menu');
const {Msg} = goog.require('Blockly.Msg');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Size} = goog.require('Blockly.utils.Size');
const {VariableModel} = goog.require('Blockly.VariableModel');
/** @suppress {extraRequire} */
@@ -37,483 +39,519 @@ goog.require('Blockly.Events.BlockChange');
/**
* Class for a variable's dropdown field.
* @param {?string} varName The default name for the variable. If null,
* a unique variable name will be generated.
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a variable ID & returns a
* validated variable ID, or null to abort the change.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown.
* @param {string=} opt_defaultType The type of variable to create if this
* field's value is not explicitly set. Defaults to ''.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/variable#creation}
* for a list of properties this parameter supports.
* @extends {FieldDropdown}
* @constructor
* @alias Blockly.FieldVariable
*/
const FieldVariable = function(
varName, opt_validator, opt_variableTypes, opt_defaultType, opt_config) {
// The FieldDropdown constructor expects the field's initial value to be
// the first entry in the menu generator, which it may or may not be.
// Just do the relevant parts of the constructor.
class FieldVariable extends FieldDropdown {
/**
* @param {?string|!Sentinel} varName The default name for the variable.
* If null, a unique variable name will be generated.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a variable ID & returns a
* validated variable ID, or null to abort the change.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown. Will only be used if opt_config is not
* provided.
* @param {string=} opt_defaultType The type of variable to create if this
* field's value is not explicitly set. Defaults to ''. Will only be used
* if opt_config is not provided.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/variable#creation}
* for a list of properties this parameter supports.
* @alias Blockly.FieldVariable
*/
constructor(
varName, opt_validator, opt_variableTypes, opt_defaultType, opt_config) {
super(Field.SKIP_SETUP);
/**
* An array of options for a dropdown list,
* or a function which generates these options.
* @type {(!Array<!Array>|
* !function(this:FieldDropdown): !Array<!Array>)}
* @protected
*/
this.menuGenerator_ = FieldVariable.dropdownCreate;
/**
* The initial variable name passed to this field's constructor, or an
* empty string if a name wasn't provided. Used to create the initial
* variable.
* @type {string}
*/
this.defaultVariableName = typeof varName === 'string' ? varName : '';
/**
* The type of the default variable for this field.
* @type {string}
* @private
*/
this.defaultType_ = '';
/**
* All of the types of variables that will be available in this field's
* dropdown.
* @type {?Array<string>}
*/
this.variableTypes = [];
/**
* The size of the area rendered by the field.
* @type {Size}
* @protected
* @override
*/
this.size_ = new Size(0, 0);
/**
* The variable model associated with this field.
* @type {?VariableModel}
* @private
*/
this.variable_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
if (varName === Field.SKIP_SETUP) return;
if (opt_config) {
this.configure_(opt_config);
} else {
this.setTypes_(opt_variableTypes, opt_defaultType);
}
if (opt_validator) this.setValidator(opt_validator);
}
/**
* An array of options for a dropdown list,
* or a function which generates these options.
* @type {(!Array<!Array>|
* !function(this:FieldDropdown): !Array<!Array>)}
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
*/
this.menuGenerator_ = FieldVariable.dropdownCreate;
configure_(config) {
super.configure_(config);
this.setTypes_(config['variableTypes'], config['defaultType']);
}
/**
* The initial variable name passed to this field's constructor, or an
* empty string if a name wasn't provided. Used to create the initial
* variable.
* @type {string}
* Initialize the model for this field if it has not already been initialized.
* If the value has not been set to a variable by the first render, we make up
* a variable rather than let the value be invalid.
* @package
*/
this.defaultVariableName = typeof varName === 'string' ? varName : '';
initModel() {
if (this.variable_) {
return; // Initialization already happened.
}
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, null, this.defaultVariableName,
this.defaultType_);
// Don't call setValue because we don't want to cause a rerender.
this.doValueUpdate_(variable.getId());
}
/**
* The size of the area rendered by the field.
* @type {Size}
* @protected
* @override
*/
this.size_ = new Size(0, 0);
opt_config && this.configure_(opt_config);
opt_validator && this.setValidator(opt_validator);
if (!opt_config) { // Only do one kind of configuration or the other.
this.setTypes_(opt_variableTypes, opt_defaultType);
}
};
object.inherits(FieldVariable, FieldDropdown);
/**
* Construct a FieldVariable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (variable,
* variableTypes, and defaultType).
* @return {!FieldVariable} The new field instance.
* @package
* @nocollapse
*/
FieldVariable.fromJson = function(options) {
const varName = parsing.replaceMessageReferences(options['variable']);
// `this` might be a subclass of FieldVariable if that class doesn't override
// the static fromJson method.
return new this(varName, undefined, undefined, undefined, options);
};
/**
* Serializable fields are saved by the XML renderer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
FieldVariable.prototype.SERIALIZABLE = true;
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
*/
FieldVariable.prototype.configure_ = function(config) {
FieldVariable.superClass_.configure_.call(this, config);
this.setTypes_(config['variableTypes'], config['defaultType']);
};
/**
* Initialize the model for this field if it has not already been initialized.
* If the value has not been set to a variable by the first render, we make up a
* variable rather than let the value be invalid.
* @package
*/
FieldVariable.prototype.initModel = function() {
if (this.variable_) {
return; // Initialization already happened.
}
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, null, this.defaultVariableName,
this.defaultType_);
// Don't call setValue because we don't want to cause a rerender.
this.doValueUpdate_(variable.getId());
};
/**
* @override
*/
FieldVariable.prototype.shouldAddBorderRect_ = function() {
return FieldVariable.superClass_.shouldAddBorderRect_.call(this) &&
(!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.sourceBlock_.type !== 'variables_get');
};
/**
* Initialize this field based on the given XML.
* @param {!Element} fieldElement The element containing information about the
* variable field's state.
*/
FieldVariable.prototype.fromXml = function(fieldElement) {
const id = fieldElement.getAttribute('id');
const variableName = fieldElement.textContent;
// 'variabletype' should be lowercase, but until July 2019 it was sometimes
// recorded as 'variableType'. Thus we need to check for both.
const variableType = fieldElement.getAttribute('variabletype') ||
fieldElement.getAttribute('variableType') || '';
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, id, variableName, variableType);
// This should never happen :)
if (variableType !== null && variableType !== variable.type) {
throw Error(
'Serialized variable type with id \'' + variable.getId() +
'\' had type ' + variable.type + ', and ' +
'does not match variable field that references it: ' +
Xml.domToText(fieldElement) + '.');
shouldAddBorderRect_() {
return super.shouldAddBorderRect_() &&
(!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.sourceBlock_.type !== 'variables_get');
}
this.setValue(variable.getId());
};
/**
* Initialize this field based on the given XML.
* @param {!Element} fieldElement The element containing information about the
* variable field's state.
*/
fromXml(fieldElement) {
const id = fieldElement.getAttribute('id');
const variableName = fieldElement.textContent;
// 'variabletype' should be lowercase, but until July 2019 it was sometimes
// recorded as 'variableType'. Thus we need to check for both.
const variableType = fieldElement.getAttribute('variabletype') ||
fieldElement.getAttribute('variableType') || '';
/**
* Serialize this field to XML.
* @param {!Element} fieldElement The element to populate with info about the
* field's state.
* @return {!Element} The element containing info about the field's state.
*/
FieldVariable.prototype.toXml = function(fieldElement) {
// Make sure the variable is initialized.
this.initModel();
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, id, variableName, variableType);
fieldElement.id = this.variable_.getId();
fieldElement.textContent = this.variable_.name;
if (this.variable_.type) {
fieldElement.setAttribute('variabletype', this.variable_.type);
}
return fieldElement;
};
/**
* Saves this field's value.
* @param {boolean=} doFullSerialization If true, the variable field will
* serialize the full state of the field being referenced (ie ID, name,
* and type) rather than just a reference to it (ie ID).
* @return {*} The state of the variable field.
* @override
* @package
*/
FieldVariable.prototype.saveState = function(doFullSerialization) {
const legacyState = this.saveLegacyState(FieldVariable);
if (legacyState !== null) {
return legacyState;
}
// Make sure the variable is initialized.
this.initModel();
const state = {'id': this.variable_.getId()};
if (doFullSerialization) {
state['name'] = this.variable_.name;
state['type'] = this.variable_.type;
}
return state;
};
/**
* Sets the field's value based on the given state.
* @param {*} state The state of the variable to assign to this variable field.
* @override
* @package
*/
FieldVariable.prototype.loadState = function(state) {
if (this.loadLegacyState(FieldVariable, state)) {
return;
}
// This is necessary so that blocks in the flyout can have custom var names.
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, state['id'] || null, state['name'],
state['type'] || '');
this.setValue(variable.getId());
};
/**
* Attach this field to a block.
* @param {!Block} block The block containing this field.
*/
FieldVariable.prototype.setSourceBlock = function(block) {
if (block.isShadow()) {
throw Error('Variable fields are not allowed to exist on shadow blocks.');
}
FieldVariable.superClass_.setSourceBlock.call(this, block);
};
/**
* Get the variable's ID.
* @return {string} Current variable's ID.
*/
FieldVariable.prototype.getValue = function() {
return this.variable_ ? this.variable_.getId() : null;
};
/**
* Get the text from this field, which is the selected variable's name.
* @return {string} The selected variable's name, or the empty string if no
* variable is selected.
*/
FieldVariable.prototype.getText = function() {
return this.variable_ ? this.variable_.name : '';
};
/**
* Get the variable model for the selected variable.
* Not guaranteed to be in the variable map on the workspace (e.g. if accessed
* after the variable has been deleted).
* @return {?VariableModel} The selected variable, or null if none was
* selected.
* @package
*/
FieldVariable.prototype.getVariable = function() {
return this.variable_;
};
/**
* Gets the validation function for this field, or null if not set.
* Returns null if the variable is not set, because validators should not
* run on the initial setValue call, because the field won't be attached to
* a block and workspace at that point.
* @return {?Function} Validation function, or null.
*/
FieldVariable.prototype.getValidator = function() {
// Validators shouldn't operate on the initial setValue call.
// Normally this is achieved by calling setValidator after setValue, but
// this is not a possibility with variable fields.
if (this.variable_) {
return this.validator_;
}
return null;
};
/**
* Ensure that the ID belongs to a valid variable of an allowed type.
* @param {*=} opt_newValue The ID of the new variable to set.
* @return {?string} The validated ID, or null if invalid.
* @protected
*/
FieldVariable.prototype.doClassValidation_ = function(opt_newValue) {
if (opt_newValue === null) {
return null;
}
const newId = /** @type {string} */ (opt_newValue);
const variable = Variables.getVariable(this.sourceBlock_.workspace, newId);
if (!variable) {
console.warn(
'Variable id doesn\'t point to a real variable! ' +
'ID was ' + newId);
return null;
}
// Type Checks.
const type = variable.type;
if (!this.typeIsAllowed_(type)) {
console.warn('Variable type doesn\'t match this field! Type was ' + type);
return null;
}
return newId;
};
/**
* Update the value of this variable field, as well as its variable and text.
*
* The variable ID should be valid at this point, but if a variable field
* validator returns a bad ID, this could break.
* @param {*} newId The value to be saved.
* @protected
*/
FieldVariable.prototype.doValueUpdate_ = function(newId) {
this.variable_ = Variables.getVariable(
this.sourceBlock_.workspace, /** @type {string} */ (newId));
FieldVariable.superClass_.doValueUpdate_.call(this, newId);
};
/**
* Check whether the given variable type is allowed on this field.
* @param {string} type The type to check.
* @return {boolean} True if the type is in the list of allowed types.
* @private
*/
FieldVariable.prototype.typeIsAllowed_ = function(type) {
const typeList = this.getVariableTypes_();
if (!typeList) {
return true; // If it's null, all types are valid.
}
for (let i = 0; i < typeList.length; i++) {
if (type === typeList[i]) {
return true;
// This should never happen :)
if (variableType !== null && variableType !== variable.type) {
throw Error(
'Serialized variable type with id \'' + variable.getId() +
'\' had type ' + variable.type + ', and ' +
'does not match variable field that references it: ' +
Xml.domToText(fieldElement) + '.');
}
}
return false;
};
/**
* Return a list of variable types to include in the dropdown.
* @return {!Array<string>} Array of variable types.
* @throws {Error} if variableTypes is an empty array.
* @private
*/
FieldVariable.prototype.getVariableTypes_ = function() {
// TODO (#1513): Try to avoid calling this every time the field is edited.
let variableTypes = this.variableTypes;
if (variableTypes === null) {
// If variableTypes is null, return all variable types.
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
return this.sourceBlock_.workspace.getVariableTypes();
this.setValue(variable.getId());
}
/**
* Serialize this field to XML.
* @param {!Element} fieldElement The element to populate with info about the
* field's state.
* @return {!Element} The element containing info about the field's state.
*/
toXml(fieldElement) {
// Make sure the variable is initialized.
this.initModel();
fieldElement.id = this.variable_.getId();
fieldElement.textContent = this.variable_.name;
if (this.variable_.type) {
fieldElement.setAttribute('variabletype', this.variable_.type);
}
return fieldElement;
}
variableTypes = variableTypes || [''];
if (variableTypes.length === 0) {
// Throw an error if variableTypes is an empty list.
const name = this.getText();
throw Error(
'\'variableTypes\' of field variable ' + name + ' was an empty list');
}
return variableTypes;
};
/**
* Parse the optional arguments representing the allowed variable types and the
* default variable type.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown. If null or undefined, variables of all types
* will be displayed in the dropdown.
* @param {string=} opt_defaultType The type of the variable to create if this
* field's value is not explicitly set. Defaults to ''.
* @private
*/
FieldVariable.prototype.setTypes_ = function(
opt_variableTypes, opt_defaultType) {
// If you expected that the default type would be the same as the only entry
// in the variable types array, tell the Blockly team by commenting on #1499.
const defaultType = opt_defaultType || '';
let variableTypes;
// Set the allowable variable types. Null means all types on the workspace.
if (opt_variableTypes === null || opt_variableTypes === undefined) {
variableTypes = null;
} else if (Array.isArray(opt_variableTypes)) {
variableTypes = opt_variableTypes;
// Make sure the default type is valid.
let isInArray = false;
for (let i = 0; i < variableTypes.length; i++) {
if (variableTypes[i] === defaultType) {
isInArray = true;
/**
* Saves this field's value.
* @param {boolean=} doFullSerialization If true, the variable field will
* serialize the full state of the field being referenced (ie ID, name,
* and type) rather than just a reference to it (ie ID).
* @return {*} The state of the variable field.
* @override
* @package
*/
saveState(doFullSerialization) {
const legacyState = this.saveLegacyState(FieldVariable);
if (legacyState !== null) {
return legacyState;
}
// Make sure the variable is initialized.
this.initModel();
const state = {'id': this.variable_.getId()};
if (doFullSerialization) {
state['name'] = this.variable_.name;
state['type'] = this.variable_.type;
}
return state;
}
/**
* Sets the field's value based on the given state.
* @param {*} state The state of the variable to assign to this variable
* field.
* @override
* @package
*/
loadState(state) {
if (this.loadLegacyState(FieldVariable, state)) {
return;
}
// This is necessary so that blocks in the flyout can have custom var names.
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, state['id'] || null, state['name'],
state['type'] || '');
this.setValue(variable.getId());
}
/**
* Attach this field to a block.
* @param {!Block} block The block containing this field.
*/
setSourceBlock(block) {
if (block.isShadow()) {
throw Error('Variable fields are not allowed to exist on shadow blocks.');
}
super.setSourceBlock(block);
}
/**
* Get the variable's ID.
* @return {?string} Current variable's ID.
*/
getValue() {
return this.variable_ ? this.variable_.getId() : null;
}
/**
* Get the text from this field, which is the selected variable's name.
* @return {string} The selected variable's name, or the empty string if no
* variable is selected.
*/
getText() {
return this.variable_ ? this.variable_.name : '';
}
/**
* Get the variable model for the selected variable.
* Not guaranteed to be in the variable map on the workspace (e.g. if accessed
* after the variable has been deleted).
* @return {?VariableModel} The selected variable, or null if none was
* selected.
* @package
*/
getVariable() {
return this.variable_;
}
/**
* Gets the validation function for this field, or null if not set.
* Returns null if the variable is not set, because validators should not
* run on the initial setValue call, because the field won't be attached to
* a block and workspace at that point.
* @return {?Function} Validation function, or null.
*/
getValidator() {
// Validators shouldn't operate on the initial setValue call.
// Normally this is achieved by calling setValidator after setValue, but
// this is not a possibility with variable fields.
if (this.variable_) {
return this.validator_;
}
return null;
}
/**
* Ensure that the ID belongs to a valid variable of an allowed type.
* @param {*=} opt_newValue The ID of the new variable to set.
* @return {?string} The validated ID, or null if invalid.
* @protected
*/
doClassValidation_(opt_newValue) {
if (opt_newValue === null) {
return null;
}
const newId = /** @type {string} */ (opt_newValue);
const variable = Variables.getVariable(this.sourceBlock_.workspace, newId);
if (!variable) {
console.warn(
'Variable id doesn\'t point to a real variable! ' +
'ID was ' + newId);
return null;
}
// Type Checks.
const type = variable.type;
if (!this.typeIsAllowed_(type)) {
console.warn(
'Variable type doesn\'t match this field! Type was ' + type);
return null;
}
return newId;
}
/**
* Update the value of this variable field, as well as its variable and text.
*
* The variable ID should be valid at this point, but if a variable field
* validator returns a bad ID, this could break.
* @param {*} newId The value to be saved.
* @protected
*/
doValueUpdate_(newId) {
this.variable_ = Variables.getVariable(
this.sourceBlock_.workspace, /** @type {string} */ (newId));
super.doValueUpdate_(newId);
}
/**
* Check whether the given variable type is allowed on this field.
* @param {string} type The type to check.
* @return {boolean} True if the type is in the list of allowed types.
* @private
*/
typeIsAllowed_(type) {
const typeList = this.getVariableTypes_();
if (!typeList) {
return true; // If it's null, all types are valid.
}
for (let i = 0; i < typeList.length; i++) {
if (type === typeList[i]) {
return true;
}
}
if (!isInArray) {
return false;
}
/**
* Return a list of variable types to include in the dropdown.
* @return {!Array<string>} Array of variable types.
* @throws {Error} if variableTypes is an empty array.
* @private
*/
getVariableTypes_() {
// TODO (#1513): Try to avoid calling this every time the field is edited.
let variableTypes = this.variableTypes;
if (variableTypes === null) {
// If variableTypes is null, return all variable types.
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
return this.sourceBlock_.workspace.getVariableTypes();
}
}
variableTypes = variableTypes || [''];
if (variableTypes.length === 0) {
// Throw an error if variableTypes is an empty list.
const name = this.getText();
throw Error(
'Invalid default type \'' + defaultType + '\' in ' +
'the definition of a FieldVariable');
'\'variableTypes\' of field variable ' + name + ' was an empty list');
}
} else {
throw Error(
'\'variableTypes\' was not an array in the definition of ' +
'a FieldVariable');
return variableTypes;
}
// Only update the field once all checks pass.
this.defaultType_ = defaultType;
this.variableTypes = variableTypes;
};
/**
* Refreshes the name of the variable by grabbing the name of the model.
* Used when a variable gets renamed, but the ID stays the same. Should only
* be called by the block.
* @package
*/
FieldVariable.prototype.refreshVariableName = function() {
this.forceRerender();
};
/**
* Return a sorted list of variable names for variable dropdown menus.
* Include a special option at the end for creating a new variable name.
* @return {!Array<!Array>} Array of variable names/id tuples.
* @this {FieldVariable}
*/
FieldVariable.dropdownCreate = function() {
if (!this.variable_) {
throw Error(
'Tried to call dropdownCreate on a variable field with no' +
' variable selected.');
}
const name = this.getText();
let variableModelList = [];
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
const variableTypes = this.getVariableTypes_();
// Get a copy of the list, so that adding rename and new variable options
// doesn't modify the workspace's list.
for (let i = 0; i < variableTypes.length; i++) {
const variableType = variableTypes[i];
const variables =
this.sourceBlock_.workspace.getVariablesOfType(variableType);
variableModelList = variableModelList.concat(variables);
/**
* Parse the optional arguments representing the allowed variable types and
* the default variable type.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown. If null or undefined, variables of all
* types will be displayed in the dropdown.
* @param {string=} opt_defaultType The type of the variable to create if this
* field's value is not explicitly set. Defaults to ''.
* @private
*/
setTypes_(opt_variableTypes, opt_defaultType) {
// If you expected that the default type would be the same as the only entry
// in the variable types array, tell the Blockly team by commenting on
// #1499.
const defaultType = opt_defaultType || '';
let variableTypes;
// Set the allowable variable types. Null means all types on the workspace.
if (opt_variableTypes === null || opt_variableTypes === undefined) {
variableTypes = null;
} else if (Array.isArray(opt_variableTypes)) {
variableTypes = opt_variableTypes;
// Make sure the default type is valid.
let isInArray = false;
for (let i = 0; i < variableTypes.length; i++) {
if (variableTypes[i] === defaultType) {
isInArray = true;
}
}
if (!isInArray) {
throw Error(
'Invalid default type \'' + defaultType + '\' in ' +
'the definition of a FieldVariable');
}
} else {
throw Error(
'\'variableTypes\' was not an array in the definition of ' +
'a FieldVariable');
}
}
variableModelList.sort(VariableModel.compareByName);
const options = [];
for (let i = 0; i < variableModelList.length; i++) {
// Set the UUID as the internal representation of the variable.
options[i] = [variableModelList[i].name, variableModelList[i].getId()];
}
options.push([Msg['RENAME_VARIABLE'], internalConstants.RENAME_VARIABLE_ID]);
if (Msg['DELETE_VARIABLE']) {
options.push([
Msg['DELETE_VARIABLE'].replace('%1', name),
internalConstants.DELETE_VARIABLE_ID,
]);
// Only update the field once all checks pass.
this.defaultType_ = defaultType;
this.variableTypes = variableTypes;
}
return options;
};
/**
* Refreshes the name of the variable by grabbing the name of the model.
* Used when a variable gets renamed, but the ID stays the same. Should only
* be called by the block.
* @override
* @package
*/
refreshVariableName() {
this.forceRerender();
}
/**
* Handle the selection of an item in the variable dropdown menu.
* Special case the 'Rename variable...' and 'Delete variable...' options.
* In the rename case, prompt the user for a new name.
* @param {!Menu} menu The Menu component clicked.
* @param {!MenuItem} menuItem The MenuItem selected within menu.
* @protected
*/
FieldVariable.prototype.onItemSelected_ = function(menu, menuItem) {
const id = menuItem.getValue();
// Handle special cases.
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
if (id === internalConstants.RENAME_VARIABLE_ID) {
// Rename variable.
Variables.renameVariable(this.sourceBlock_.workspace, this.variable_);
return;
} else if (id === internalConstants.DELETE_VARIABLE_ID) {
// Delete variable.
this.sourceBlock_.workspace.deleteVariableById(this.variable_.getId());
return;
/**
* Handle the selection of an item in the variable dropdown menu.
* Special case the 'Rename variable...' and 'Delete variable...' options.
* In the rename case, prompt the user for a new name.
* @param {!Menu} menu The Menu component clicked.
* @param {!MenuItem} menuItem The MenuItem selected within menu.
* @protected
*/
onItemSelected_(menu, menuItem) {
const id = menuItem.getValue();
// Handle special cases.
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
if (id === internalConstants.RENAME_VARIABLE_ID) {
// Rename variable.
Variables.renameVariable(
this.sourceBlock_.workspace,
/** @type {!VariableModel} */ (this.variable_));
return;
} else if (id === internalConstants.DELETE_VARIABLE_ID) {
// Delete variable.
this.sourceBlock_.workspace.deleteVariableById(this.variable_.getId());
return;
}
}
// Handle unspecial case.
this.setValue(id);
}
// Handle unspecial case.
this.setValue(id);
};
/**
* Overrides referencesVariables(), indicating this field refers to a variable.
* @return {boolean} True.
* @package
* @override
*/
FieldVariable.prototype.referencesVariables = function() {
return true;
};
/**
* Overrides referencesVariables(), indicating this field refers to a
* variable.
* @return {boolean} True.
* @package
* @override
*/
referencesVariables() {
return true;
}
/**
* Construct a FieldVariable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (variable,
* variableTypes, and defaultType).
* @return {!FieldVariable} The new field instance.
* @package
* @nocollapse
* @override
*/
static fromJson(options) {
const varName = parsing.replaceMessageReferences(options['variable']);
// `this` might be a subclass of FieldVariable if that class doesn't
// override the static fromJson method.
return new this(varName, undefined, undefined, undefined, options);
}
/**
* Return a sorted list of variable names for variable dropdown menus.
* Include a special option at the end for creating a new variable name.
* @return {!Array<!Array>} Array of variable names/id tuples.
* @this {FieldVariable}
*/
static dropdownCreate() {
if (!this.variable_) {
throw Error(
'Tried to call dropdownCreate on a variable field with no' +
' variable selected.');
}
const name = this.getText();
let variableModelList = [];
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
const variableTypes = this.getVariableTypes_();
// Get a copy of the list, so that adding rename and new variable options
// doesn't modify the workspace's list.
for (let i = 0; i < variableTypes.length; i++) {
const variableType = variableTypes[i];
const variables =
this.sourceBlock_.workspace.getVariablesOfType(variableType);
variableModelList = variableModelList.concat(variables);
}
}
variableModelList.sort(VariableModel.compareByName);
const options = [];
for (let i = 0; i < variableModelList.length; i++) {
// Set the UUID as the internal representation of the variable.
options[i] = [variableModelList[i].name, variableModelList[i].getId()];
}
options.push(
[Msg['RENAME_VARIABLE'], internalConstants.RENAME_VARIABLE_ID]);
if (Msg['DELETE_VARIABLE']) {
options.push([
Msg['DELETE_VARIABLE'].replace('%1', name),
internalConstants.DELETE_VARIABLE_ID,
]);
}
return options;
}
}
fieldRegistry.register('field_variable', FieldVariable);

24
core/utils/sentinel.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview A type used to create flag values (e.g. SKIP_SETUP).
*/
'use strict';
/**
* A type used to create flag values.
* @class
*/
goog.module('Blockly.utils.Sentinel');
/**
* A type used to create flag values.
*/
class Sentinel {}
exports.Sentinel = Sentinel;

View File

@@ -1,6 +1,6 @@
{
"chunk": [
"blockly:259",
"blockly:260",
"blocks:10:blockly",
"all:11:blockly",
"all1:11:blockly",
@@ -21,7 +21,6 @@
"./core/field_number.js",
"./core/field_multilineinput.js",
"./core/field_label_serializable.js",
"./core/field_dropdown.js",
"./core/field_colour.js",
"./core/field_checkbox.js",
"./core/field_angle.js",
@@ -57,8 +56,6 @@
"./core/contextmenu_items.js",
"./core/widgetdiv.js",
"./core/clipboard.js",
"./core/menuitem.js",
"./core/menu.js",
"./core/contextmenu.js",
"./core/utils/global.js",
"./core/utils/useragent.js",
@@ -80,16 +77,23 @@
"./core/utils/math.js",
"./core/utils/array.js",
"./core/workspace.js",
"./core/keyboard_nav/basic_cursor.js",
"./core/keyboard_nav/tab_navigate_cursor.js",
"./core/warning.js",
"./core/comment.js",
"./core/menu.js",
"./core/menuitem.js",
"./core/interfaces/i_registrable_field.js",
"./core/shortcut_registry.js",
"./core/interfaces/i_keyboard_accessible.js",
"./core/events/events_block_drag.js",
"./core/events/events_block_move.js",
"./core/bump_objects.js",
"./core/block_dragger.js",
"./core/workspace_dragger.js",
"./core/interfaces/i_block_dragger.js",
"./core/bubble_dragger.js",
"./core/keyboard_nav/basic_cursor.js",
"./core/keyboard_nav/tab_navigate_cursor.js",
"./core/mutator.js",
"./core/warning.js",
"./core/comment.js",
"./core/events/events_block_move.js",
"./core/events/events_viewport.js",
"./core/events/events_theme_change.js",
"./core/events/events_block_create.js",
@@ -119,10 +123,12 @@
"./core/theme_manager.js",
"./core/scrollbar_pair.js",
"./core/options.js",
"./core/marker_manager.js",
"./core/interfaces/i_bounded_element.js",
"./core/grid.js",
"./core/css.js",
"./core/flyout_button.js",
"./core/keyboard_nav/cursor.js",
"./core/contextmenu_registry.js",
"./core/theme/classic.js",
"./core/blockly_options.js",
@@ -134,7 +140,8 @@
"./core/renderers/zelos/path_object.js",
"./core/renderers/zelos/drawer.js",
"./core/renderers/zelos/renderer.js",
"./core/utils/aria.js",
"./core/utils/keycodes.js",
"./core/dropdowndiv.js",
"./core/field_textinput.js",
"./core/field_image.js",
"./core/renderers/zelos/constants.js",
@@ -145,6 +152,13 @@
"./core/renderers/measurables/spacer_row.js",
"./core/renderers/measurables/round_corner.js",
"./core/renderers/common/path_object.js",
"./core/events/events_marker_move.js",
"./core/keyboard_nav/marker.js",
"./core/interfaces/i_ast_node_location_svg.js",
"./core/interfaces/i_ast_node_location.js",
"./core/interfaces/i_ast_node_location_with_block.js",
"./core/keyboard_nav/ast_node.js",
"./core/renderers/common/marker_svg.js",
"./core/interfaces/i_positionable.js",
"./core/interfaces/i_drag_target.js",
"./core/interfaces/i_delete_area.js",
@@ -166,6 +180,7 @@
"./core/interfaces/i_toolbox.js",
"./core/utils/metrics.js",
"./core/interfaces/i_metrics_manager.js",
"./core/interfaces/i_registrable.js",
"./core/interfaces/i_flyout.js",
"./core/metrics_manager.js",
"./core/interfaces/i_deletable.js",
@@ -181,6 +196,10 @@
"./core/renderers/common/info.js",
"./core/renderers/measurables/field.js",
"./core/renderers/common/debugger.js",
"./core/utils/sentinel.js",
"./core/field_label.js",
"./core/input_types.js",
"./core/input.js",
"./core/renderers/measurables/input_connection.js",
"./core/renderers/measurables/in_row_spacer.js",
"./core/renderers/measurables/row.js",
@@ -188,42 +207,23 @@
"./core/renderers/measurables/base.js",
"./core/renderers/measurables/connection.js",
"./core/renderers/measurables/next_connection.js",
"./core/renderers/measurables/bottom_row.js",
"./core/renderers/common/debug.js",
"./core/renderers/common/block_rendering.js",
"./core/variables_dynamic.js",
"./core/events/events_var_rename.js",
"./core/events/events_var_delete.js",
"./core/variable_map.js",
"./core/names.js",
"./core/events/events_marker_move.js",
"./core/renderers/common/marker_svg.js",
"./core/keyboard_nav/marker.js",
"./core/keyboard_nav/ast_node.js",
"./core/keyboard_nav/cursor.js",
"./core/marker_manager.js",
"./core/field_label.js",
"./core/input_types.js",
"./core/interfaces/i_registrable_field.js",
"./core/field_registry.js",
"./core/input.js",
"./core/interfaces/i_registrable.js",
"./core/utils/keycodes.js",
"./core/shortcut_registry.js",
"./core/interfaces/i_keyboard_accessible.js",
"./core/interfaces/i_ast_node_location_with_block.js",
"./core/interfaces/i_ast_node_location.js",
"./core/interfaces/i_ast_node_location_svg.js",
"./core/dropdowndiv.js",
"./core/theme.js",
"./core/constants.js",
"./core/interfaces/i_connection_checker.js",
"./core/connection_db.js",
"./core/config.js",
"./core/rendered_connection.js",
"./core/utils/svg_paths.js",
"./core/renderers/common/constants.js",
"./core/field.js",
"./core/renderers/measurables/bottom_row.js",
"./core/renderers/common/debug.js",
"./core/renderers/common/block_rendering.js",
"./core/variables_dynamic.js",
"./core/events/events_block_base.js",
"./core/events/events_block_change.js",
"./core/events/events_var_rename.js",
"./core/events/events_var_delete.js",
"./core/variable_map.js",
"./core/names.js",
"./core/events/events_ui_base.js",
"./core/events/events_bubble_open.js",
"./core/procedures.js",
@@ -234,31 +234,32 @@
"./core/utils/style.js",
"./core/utils/deprecation.js",
"./core/utils/svg_math.js",
"./core/bubble_dragger.js",
"./core/connection_type.js",
"./core/internal_constants.js",
"./core/constants.js",
"./core/block_svg.js",
"./core/block_animations.js",
"./core/gesture.js",
"./core/touch.js",
"./core/browser_events.js",
"./core/tooltip.js",
"./core/block_svg.js",
"./core/events/events_block_base.js",
"./core/events/events_block_change.js",
"./core/utils/xml.js",
"./core/mutator.js",
"./core/field.js",
"./core/field_registry.js",
"./core/utils/aria.js",
"./core/field_dropdown.js",
"./core/msg.js",
"./core/utils/colour.js",
"./core/utils/parsing.js",
"./core/extensions.js",
"./core/block.js",
"./core/utils/string.js",
"./core/utils/object.js",
"./core/dialog.js",
"./core/utils/xml.js",
"./core/events/events_var_base.js",
"./core/events/events_var_create.js",
"./core/variable_model.js",
"./core/variables.js",
"./core/utils/object.js",
"./core/events/events_abstract.js",
"./core/registry.js",
"./core/events/utils.js",

View File

@@ -67,20 +67,20 @@ goog.addDependency('../../core/events/events_var_rename.js', ['Blockly.Events.Va
goog.addDependency('../../core/events/events_viewport.js', ['Blockly.Events.ViewportChange'], ['Blockly.Events.UiBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/events/utils.js', ['Blockly.Events.utils'], ['Blockly.registry', 'Blockly.utils.idGenerator'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/events/workspace_events.js', ['Blockly.Events.FinishedLoading'], ['Blockly.Events.Abstract', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/extensions.js', ['Blockly.Extensions'], ['Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field.js', ['Blockly.Field'], ['Blockly.DropDownDiv', 'Blockly.Events.BlockChange', 'Blockly.Events.utils', 'Blockly.Gesture', 'Blockly.IASTNodeLocationSvg', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.IKeyboardAccessible', 'Blockly.IRegistrable', 'Blockly.MarkerManager', 'Blockly.Tooltip', 'Blockly.WidgetDiv', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.parsing', 'Blockly.utils.style', 'Blockly.utils.userAgent', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_angle.js', ['Blockly.FieldAngle'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.FieldTextInput', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_checkbox.js', ['Blockly.FieldCheckbox'], ['Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.dom', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_colour.js', ['Blockly.FieldColour'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.browserEvents', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Size', 'Blockly.utils.aria', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.idGenerator', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_dropdown.js', ['Blockly.FieldDropdown'], ['Blockly.DropDownDiv', 'Blockly.Field', 'Blockly.Menu', 'Blockly.MenuItem', 'Blockly.fieldRegistry', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing', 'Blockly.utils.string', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_image.js', ['Blockly.FieldImage'], ['Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_label.js', ['Blockly.FieldLabel'], ['Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_label_serializable.js', ['Blockly.FieldLabelSerializable'], ['Blockly.FieldLabel', 'Blockly.fieldRegistry', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/extensions.js', ['Blockly.Extensions'], ['Blockly.FieldDropdown', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field.js', ['Blockly.Field'], ['Blockly.DropDownDiv', 'Blockly.Events.BlockChange', 'Blockly.Events.utils', 'Blockly.Gesture', 'Blockly.IASTNodeLocationSvg', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.IKeyboardAccessible', 'Blockly.IRegistrable', 'Blockly.MarkerManager', 'Blockly.Tooltip', 'Blockly.WidgetDiv', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.utils.Rect', 'Blockly.utils.Sentinel', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.parsing', 'Blockly.utils.style', 'Blockly.utils.userAgent', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_angle.js', ['Blockly.FieldAngle'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.Field', 'Blockly.FieldTextInput', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_checkbox.js', ['Blockly.FieldCheckbox'], ['Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_colour.js', ['Blockly.FieldColour'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.browserEvents', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Size', 'Blockly.utils.aria', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.idGenerator'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_dropdown.js', ['Blockly.FieldDropdown'], ['Blockly.DropDownDiv', 'Blockly.Field', 'Blockly.Menu', 'Blockly.MenuItem', 'Blockly.fieldRegistry', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.parsing', 'Blockly.utils.string', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_image.js', ['Blockly.FieldImage'], ['Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_label.js', ['Blockly.FieldLabel'], ['Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.dom', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_label_serializable.js', ['Blockly.FieldLabelSerializable'], ['Blockly.FieldLabel', 'Blockly.fieldRegistry', 'Blockly.utils.parsing'], {'lang': 'es_2020', 'module': 'goog'});
goog.addDependency('../../core/field_multilineinput.js', ['Blockly.FieldMultilineInput'], ['Blockly.Css', 'Blockly.Field', 'Blockly.FieldTextInput', 'Blockly.WidgetDiv', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Svg', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.parsing', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_number.js', ['Blockly.FieldNumber'], ['Blockly.FieldTextInput', 'Blockly.fieldRegistry', 'Blockly.utils.aria', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_number.js', ['Blockly.FieldNumber'], ['Blockly.Field', 'Blockly.FieldTextInput', 'Blockly.fieldRegistry', 'Blockly.utils.aria'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_registry.js', ['Blockly.fieldRegistry'], ['Blockly.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_textinput.js', ['Blockly.FieldTextInput'], ['Blockly.DropDownDiv', 'Blockly.Events.BlockChange', 'Blockly.Events.utils', 'Blockly.Field', 'Blockly.Msg', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.dialog', 'Blockly.fieldRegistry', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.parsing', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_variable.js', ['Blockly.FieldVariable'], ['Blockly.Events.BlockChange', 'Blockly.FieldDropdown', 'Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.Xml', 'Blockly.fieldRegistry', 'Blockly.internalConstants', 'Blockly.utils.Size', 'Blockly.utils.object', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_textinput.js', ['Blockly.FieldTextInput'], ['Blockly.DropDownDiv', 'Blockly.Events.BlockChange', 'Blockly.Events.utils', 'Blockly.Field', 'Blockly.Msg', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.dialog', 'Blockly.fieldRegistry', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.parsing', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_variable.js', ['Blockly.FieldVariable'], ['Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.FieldDropdown', 'Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.Xml', 'Blockly.fieldRegistry', 'Blockly.internalConstants', 'Blockly.utils.Size', 'Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.Events.utils', 'Blockly.FlyoutMetricsManager', 'Blockly.Gesture', 'Blockly.IFlyout', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.Variables', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.common', 'Blockly.serialization.blocks', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.idGenerator', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/flyout_button.js', ['Blockly.FlyoutButton'], ['Blockly.Css', 'Blockly.browserEvents', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.parsing', 'Blockly.utils.style'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/flyout_horizontal.js', ['Blockly.HorizontalFlyout'], ['Blockly.DropDownDiv', 'Blockly.Flyout', 'Blockly.Scrollbar', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.registry', 'Blockly.utils.Rect', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});
@@ -242,6 +242,7 @@ goog.addDependency('../../core/utils/metrics.js', ['Blockly.utils.Metrics'], [],
goog.addDependency('../../core/utils/object.js', ['Blockly.utils.object'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/utils/parsing.js', ['Blockly.utils.parsing'], ['Blockly.Msg', 'Blockly.utils.colour', 'Blockly.utils.string'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/utils/rect.js', ['Blockly.utils.Rect'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/utils/sentinel.js', ['Blockly.utils.Sentinel'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/utils/size.js', ['Blockly.utils.Size'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/utils/string.js', ['Blockly.utils.string'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/utils/style.js', ['Blockly.utils.style'], ['Blockly.utils.Coordinate', 'Blockly.utils.Size'], {'lang': 'es6', 'module': 'goog'});

View File

@@ -10,13 +10,15 @@ const {createDeprecationWarningStub, sharedTestSetup, sharedTestTeardown} = goog
suite('Field Registry', function() {
function CustomFieldType(value) {
CustomFieldType.superClass_.constructor.call(this, value);
class CustomFieldType extends Blockly.Field {
constructor(value) {
super(value);
}
static fromJson(options) {
return new CustomFieldType(options['value']);
}
}
Blockly.utils.object.inherits(CustomFieldType, Blockly.Field);
CustomFieldType.fromJson = function(options) {
return new CustomFieldType(options['value']);
};
setup(function() {
sharedTestSetup.call(this);

View File

@@ -20,29 +20,40 @@ suite('Abstract Fields', function() {
suite('Is Serializable', function() {
// Both EDITABLE and SERIALIZABLE are default.
function FieldDefault() {
this.name = 'NAME';
class FieldDefault extends Blockly.Field {
constructor() {
super();
this.name = 'NAME';
}
}
FieldDefault.prototype = Object.create(Blockly.Field.prototype);
// EDITABLE is false and SERIALIZABLE is default.
function FieldFalseDefault() {
this.name = 'NAME';
class FieldFalseDefault extends Blockly.Field {
constructor() {
super();
this.name = 'NAME';
this.EDITABLE = false;
}
}
FieldFalseDefault.prototype = Object.create(Blockly.Field.prototype);
FieldFalseDefault.prototype.EDITABLE = false;
// EDITABLE is default and SERIALIZABLE is true.
function FieldDefaultTrue() {
this.name = 'NAME';
class FieldDefaultTrue extends Blockly.Field {
constructor() {
super();
this.name = 'NAME';
this.SERIALIZABLE = true;
}
}
FieldDefaultTrue.prototype = Object.create(Blockly.Field.prototype);
FieldDefaultTrue.prototype.SERIALIZABLE = true;
// EDITABLE is false and SERIALIZABLE is true.
function FieldFalseTrue() {
this.name = 'NAME';
class FieldFalseTrue extends Blockly.Field {
constructor() {
super();
this.name = 'NAME';
this.EDITABLE = false;
this.SERIALIZABLE = true;
}
}
FieldFalseTrue.prototype = Object.create(Blockly.Field.prototype);
FieldFalseTrue.prototype.EDITABLE = false;
FieldFalseTrue.prototype.SERIALIZABLE = true;
/* Test Backwards Compatibility */
test('Editable Default(true), Serializable Default(false)', function() {
@@ -585,14 +596,15 @@ suite('Abstract Fields', function() {
suite('Customization', function() {
// All this field does is wrap the abstract field.
function CustomField(opt_config) {
CustomField.superClass_.constructor.call(
this, 'value', null, opt_config);
class CustomField extends Blockly.Field {
constructor(opt_config) {
super('value', null, opt_config);
}
static fromJson(options) {
return new CustomField(options);
}
}
Blockly.utils.object.inherits(CustomField, Blockly.Field);
CustomField.fromJson = function(options) {
return new CustomField(options);
};
suite('Tooltip', function() {
test('JS Constructor', function() {