mirror of
https://github.com/google/blockly.git
synced 2026-03-09 14:50:09 +01:00
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:
@@ -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
|
||||
|
||||
2247
core/field.js
2247
core/field.js
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
1000
core/field_colour.js
1000
core/field_colour.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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
24
core/utils/sentinel.js
Normal 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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user