diff --git a/blocks/loops.js b/blocks/loops.js index d9d8cbfcd..b9bae1e80 100644 --- a/blocks/loops.js +++ b/blocks/loops.js @@ -73,7 +73,9 @@ Blockly.Blocks['controls_repeat'] = { { "type": "field_number", "name": "TIMES", - "text": "10" + "value": 10, + "min": 0, + "precision": 1 } ], "previousStatement": null, @@ -84,8 +86,6 @@ Blockly.Blocks['controls_repeat'] = { }); this.appendStatementInput('DO') .appendField(Blockly.Msg.CONTROLS_REPEAT_INPUT_DO); - this.getField('TIMES').setValidator( - Blockly.FieldTextInput.nonnegativeIntegerValidator); } }; diff --git a/blocks/math.js b/blocks/math.js index 311d9875b..475fd6ce2 100644 --- a/blocks/math.js +++ b/blocks/math.js @@ -43,8 +43,7 @@ Blockly.Blocks['math_number'] = { this.setHelpUrl(Blockly.Msg.MATH_NUMBER_HELPURL); this.setColour(Blockly.Blocks.math.HUE); this.appendDummyInput() - .appendField(new Blockly.FieldNumber('0', - Blockly.FieldTextInput.numberValidator), 'NUM'); + .appendField(new Blockly.FieldNumber('0'), 'NUM'); this.setOutput(true, 'Number'); // Assign 'this' to a variable for use in the tooltip closure below. var thisBlock = this; diff --git a/core/block.js b/core/block.js index bf6041416..36a703171 100644 --- a/core/block.js +++ b/core/block.js @@ -1111,7 +1111,10 @@ Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { element['width'], element['height'], element['alt']); break; case 'field_number': - field = new Blockly.FieldNumber(element['text']); + field = new Blockly.FieldNumber(element['value']); + field.setPrecision(element['precision']); + field.setMin(element['min']); + field.setMax(element['max']); break; case 'field_date': if (Blockly.FieldDate) { diff --git a/core/field_angle.js b/core/field_angle.js index b5ca196ce..35534a426 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -305,16 +305,19 @@ Blockly.FieldAngle.prototype.updateGraph_ = function() { * @return {?string} A string representing a valid angle, or null if invalid. */ Blockly.FieldAngle.angleValidator = function(text) { - var n = Blockly.FieldTextInput.numberValidator(text); - if (n !== null) { - n = n % 360; - if (n < 0) { - n += 360; - } - if (n > Blockly.FieldAngle.WRAP) { - n -= 360; - } - n = String(n); + if (text === null) { + return null; } - return n; + var n = parseFloat(text || 0); + if (isNaN(n)) { + return null; + } + n = n % 360; + if (n < 0) { + n += 360; + } + if (n > Blockly.FieldAngle.WRAP) { + n -= 360; + } + return String(n); }; diff --git a/core/field_number.js b/core/field_number.js index eb92291bf..0ce38bf1e 100644 --- a/core/field_number.js +++ b/core/field_number.js @@ -27,10 +27,11 @@ goog.provide('Blockly.FieldNumber'); goog.require('Blockly.FieldTextInput'); +goog.require('goog.math'); /** * Class for an editable number field. - * @param {string} text The initial content of the field. + * @param {string} value The initial content of the field. * @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 @@ -38,8 +39,122 @@ goog.require('Blockly.FieldTextInput'); * @extends {Blockly.FieldTextInput} * @constructor */ -Blockly.FieldNumber = function(text, opt_validator) { - Blockly.FieldNumber.superClass_.constructor.call(this, text, - opt_validator); +Blockly.FieldNumber = function(value, opt_validator) { + Blockly.FieldNumber.superClass_.constructor.call(this, value, opt_validator); }; goog.inherits(Blockly.FieldNumber, Blockly.FieldTextInput); + +/** + * Steps between allowed numbers. + * @private + * @type {number} + */ +Blockly.FieldNumber.prototype.precision_ = 0; + +/** + * Minimum allowed value. + * @private + * @type {number} + */ +Blockly.FieldNumber.prototype.min_ = -Infinity; + +/** + * Maximum allowed value. + * @private + * @type {number} + */ +Blockly.FieldNumber.prototype.max_ = Infinity; + +/** + * 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} precision Precision for value. + */ +Blockly.FieldNumber.prototype.setPrecision = function(precision) { + precision = parseFloat(precision); + if (!isNaN(precision)) { + this.precision_ = precision; + } +}; + +/** + * Set a maximum limit on this field's value. + * @param {number|string|undefined} max Maximum value. + */ +Blockly.FieldNumber.prototype.setMin = function(min) { + min = parseFloat(min); + if (!isNaN(min)) { + this.min_ = min; + } +}; + +/** + * Set a maximum limit on this field's value. + * @param {number|string|undefined} max Minimum value. + */ +Blockly.FieldNumber.prototype.setMax = function(max) { + max = parseFloat(max); + if (!isNaN(max)) { + this.max_ = max; + } +}; + +/** + * Sets a new change handler for number field. + * @param {Function} handler New change handler, or null. + */ +Blockly.FieldNumber.prototype.setValidator = function(handler) { + var wrappedHandler; + if (handler) { + // Wrap the user's change handler together with the angle validator. + wrappedHandler = function(value) { + var v1 = handler.call(this, value); + if (v1 === null) { + var v2 = v1; + } else { + if (v1 === undefined) { + v1 = value; + } + var v2 = Blockly.FieldNumber.numberValidator.call(this, v1); + if (v2 === undefined) { + v2 = v1; + } + } + return v2 === value ? undefined : v2; + }; + } else { + wrappedHandler = Blockly.FieldNumber.numberValidator; + } + Blockly.FieldNumber.superClass_.setValidator.call(this, wrappedHandler); +}; + +/** + * Ensure that only a number in the correct range may be entered. + * @param {string} text The user's text. + * @return {?string} A string representing a valid number, or null if invalid. + */ +Blockly.FieldNumber.numberValidator = function(text) { + if (text === null) { + return null; + } + text = String(text); + // TODO: Handle cases like 'ten', '1.203,14', etc. + // 'O' is sometimes mistaken for '0' by inexperienced users. + text = text.replace(/O/ig, '0'); + // Strip out thousands separators. + text = text.replace(/,/g, ''); + var n = parseFloat(text || 0); + if (isNaN(n)) { + // Invalid number. + return null; + } + // Round to nearest multiple of precision. + if (this.precision_ && Number.isFinite(n)) { + n = Math.round(n / this.precision_) * this.precision_; + } + // Get the value in range. + n = goog.math.clamp(n, this.min_, this.max_); + return String(n); +}; diff --git a/core/field_textinput.js b/core/field_textinput.js index 994db7263..6ef6bf1d2 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -296,6 +296,8 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() { * @return {?string} A string representing a valid number, or null if invalid. */ Blockly.FieldTextInput.numberValidator = function(text) { + console.warn('Blockly.FieldTextInput.numberValidator is deprecated. ' + + 'Use Blockly.FieldNumber instead.'); if (text === null) { return null; } diff --git a/demos/blockfactory/blocks.js b/demos/blockfactory/blocks.js index ee7cc285a..f2d8dd290 100644 --- a/demos/blockfactory/blocks.js +++ b/demos/blockfactory/blocks.js @@ -470,11 +470,9 @@ Blockly.Blocks['field_image'] = { .appendField(new Blockly.FieldTextInput(src), 'SRC'); this.appendDummyInput() .appendField('width') - .appendField(new Blockly.FieldTextInput('15', - Blockly.FieldTextInput.numberValidator), 'WIDTH') + .appendField(new Blockly.FieldNumber('15'), 'WIDTH') .appendField('height') - .appendField(new Blockly.FieldTextInput('15', - Blockly.FieldTextInput.numberValidator), 'HEIGHT') + .appendField(new Blockly.FieldNumber('15'), 'HEIGHT') .appendField('alt text') .appendField(new Blockly.FieldTextInput('*'), 'ALT'); this.setPreviousStatement(true, 'Field');